From d3cdbae073ff5fc65c0e83679b33571576036d08 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Tue, 3 Mar 2026 22:23:32 -0500 Subject: [PATCH 01/81] DataManagers fixes --- .../raw_code/DataManagers/DataManagers.py | 12 +- trade/backtester_/_helper.py | 12 +- trade/backtester_/backtester_.py | 37 +++++- trade/backtester_/data.py | 7 +- trade/backtester_/utils/aggregators.py | 121 +++++++++++------- 5 files changed, 135 insertions(+), 54 deletions(-) diff --git a/module_test/raw_code/DataManagers/DataManagers.py b/module_test/raw_code/DataManagers/DataManagers.py index 0a66f41..300ca00 100644 --- a/module_test/raw_code/DataManagers/DataManagers.py +++ b/module_test/raw_code/DataManagers/DataManagers.py @@ -347,10 +347,14 @@ def query_thetadata(self, data = pd.DataFrame(columns=THETA_DATA_COLUMNS) return data data = data[~data.index.duplicated(keep='first')] - open_interest = retrieve_openInterest(symbol=self.symbol, end_date=end, exp=exp, right=right, start_date=start, strike=strike, print_url=print_url).set_index('Datetime') - open_interest.drop_duplicates(inplace = True) - data['Open_interest'] = open_interest['Open_interest'] - data.index = default_timestamp(data.index) + try: + open_interest = retrieve_openInterest(symbol=self.symbol, end_date=end, exp=exp, right=right, start_date=start, strike=strike, print_url=print_url).set_index('Datetime') + open_interest.drop_duplicates(inplace = True) + data['Open_interest'] = open_interest['Open_interest'] + data.index = default_timestamp(data.index) + except Exception as e: + logger.error(f"Error retrieving open interest data for {self.symbol} from {start} to {end}: {e}. Filling Open_interest with NaN.") + data['Open_interest'] = np.nan return data else: diff --git a/trade/backtester_/_helper.py b/trade/backtester_/_helper.py index b26933d..c1a2525 100644 --- a/trade/backtester_/_helper.py +++ b/trade/backtester_/_helper.py @@ -1,3 +1,4 @@ + from typing import Any, Callable, Dict, Optional, TYPE_CHECKING import pandas as pd from .data import PTDataset @@ -86,7 +87,16 @@ def _next(self): if verbose: print(f"Opening position on {date} at price {self.data.Close[-1]}") print(f"Info: {self.brain.info_on_date(date=date)}") - self.buy() + if open_decision.side == 1: + if verbose: + print("Going LONG") + self.buy() + elif open_decision.side == -1: + if verbose: + print("Going SHORT") + self.sell() + else: + raise ValueError(f"Invalid side in open_decision: {open_decision.side}") self.brain.open_action( date=date, signal_id=open_decision.signal_id, diff --git a/trade/backtester_/backtester_.py b/trade/backtester_/backtester_.py index f84a695..4779788 100644 --- a/trade/backtester_/backtester_.py +++ b/trade/backtester_/backtester_.py @@ -104,11 +104,11 @@ def __init__(self, raise ValueError("PTBacktester currently only supports trade_on_close=True. trade_on_close=False is not supported.") else: logger.info(f"PTBacktester initialized with trade_on_close={trade_on_close}, finalize_trades={finalize_trades}") - self.datasets = [] + self.datasets: List[PTDataset] = [] self.__strategy = deepcopy((_setup_strategy(strategy, start_date=start_overwrite, - verbose=kwargs.get('verbose', False), - plot_indicators=kwargs.get('plot_indicators', True)))) + verbose=kwargs.pop('verbose', False), + plot_indicators=kwargs.pop('plot_indicators', True)))) self.__port_stats = None self._trades = None self._equity = None @@ -190,7 +190,7 @@ def run(self) -> pd.DataFrame: """ results = [] for i, d in enumerate(self.datasets): - d.backtest._strategy._name = d.name + d.backtest._strategy._name = d.name d.backtest._strategy._runIndex = i if d.param_settings: # if d.param_settings: @@ -228,6 +228,27 @@ def pf_value_ts(self) -> pd.DataFrame: """ Returns Timeseries of periodic portfolio value """ + + ## Initialize empty dataframe to hold equity curves for each ticker. + eq = pd.DataFrame() + + ## If cash is a single value, we create a dictionary with the same cash for each ticker to fill in missing values in the equity curve + if isinstance(self.cash, (float, int)): + cash_per_asset = {tick: self.cash for tick in self.get_port_stats().keys()} + + ## Loop through each ticker's stats, extract the equity curve, and fill in missing values with the initial cash + for tick, stats in self.get_port_stats().items(): + eq[tick] = stats["_equity_curve"]["Equity"] + + ## If cash is a dict, we use the specific cash for that ticker to fill in missing values. If not, we use the single cash value for all tickers. + if tick not in cash_per_asset: + raise ValueError(f"Cash value for ticker {tick} not found in cash_per_asset. Please provide a cash value for this ticker.") + eq[tick].fillna(cash_per_asset[tick], inplace=True) + eq["Total"] = eq.sum(axis=1) + + return eq + + PortStats = self.__port_stats if self.start_overwrite: start = pd.to_datetime(self.start_overwrite).date() @@ -399,12 +420,20 @@ def position_optimize(self, default_params = {} for param in param_kwargs.keys(): default_params[param] = getattr(self.strategy, param) + ## Loop through each datasets backtest, optimize & append to optimized dataframe for dataset in self.datasets: name = dataset.name + + # If the dataset has specific settings for the strategy, we set the strategy settings. + print(f"Optimizing for dataset: {name} with settings: {dataset.param_settings}") + for setting, value in dataset.param_settings.items(): + setattr(self.strategy, setting, value) ## Make sure strategy name is set for each optimize run dataset.backtest._strategy._name = name + + if return_heatmap: opt, hm = dataset.backtest.optimize(**param_kwargs, **kwargs) diff --git a/trade/backtester_/data.py b/trade/backtester_/data.py index 14a8437..49ec42a 100644 --- a/trade/backtester_/data.py +++ b/trade/backtester_/data.py @@ -1,5 +1,6 @@ import pandas as pd - +from backtesting import Backtest +from typing import Optional class PTDataset: """ Custom dataset holding ticker name, ticker timeseries & backtest object from backtesting.py @@ -14,7 +15,7 @@ def __init__(self, name: str, data: pd.DataFrame, param_settings: dict = None): self.__param_settings = param_settings ## Making param_settings private self.name = name self.data = data - self.backtest = None + self.backtest: Optional[Backtest] = None def __repr__(self): return f"PTDataset({self.name})" @@ -30,4 +31,6 @@ def param_settings(self): @param_settings.setter def param_settings(self, value: dict): """Setter for param_settings with type checking""" + if not isinstance(value, dict): + raise TypeError("param_settings must be a dictionary") self.__param_settings = value diff --git a/trade/backtester_/utils/aggregators.py b/trade/backtester_/utils/aggregators.py index a91067d..80a5eac 100644 --- a/trade/backtester_/utils/aggregators.py +++ b/trade/backtester_/utils/aggregators.py @@ -417,6 +417,21 @@ def bestTrade(trades_df: pd.DataFrame) -> float: def worstTrade(trades_df) -> float: return trades_df.ReturnPct.min()*100 +def trade_percentile(trades_df: pd.DataFrame, percentile: float) -> float: + """ + Returns the PnL or ReturnPct value at a given percentile. Eg: 5th percentile would give the value at which 5% of the trades are worse than that value + + Parameters: + trades_df (pd.DataFrame): DataFrame Contatining Trades & PnL or ReturnPct column + percentile (float): A value between 0 and 100 representing the desired percentile + + Returns: + float: Corresponding PnL or ReturnPct value at the given percentile + """ + assert 'PnL' in trades_df.columns or 'ReturnPct' in trades_df.columns, f"Please pass a dataframe holding trades and ensure it has either 'PnL' or 'ReturnPct' in the columns. Current Columns {trades_df.columns}" + assert 0 <= percentile <= 100, f"Percentile must be between 0 and 100. Current Value: {percentile}" + return np.percentile(trades_df.ReturnPct, percentile)*100 if 'ReturnPct' in trades_df.columns else np.percentile(trades_df.PnL, percentile) + def profitFactor(trades_df: pd.DataFrame) -> float: """ @@ -755,6 +770,18 @@ def winRate(self) -> float: def lossRate(self) -> float: return round((100 - winRate(self._trades)), 2) + def trade_percentile(self, percentile: float) -> float: + """ + Returns the PnL or ReturnPct value at a given percentile. Eg: 5th percentile would give the value at which 5% of the trades are worse than that value + + Parameters: + percentile (float): A value between 0 and 100 representing the desired percentile + + Returns: + float: Corresponding PnL or ReturnPct value at the given percentile + """ + return trade_percentile(self._trades, percentile) + def avgPnL(self, Type_: str, value=True) -> float: """ params: @@ -910,49 +937,57 @@ def aggregate(self, raise Exception('Either implement datasets attribute with PTDataset or self.symbol_list') rtrn_ = self.rtrn() - series1 = pd.Series({ - 'Start': start_overwrite if start_overwrite else self.dates_(True), - 'End': self.dates_(False), - 'Duration': self.dates_(False) - self.dates_(True), - 'Exposure Time [%]': self.ExposureDays(), - 'Equity Final [$]': self.final_value_func(), - 'Equity Peak [$]': self.peak_value_func(), - 'Return [%]': rtrn_, - 'Buy & Hold Return [%]': self.buyNhold(), - 'Median Daily Return [%]': f"{self.daily_rtrns().median(): .4%}", - 'VaR 95% [%]': f"{self.daily_rtrns().quantile(0.05): .2%}", - 'VaR 05% [%]': f"{self.daily_rtrns().quantile(0.95): .2%}", - 'CAGR [%]': self.cagr(), - 'Volatility Ann. [%]': self.vol_annualized(), - 'Sharpe Ratio': self.sharpe(risk_free_rate), - 'Sortino Ratio': self.sortino(risk_free_rate, MAR), - 'Skew': self._equity.Total.pct_change().skew(), - 'Log Return Skew': np.log(self._equity.Total/self._equity.Total.shift(1)).skew(), - 'Calmar Ratio': self.calmar(), - 'Max. Drawdown [%]': self.mdd(), - 'Max. Drawdown Value [$]': self.mdd_value(), - 'Avg. Drawdown [%]': self.avg_dd_percent(), - 'Max. Drawdown Duration': self.mdd_duration(), - 'Avg Dradown Duration': self.avg_dd_duration(), - '# Trades': self.numOfTrades(), - 'Win Rate [%]': self.winRate(), - 'Lose Rate [%]': self.lossRate(), - 'Avg. Trade [%]': self.avgPnL('A', False), - 'Avg. Winning Trade [%]': self.avgPnL('W', False), - 'Avg. Losing Trade [%]': self.avgPnL('L', False), - 'Best Trade [%]': self.bestTrade(), - 'Worst Trade [%]': self.worstTrade(), - 'Avg Trade Duration': self.holding_period(np.mean, 'A'), - 'Avg Win Trade Duration': self.holding_period(np.mean, 'W'), - 'Avg Lose Duration': self.holding_period(np.mean, 'L'), - 'Max Trade Duration': self.holding_period(np.max, 'A'), - 'Max Win Trade Duration': self.holding_period(np.max, 'W'), - 'Max Lose Duration': self.holding_period(np.max, 'L'), - 'Profit Factor': self.profitFactor(), - 'Expectancy [%]': self.Expectancy(), - 'SQN': self.SQN() - - }) + series1 = pd.Series( + { + "Start": start_overwrite if start_overwrite else self.dates_(True), + "End": self.dates_(False), + "Duration": self.dates_(False) - self.dates_(True), + "Exposure Time [%]": self.ExposureDays(), + "Equity Final [$]": self.final_value_func(), + "Equity Peak [$]": self.peak_value_func(), + "Return [%]": rtrn_, + "Buy & Hold Return [%]": self.buyNhold(), + "Median Daily Return [%]": f"{self.daily_rtrns().median(): .4%}", + "VaR 95% [%]": f"{self.daily_rtrns().quantile(0.05): .2%}", + "VaR 05% [%]": f"{self.daily_rtrns().quantile(0.95): .2%}", + "CAGR [%]": self.cagr(), + "Volatility Ann. [%]": self.vol_annualized(), + "Sharpe Ratio": self.sharpe(risk_free_rate), + "Sortino Ratio": self.sortino(risk_free_rate, MAR), + "Skew": self._equity.Total.pct_change().skew(), + "Log Return Skew": np.log( + self._equity.Total / self._equity.Total.shift(1) + ).skew(), + "Calmar Ratio": self.calmar(), + "Max. Drawdown [%]": self.mdd(), + "Max. Drawdown Value [$]": self.mdd_value(), + "Avg. Drawdown [%]": self.avg_dd_percent(), + "Max. Drawdown Duration": self.mdd_duration(), + "Avg Dradown Duration": self.avg_dd_duration(), + "# Trades": self.numOfTrades(), + "Win Rate [%]": self.winRate(), + "Lose Rate [%]": self.lossRate(), + "Avg. Trade [%]": self.avgPnL("A", False), + "Avg. Winning Trade [%]": self.avgPnL("W", False), + "Avg. Losing Trade [%]": self.avgPnL("L", False), + "Best Trade [%]": self.bestTrade(), + "Worst Trade [%]": self.worstTrade(), + "5th Percentile Trade [%]": self.trade_percentile(5), + "25th Percentile Trade [%]": self.trade_percentile(25), + "50th Percentile Trade [%]": self.trade_percentile(50), + "75th Percentile Trade [%]": self.trade_percentile(75), + "95th Percentile Trade [%]": self.trade_percentile(95), + "Avg Trade Duration": self.holding_period(np.mean, "A"), + "Avg Win Trade Duration": self.holding_period(np.mean, "W"), + "Avg Lose Duration": self.holding_period(np.mean, "L"), + "Max Trade Duration": self.holding_period(np.max, "A"), + "Max Win Trade Duration": self.holding_period(np.max, "W"), + "Max Lose Duration": self.holding_period(np.max, "L"), + "Profit Factor": self.profitFactor(), + "Expectancy [%]": self.Expectancy(), + "SQN": self.SQN(), + } + ) rtrn_dict = self.yearly_retrns() rtrn_series = pd.Series( From 15866f1735eec23cf51097eaab6aad072f2d7f8a Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sat, 14 Mar 2026 20:47:06 -0400 Subject: [PATCH 02/81] stuff are done --- trade/backtester_/_strategy.py | 30 +- trade/backtester_/backtester_.py | 26 +- trade/backtester_/data.py | 11 +- trade/backtester_/utils/aggregators.py | 447 +++++++++++++++---------- trade/helpers/helper_types.py | 46 ++- 5 files changed, 367 insertions(+), 193 deletions(-) diff --git a/trade/backtester_/_strategy.py b/trade/backtester_/_strategy.py index a5c53c2..a3b4bdf 100644 --- a/trade/backtester_/_strategy.py +++ b/trade/backtester_/_strategy.py @@ -14,6 +14,8 @@ from ._types import Side, SideInt # noqa from trade.backtester_.indicators import ( compute_atr_loss, + update_atr_trail_long, + update_atr_trail_short, ) @@ -270,7 +272,7 @@ def __init__( self.position_info: Optional[PositionInfo] = PositionInfo() self.stop: Optional[float] = None - self.indicators: Dict[str, Any] = {} + self.indicators: Dict[str, Indicator] = {} # Cache index + numpy views for speed and consistent date handling self._df = self.data.data.copy() # expects a DataFrame-like @@ -1179,7 +1181,7 @@ def plot_strategy_indicators(self, log_scale: bool = True, add_signal_marker: bo ) ## Indicators for ind_name, indicator in self.indicators.items(): - ind_values = indicator.values.loc[df.index] + ind_values = indicator.series[df.index] if indicator.overlay: fig.add_trace( go.Scatter( @@ -1281,6 +1283,8 @@ def __init__( average_type: str = "w", start_trading_date: Optional[str] = None, ticker: Optional[str] = None, + is_long: bool = True, + is_short: bool = False, **kwargs, ): super().__init__(data=data, **kwargs) @@ -1288,6 +1292,8 @@ def __init__( self.atr_factor = atr_factor self.trail_type = trail_type self.average_type = average_type + self.is_long = is_long + self.is_short = is_short self.loss_series: Optional[pd.Series] = None def setup(self) -> None: @@ -1298,4 +1304,22 @@ def setup(self) -> None: atr_factor=self.atr_factor, trail_type=self.trail_type, average_type=self.average_type, - ) \ No newline at end of file + ) + + def open_action(self, *, signal_id = None, entry_price = None, side = None, date = None, index = None): + idx, _ = self._resolve(date=date, index=index) + + if self.is_long: + self.stop = update_atr_trail_long( + close=float(self.close[idx]), + loss=float(self.loss_series[idx]), + prev_trail=self.stop, + reset=False, + ) + elif self.is_short: + self.stop = update_atr_trail_short( + close=float(self.close[idx]), + loss=float(self.loss_series[idx]), + prev_trail=self.stop, + reset=False, + ) \ No newline at end of file diff --git a/trade/backtester_/backtester_.py b/trade/backtester_/backtester_.py index 4779788..09796dd 100644 --- a/trade/backtester_/backtester_.py +++ b/trade/backtester_/backtester_.py @@ -11,7 +11,7 @@ from .data import PTDataset # noqa: F401 from ._helper import make_bt_wrapper # noqa: F401 from ._strategy import StrategyBase -logger = setup_logger('trade.backtester_.backtester_', stream_log_level="DEBUG") +logger = setup_logger('trade.backtester_.backtester_', stream_log_level="WARNING") ## TODO: Include Benchmark DD in Portfolio Plot ## FIX: After optimization, reset strategy settings to default. Currently, it is not resetting @@ -89,6 +89,11 @@ def __init__(self, This is useful for when you have a strategy that starts at a certain date, which is greater than the earliest date in the dataset. Typically to allow buffer to calculate indicators. **kwargs: Additional keyword arguments to be passed to the Backtest class in backtesting.py + which include: `trade_on_close` (bool): Whether to trade on close or not. Currently, PTBacktester only supports trade_on_close=True. trade_on_close=False is not supported. + `finalize_trades` (bool): Whether to finalize trades or not. This is useful for when you want to keep trades open after the backtest is done. Defaults to True. + `verbose` (bool): Whether to print verbose logs during strategy setup. Defaults to False. + `plot_indicators` (bool): Whether to plot indicators in the backtest plot. Defaults to True. + `commission` (float): Commission to be passed to the Backtest class in backtesting.py. Defaults to 0.0. Returns: None @@ -98,6 +103,9 @@ def __init__(self, - _runIndex: Index of the ticker in the dataset list """ + if kwargs.pop("reset", True): + for dataset in datalist: + dataset.reset() trade_on_close = kwargs.pop("trade_on_close", True) finalize_trades = kwargs.pop("finalize_trades", True) if not trade_on_close: @@ -161,7 +169,7 @@ def update_settings(self, datalist) -> None: raise ValueError(f'For datasets settings, please assign a dictionary containing parameters as key and values, got {type(param_setting)}') dataset_obj = [x for x in datalist if x.name == name][0] dataset_obj.param_settings = param_setting - except: + except: # noqa ## Use default settings as settings in not given names dataset_obj.param_settings = default_setting no_settings_names.append(name) @@ -205,11 +213,11 @@ def run(self) -> pd.DataFrame: self.reset_settings() if d.param_settings else None try: del d.backtest._strategy._name - except: + except: # noqa pass try: del d.backtest._strategy._runIndex - except: + except: # noqa pass results.append(stats) self.__port_stats = {d.name: results[i] for i, d in enumerate(self.datasets)} @@ -245,6 +253,9 @@ def pf_value_ts(self) -> pd.DataFrame: raise ValueError(f"Cash value for ticker {tick} not found in cash_per_asset. Please provide a cash value for this ticker.") eq[tick].fillna(cash_per_asset[tick], inplace=True) eq["Total"] = eq.sum(axis=1) + eq.index = pd.to_datetime(eq.index) + if self.start_overwrite: + eq = eq[eq.index.date >= pd.to_datetime(self.start_overwrite).date()] return eq @@ -427,9 +438,10 @@ def position_optimize(self, name = dataset.name # If the dataset has specific settings for the strategy, we set the strategy settings. - print(f"Optimizing for dataset: {name} with settings: {dataset.param_settings}") - for setting, value in dataset.param_settings.items(): - setattr(self.strategy, setting, value) + if dataset.param_settings: + print(f"Optimizing for dataset: {name} with settings: {dataset.param_settings}") + for setting, value in dataset.param_settings.items(): + setattr(self.strategy, setting, value) ## Make sure strategy name is set for each optimize run dataset.backtest._strategy._name = name diff --git a/trade/backtester_/data.py b/trade/backtester_/data.py index 49ec42a..9343779 100644 --- a/trade/backtester_/data.py +++ b/trade/backtester_/data.py @@ -16,12 +16,19 @@ def __init__(self, name: str, data: pd.DataFrame, param_settings: dict = None): self.name = name self.data = data self.backtest: Optional[Backtest] = None + self.data.columns = self.data.columns.str.capitalize() # Ensure columns are capitalized + def __repr__(self): return f"PTDataset({self.name})" def __str__(self): return f"PTDataset({self.name})" + + def reset(self): + """Reset the dataset's backtest to None""" + self.backtest = None + self.param_settings = None @property def param_settings(self): @@ -31,6 +38,6 @@ def param_settings(self): @param_settings.setter def param_settings(self, value: dict): """Setter for param_settings with type checking""" - if not isinstance(value, dict): - raise TypeError("param_settings must be a dictionary") + if not isinstance(value, (dict, type(None))): + raise TypeError("param_settings must be a dictionary or None") self.__param_settings = value diff --git a/trade/backtester_/utils/aggregators.py b/trade/backtester_/utils/aggregators.py index 80a5eac..a700ba8 100644 --- a/trade/backtester_/utils/aggregators.py +++ b/trade/backtester_/utils/aggregators.py @@ -6,7 +6,7 @@ import sys import os -from trade.helpers.helper import copy_doc_from,filter_inf,filter_zeros +from trade.helpers.helper import copy_doc_from, filter_inf, filter_zeros from trade.assets.Stock import Stock from abc import ABC, abstractmethod import plotly.io as pio @@ -24,6 +24,7 @@ from backtesting import Backtest import pandas as pd + def pf_value_ts(port_stats: dict, cash: Union[dict, int, float]) -> pd.DataFrame: """ Parameters: @@ -35,12 +36,12 @@ def pf_value_ts(port_stats: dict, cash: Union[dict, int, float]) -> pd.DataFrame """ PortStats = port_stats - date_range = pd.date_range(start=dates_(True), end=dates_(False), freq='B') + date_range = pd.date_range(start=dates_(True), end=dates_(False), freq="B") start = dates_(True) end = dates_(False) port_equity_data = pd.DataFrame(index=date_range) for tick, data in PortStats.items(): - equity_curve = data['_equity_curve']['Equity'] + equity_curve = data["_equity_curve"]["Equity"].copy(deep=True) if isinstance(cash, dict): cash = cash[tick] elif isinstance(cash, int) or isinstance(cash, float): @@ -49,23 +50,23 @@ def pf_value_ts(port_stats: dict, cash: Union[dict, int, float]) -> pd.DataFrame equity_curve.name = tick tick_start = min(equity_curve.index) if tick_start > start: - temp = pd.DataFrame(index=pd.date_range( - start=start, end=equity_curve.index.min(), freq='B')) + temp = pd.DataFrame( + index=pd.date_range(start=start, end=equity_curve.index.min(), freq="B") + ) temp[tick] = cash equity_curve = pd.concat([equity_curve, temp], axis=0) port_equity_data = port_equity_data.join(equity_curve) - port_equity_data = port_equity_data.dropna(how='all') - port_equity_data = port_equity_data.fillna(method='ffill') - port_equity_data['Total'] = port_equity_data.sum(axis=1) + port_equity_data = port_equity_data.dropna(how="all") + port_equity_data = port_equity_data.fillna(method="ffill") + port_equity_data["Total"] = port_equity_data.sum(axis=1) port_equity_data.index = pd.DatetimeIndex(port_equity_data.index) return port_equity_data def short_returns(t0, t1): - return 1 - (t1/t0) - + return 1 - (t1 / t0) def dates_(port_stats: dict, start: bool = True) -> pd.Timestamp: @@ -85,14 +86,16 @@ def dates_(port_stats: dict, start: bool = True) -> pd.Timestamp: end_list = [] duration_list = [] for tick, data in port_stats.items(): - start_list.append(data['Start']) - end_list.append(data['End']) - duration_list.append(data['Duration']) + start_list.append(data["Start"]) + end_list.append(data["End"]) + duration_list.append(data["Duration"]) return min(start_list) if start else max(end_list) -def peak_value_func(equity_timeseries: pd.DataFrame, value: bool = True) -> Union[float, Dict]: +def peak_value_func( + equity_timeseries: pd.DataFrame, value: bool = True +) -> Union[float, Dict]: """ Returns the peak value of the portfolio and has the option to return corresponding date @@ -105,8 +108,8 @@ def peak_value_func(equity_timeseries: pd.DataFrame, value: bool = True) -> Unio """ ts = equity_timeseries - peak_value = ts['Total'].max() - peak_date = ts[ts['Total'] == peak_value].index[0] + peak_value = ts["Total"].max() + peak_date = ts[ts["Total"] == peak_value].index[0] peak_dict = {peak_date: round(peak_value, 2)} return peak_value if value else peak_dict @@ -122,11 +125,11 @@ def final_value_func(equity_timeseries: pd.DataFrame) -> float: """ ts = equity_timeseries - final_val = round(ts['Total'][-1], 2) + final_val = round(ts["Total"][-1], 2) return final_val -def rtrn(equity_timeseries: pd.DataFrame, use_col = 'Total', long = True) -> float: +def rtrn(equity_timeseries: pd.DataFrame, use_col="Total", long=True) -> float: """ Parameters: @@ -136,8 +139,12 @@ def rtrn(equity_timeseries: pd.DataFrame, use_col = 'Total', long = True) -> flo float: Returns returns of portfolio from initial date to final date """ ts = equity_timeseries - rtrn = (ts[use_col][-1]/ts[use_col][0])-1 if long else 1 - (ts[use_col][-1]/ts[use_col][0]) - return rtrn*100 + rtrn = ( + (ts[use_col][-1] / ts[use_col][0]) - 1 + if long + else 1 - (ts[use_col][-1] / ts[use_col][0]) + ) + return rtrn * 100 def buyNhold(port_stats: dict) -> float: @@ -145,9 +152,9 @@ def buyNhold(port_stats: dict) -> float: initial_val = np.ones(len(PortStats)).sum() return_vals = np.zeros(len(PortStats)) for i, (k, v) in enumerate(PortStats.items()): - rtrn = v['Buy & Hold Return [%]']/100 - return_vals[i] = (1+rtrn) - bNh_rtrn = round(((return_vals.sum()/initial_val)-1)*100, 2) + rtrn = v["Buy & Hold Return [%]"] / 100 + return_vals[i] = 1 + rtrn + bNh_rtrn = round(((return_vals.sum() / initial_val) - 1) * 100, 2) return bNh_rtrn @@ -161,20 +168,24 @@ def cagr(equity_timeseries: pd.DataFrame) -> float: float: Returns average annualize retruns for the portfolio. Cumulative Annual Growth Rate """ ts = equity_timeseries - begin_val = ts['Total'].iloc[0] - end_val = ts['Total'].iloc[-1] + begin_val = ts["Total"].iloc[0] + end_val = ts["Total"].iloc[-1] if isinstance(ts.index, pd.DatetimeIndex): days = (ts.index.max() - ts.index.min()).days elif isinstance(ts.index, pd.RangeIndex): - days = (ts.index.max() - ts.index.min()) - return ((end_val/begin_val)**(365/days) - 1)*100 + days = ts.index.max() - ts.index.min() + return ((end_val / begin_val) ** (252 / days) - 1) * 100 -def vol_annualized(equity_timeseries: pd.DataFrame, downside: Optional[bool] = False, MAR: Optional[Union[int, float]] = 0) -> float: +def vol_annualized( + equity_timeseries: pd.DataFrame, + downside: Optional[bool] = False, + MAR: Optional[Union[int, float]] = 0, +) -> float: """ Returns the annualized volatility of the portfolio, which is calculated from the Portfolio Timeseries Value - Parameters: + Parameters: equity_timeseries (pd.DataFrame): Timeseries of the periodic equity values downside (Optional[bool]): False for regular volatility, True to calculate downside volatility MAR (Optional[Union[int, float]]): Minimum Acceptable Return @@ -186,17 +197,22 @@ def vol_annualized(equity_timeseries: pd.DataFrame, downside: Optional[bool] = F annual_trading_days = 365 ts_date_width = (ts.index.to_series().diff()).dt.days.mean() if not downside: - return round(np.std(filter_zeros(ts['Total']).pct_change(), ddof=1) * np.sqrt(annual_trading_days/ts_date_width) * 100, 6) + return round( + np.std(filter_zeros(ts["Total"]).pct_change(), ddof=1) + * np.sqrt(annual_trading_days / ts_date_width) + * 100, + 6, + ) else: if not MAR: MAR = 0 - ts = ts['Total'].pct_change() - MAR + ts = ts["Total"].pct_change() - MAR ts_d = ts[ts < 0] return round(np.std(ts_d, ddof=1) * np.sqrt(252) * 100, 6) -def daily_rtrns(equity_timeseries: pd.DataFrame, long = True) -> pd.Series: +def daily_rtrns(equity_timeseries: pd.DataFrame, long=True) -> pd.Series: """ Parameters: equity_timeseries (pd.DataFrame): This is the timeseries of the periodic equity values @@ -204,14 +220,16 @@ def daily_rtrns(equity_timeseries: pd.DataFrame, long = True) -> pd.Series: Returns: pd.Series: Utility method. Returns timeseries of daily portfolio returns """ - ts = filter_zeros(equity_timeseries['Total']).pct_change() + ts = filter_zeros(equity_timeseries["Total"]).pct_change() return ts if long else -ts -def sharpe(equity_timeseries: pd.DataFrame, risk_free_rate: float = 0.055, long = True) -> float: +def sharpe( + equity_timeseries: pd.DataFrame, risk_free_rate: float = 0.055, long=True +) -> float: """ Returns the Sharpe ratio of the portfolio - Parameters: + Parameters: risk_free_rate (float): A single value representing the risk free rate. This should be an annualized value equity_timeseries (pd.DataFrame): This is the timeseries of the periodic equity values @@ -222,19 +240,23 @@ def sharpe(equity_timeseries: pd.DataFrame, risk_free_rate: float = 0.055, long # ANNUALIZED MEAN EXCESS RETURN / ANNUALIZED VOLATILITY annual_trading_days = 365 ts_date_width = (equity_timeseries.index.to_series().diff()).dt.days.mean() - annual_period = annual_trading_days/ts_date_width + annual_period = annual_trading_days / ts_date_width equity_timeseries = filter_zeros(equity_timeseries) - daily_rfrate = (1+risk_free_rate)**(1/252) - 1 - annualized_vol = vol_annualized(equity_timeseries)/100 - excess_retrns = np.mean(daily_rtrns(equity_timeseries, long) - daily_rfrate)*annual_period - return excess_retrns/annualized_vol + daily_rfrate = (1 + risk_free_rate) ** (1 / 252) - 1 + annualized_vol = vol_annualized(equity_timeseries) / 100 + excess_retrns = ( + np.mean(daily_rtrns(equity_timeseries, long) - daily_rfrate) * annual_period + ) + return excess_retrns / annualized_vol -def sortino(equity_timeseries: pd.DataFrame, risk_free_rate: float, MAR: Optional[float] = None) -> float: +def sortino( + equity_timeseries: pd.DataFrame, risk_free_rate: float, MAR: Optional[float] = None +) -> float: """ Returns the Sortino ratio of the portfolio - Parameters: + Parameters: risk_free_rate (float): A single value representing the risk free rate. This should be an annualized value MAR: Minimum Acceptable Return. A Value to compare with returns to ascertain true downside returns. Eg can be inflation rate, to show that real returns can be negative even though nominal is positive equity_timeseries (pd.DataFrame): Timeseries of the periodic equity values @@ -245,14 +267,16 @@ def sortino(equity_timeseries: pd.DataFrame, risk_free_rate: float, MAR: Optiona # ANNUALIZED MEAN EXCESS RETURN / ANNUALIZED VOLATILITY if not MAR: - MAR = (1+risk_free_rate)**(1/252) - 1 - daily_rfrate = (1+risk_free_rate)**(1/252) - 1 - annualized_vol = vol_annualized(equity_timeseries, True, MAR)/100 - excess_retrns = np.mean(daily_rtrns(equity_timeseries) - daily_rfrate)*252 - return excess_retrns/annualized_vol + MAR = (1 + risk_free_rate) ** (1 / 252) - 1 + daily_rfrate = (1 + risk_free_rate) ** (1 / 252) - 1 + annualized_vol = vol_annualized(equity_timeseries, True, MAR) / 100 + excess_retrns = np.mean(daily_rtrns(equity_timeseries) - daily_rfrate) * 252 + return excess_retrns / annualized_vol -def dd(equity_timeseries: pd.DataFrame, full: bool = False) -> Union[pd.DataFrame, pd.Series]: +def dd( + equity_timeseries: pd.DataFrame, full: bool = False +) -> Union[pd.DataFrame, pd.Series]: """ Returns portfolio DrawDrown timeseires @@ -262,13 +286,13 @@ def dd(equity_timeseries: pd.DataFrame, full: bool = False) -> Union[pd.DataFram """ ts = equity_timeseries data = pd.DataFrame() - data['Total'] = ts['Total'] - data['Running_max'] = data.Total.cummax() - data['dd'] = (data.Total/data.Running_max)-1 + data["Total"] = ts["Total"] + data["Running_max"] = data.Total.cummax() + data["dd"] = (data.Total / data.Running_max) - 1 if full: return data else: - return data['dd'] + return data["dd"] def mdd(equity_timeseries: pd.DataFrame) -> float: @@ -279,7 +303,7 @@ def mdd(equity_timeseries: pd.DataFrame) -> float: Returns Max Drawdown """ dd_ = dd(equity_timeseries) - return dd_.min()*100 + return dd_.min() * 100 def calmar(equity_timeseries: pd.DataFrame) -> float: @@ -290,7 +314,7 @@ def calmar(equity_timeseries: pd.DataFrame) -> float: Returns calmar Ratio """ - return abs(cagr(equity_timeseries)/mdd(equity_timeseries)) + return abs(cagr(equity_timeseries) / mdd(equity_timeseries)) def avg_dd_percent(equity_timeseries: pd.DataFrame) -> float: @@ -300,7 +324,7 @@ def avg_dd_percent(equity_timeseries: pd.DataFrame) -> float: Returns avg Drawdown % """ - return round(dd(equity_timeseries).mean()*100, 6) + return round(dd(equity_timeseries).mean() * 100, 6) def mdd_value(equity_timeseries: pd.DataFrame) -> float: @@ -311,7 +335,7 @@ def mdd_value(equity_timeseries: pd.DataFrame) -> float: Returns Maximum Drawdown value """ dd_ = dd(equity_timeseries, True) - return round((dd_['Total'] - dd_['Running_max']).min(), 2) + return round((dd_["Total"] - dd_["Running_max"]).min(), 2) def mdd_duration(equity_timeseries: pd.DataFrame, full: bool = False) -> pd.Timedelta: @@ -326,12 +350,13 @@ def mdd_duration(equity_timeseries: pd.DataFrame, full: bool = False) -> pd.Time maximum drawdown duration """ from datetime import timedelta + dd_ = dd(equity_timeseries, True) for i, (index, row) in enumerate(dd_.iterrows()): - total, running_max, date = row['Total'], row['Running_max'], index - running_max_date = dd_[dd_['Total'] == running_max].index[0] - dd_.at[index, 'timedelta'] = (date - running_max_date) + total, running_max, date = row["Total"], row["Running_max"], index + running_max_date = dd_[dd_["Total"] == running_max].index[0] + dd_.at[index, "timedelta"] = date - running_max_date if full: return dd_ @@ -358,10 +383,10 @@ def trades(port_stats) -> pd.DataFrame: """ trades_df = pd.DataFrame() for k, v in port_stats.items(): - holder = v['_trades'] - holder['Ticker'] = k + holder = v["_trades"].copy(deep=True) + holder["Ticker"] = k trades_df = pd.concat([trades_df, holder]) - return trades_df.sort_values(['EntryTime', 'ExitTime']).reset_index(drop = True) + return trades_df.sort_values(["EntryTime", "ExitTime"]).reset_index(drop=True) def numOfTrades(trades_df) -> int: @@ -378,7 +403,7 @@ def winRate(trades_df: pd.DataFrame) -> float: trades_df (pd.DataFrame): DataFrame Contatining Trades """ trades_ = trades_df - return round(((trades_.ReturnPct > 0).sum()/(trades_.ReturnPct).count())*100, 2) + return round(((trades_.ReturnPct > 0).sum() / (trades_.ReturnPct).count()) * 100, 2) def lossRate(trades_df: pd.DataFrame) -> float: @@ -395,27 +420,36 @@ def avgPnL(trades_df: pd.DataFrame, Type_: str, value=True) -> float: trades_df (pd.DataFrame): DataFrame Contatining Trades & PnL or ReturnPct column Type_ (str): 'W', 'L', 'A'. Win, Loss or All - value (bool): True to return + value (bool): True to return """ - assert Type_.upper() in [ - 'W', 'L', 'A'], f"Invalid Type_: '{Type_}'. Must be 'L', 'W' or 'A." - assert 'PnL' in trades_df.columns or 'ReturnPct' in trades_df.columns, f"Please pass a dataframe holding trades and ensure it has either 'PnL' or 'ReturnPct' in the columns. Current Columns {trades_df.columns}" + assert Type_.upper() in ["W", "L", "A"], ( + f"Invalid Type_: '{Type_}'. Must be 'L', 'W' or 'A." + ) + assert "PnL" in trades_df.columns or "ReturnPct" in trades_df.columns, ( + f"Please pass a dataframe holding trades and ensure it has either 'PnL' or 'ReturnPct' in the columns. Current Columns {trades_df.columns}" + ) trades_ = trades_df PnL = (trades_.PnL if value else trades_.ReturnPct).astype(float) - WPnL = PnL[PnL > 0] if Type_.upper() == 'W' else PnL[PnL <= - 0] if Type_.upper() == 'L' else PnL + WPnL = ( + PnL[PnL > 0] + if Type_.upper() == "W" + else PnL[PnL <= 0] + if Type_.upper() == "L" + else PnL + ) return WPnL.mean() * 100 if not WPnL.empty else 0 def bestTrade(trades_df: pd.DataFrame) -> float: - return trades_df.ReturnPct.max()*100 + return trades_df.ReturnPct.max() * 100 def worstTrade(trades_df) -> float: - return trades_df.ReturnPct.min()*100 + return trades_df.ReturnPct.min() * 100 + def trade_percentile(trades_df: pd.DataFrame, percentile: float) -> float: """ @@ -428,9 +462,19 @@ def trade_percentile(trades_df: pd.DataFrame, percentile: float) -> float: Returns: float: Corresponding PnL or ReturnPct value at the given percentile """ - assert 'PnL' in trades_df.columns or 'ReturnPct' in trades_df.columns, f"Please pass a dataframe holding trades and ensure it has either 'PnL' or 'ReturnPct' in the columns. Current Columns {trades_df.columns}" - assert 0 <= percentile <= 100, f"Percentile must be between 0 and 100. Current Value: {percentile}" - return np.percentile(trades_df.ReturnPct, percentile)*100 if 'ReturnPct' in trades_df.columns else np.percentile(trades_df.PnL, percentile) + if trades_df.empty: + return 0.0 + assert "PnL" in trades_df.columns or "ReturnPct" in trades_df.columns, ( + f"Please pass a dataframe holding trades and ensure it has either 'PnL' or 'ReturnPct' in the columns. Current Columns {trades_df.columns}" + ) + assert 0 <= percentile <= 100, ( + f"Percentile must be between 0 and 100. Current Value: {percentile}" + ) + return ( + np.percentile(trades_df.ReturnPct, percentile) * 100 + if "ReturnPct" in trades_df.columns + else np.percentile(trades_df.PnL, percentile) + ) def profitFactor(trades_df: pd.DataFrame) -> float: @@ -441,9 +485,9 @@ def profitFactor(trades_df: pd.DataFrame) -> float: Returns the profit factor of the strategy. Synonymous to R/R """ tr = trades_df - tot_loss = tr[tr['ReturnPct'] <= 0]['PnL'].sum() - tot_gain = tr[tr['ReturnPct'] > 0]['PnL'].sum() - return round(abs(tot_gain/tot_loss), 6) + tot_loss = tr[tr["ReturnPct"] <= 0]["PnL"].sum() + tot_gain = tr[tr["ReturnPct"] > 0]["PnL"].sum() + return round(abs(tot_gain / tot_loss), 6) def Expectancy(trades_df: pd.DataFrame, cash_expectancy: bool = False) -> float: @@ -451,14 +495,18 @@ def Expectancy(trades_df: pd.DataFrame, cash_expectancy: bool = False) -> float: Returns the expected %pnl based on portfolio data """ tr = trades_df - avg_win_pnl = avgPnL(tr, 'W', True) + avg_win_pnl = avgPnL(tr, "W", True) avg_win_rate = winRate(tr) avg_loss_rate = lossRate(tr) - avg_loss_pnl = avgPnL(tr, 'L', True) + avg_loss_pnl = avgPnL(tr, "L", True) if cash_expectancy: - return ((avg_win_pnl * (avg_win_rate/100)) + (avg_loss_pnl * (avg_loss_rate/100))) + return (avg_win_pnl * (avg_win_rate / 100)) + ( + avg_loss_pnl * (avg_loss_rate / 100) + ) else: - return (avgPnL(tr, 'W', False) * (winRate(tr)/100)) + (avgPnL(tr, 'L', False) * (lossRate(tr)/100)) + return (avgPnL(tr, "W", False) * (winRate(tr) / 100)) + ( + avgPnL(tr, "L", False) * (lossRate(tr) / 100) + ) def SQN(trades_df: pd.DataFrame) -> float: @@ -469,7 +517,9 @@ def SQN(trades_df: pd.DataFrame) -> float: System Quality Number. Used to guage how good a system is """ trades_ = trades_df - return ((trades_.ReturnPct.mean() * np.sqrt(len(trades_)))/np.std(trades_.ReturnPct)) + return (trades_.ReturnPct.mean() * np.sqrt(len(trades_))) / np.std( + trades_.ReturnPct + ) def ExposureDays(equity_timeseries: pd.DataFrame, trades_df: pd.DataFrame) -> float: @@ -481,18 +531,18 @@ def ExposureDays(equity_timeseries: pd.DataFrame, trades_df: pd.DataFrame) -> fl Returns the percent of days the portfolio had exposure """ - time_in = pd.DataFrame(index=equity_timeseries.index) - time_in['position'] = 0 - tr = trades_df - tr.dropna(subset=['EntryTime', 'ExitTime'], inplace=True) + time_in = pd.DataFrame(index=equity_timeseries.copy(deep=True).index) + time_in["position"] = 0 + tr = trades_df.copy(deep=True) + tr = tr.dropna(subset=["EntryTime", "ExitTime"]) for index, row in tr.iterrows(): - entry = pd.to_datetime(row['EntryTime']).date() - exit_ = pd.to_datetime(row['ExitTime']).date() - time_in['position'].loc[(time_in.index.date >= entry) - & (time_in.index.date <= exit_)] = 1 - + entry = pd.to_datetime(row["EntryTime"]).date() + exit_ = pd.to_datetime(row["ExitTime"]).date() + time_in.loc[ + (time_in.index.date >= entry) & (time_in.index.date <= exit_), "position" + ] = 1 - return round((time_in['position'] == 1).sum()/len(time_in)*100, 2) + return round((time_in["position"] == 1).sum() / len(time_in) * 100, 2) def yearly_retrns(equity_timeseries: pd.DataFrame) -> dict: @@ -503,26 +553,30 @@ def yearly_retrns(equity_timeseries: pd.DataFrame) -> dict: Returns yearly returns as a dict """ - - ts = equity_timeseries - ts['Year'] = ts.index.year - ts.drop_duplicates(inplace=True) + ts = equity_timeseries.copy(deep=True) + ts["Year"] = ts.index.year + ## dropping duplicate years if there are multiple + # ts = ts[~ts.index.duplicated(keep="first")] unq_year = ts.Year.unique() rtrn_d = {} for year in unq_year: - data = ts[ts['Year'] == year].sort_index() - ret = ((data.loc[data.index.max(), 'Total'] / - data.loc[data.index.min(), 'Total'])-1)*100 + data = ts[ts["Year"] == year].sort_index() + ret = ( + (data.loc[data.index.max(), "Total"] / data.loc[data.index.min(), "Total"]) + - 1 + ) * 100 rtrn_d[year] = ret return rtrn_d -def holding_period(trades_df: pd.DataFrame, aggfunc: Callable, Type_: str = 'W') -> pd.Timedelta: - """ +def holding_period( + trades_df: pd.DataFrame, aggfunc: Callable, Type_: str = "W" +) -> pd.Timedelta: + """ Returns the average or max holding period of the portfolio based on Trades data Parameters: - aggfunc (Callable): A callable that performs an arithmetic calculation. Eg: Mean, Median, Mode, Max, Min + aggfunc (Callable): A callable that performs an arithmetic calculation. Eg: Mean, Median, Mode, Max, Min trades_df (pd.DataFrame): DataFrame Contatining Trades & PnL or ReturnPct column Type_ (float): Which holding period are we looking for. Available options are @@ -534,23 +588,25 @@ def holding_period(trades_df: pd.DataFrame, aggfunc: Callable, Type_: str = 'W') Returns: pd.Timedelta: Corresponding Value """ - assert aggfunc.__name__.lower() in ['amax', - 'mean', 'max'], f"Function of type '{aggfunc.__name__}' cannot be used for this method. Please use a mean or max function" - assert Type_.upper() in [ - 'A', 'W', 'L'], f"Invalid type: {Type_}. Must be 'L', 'W' or 'A." + assert aggfunc.__name__.lower() in ["amax", "mean", "max"], ( + f"Function of type '{aggfunc.__name__}' cannot be used for this method. Please use a mean or max function" + ) + assert Type_.upper() in ["A", "W", "L"], ( + f"Invalid type: {Type_}. Must be 'L', 'W' or 'A." + ) # assert aggfunc.upper() in ['A', 'M'], f"Invalid type: {aggfunc}. Must be 'M' for Max or 'A' for Avg." trades_ = trades_df - if Type_.upper() == 'W': - trades_ = trades_[trades_['ReturnPct'] > 0] - elif Type_.upper() == 'L': - trades_ = trades_[trades_['ReturnPct'] <= 0] + if Type_.upper() == "W": + trades_ = trades_[trades_["ReturnPct"] > 0] + elif Type_.upper() == "L": + trades_ = trades_[trades_["ReturnPct"] <= 0] # return trades_.Duration.mean() if aggfunc.upper() == 'A' else trades_.Duration.max() return aggfunc(trades_.Duration) -def streak(trades_df: pd.DataFrame, Type_: str = 'W') -> int: - """ +def streak(trades_df: pd.DataFrame, Type_: str = "W") -> int: + """ Returns the Losing/Winning Streak based on Trades data Parameters: @@ -564,18 +620,15 @@ def streak(trades_df: pd.DataFrame, Type_: str = 'W') -> int: Returns: int: Corresponding Value """ - assert Type_.upper() in [ - 'L', 'W'], f"Invalid type: {Type_}. Must be 'L' or 'W'." - t = trades_df - t['Is_Loss'] = int - t['Is_Loss'] = t['ReturnPct'] <= 0 if Type_.upper() == 'L' else t['ReturnPct'] > 0 - t['Loss_Streak'] = (t['Is_Loss'] != t['Is_Loss'].shift()).cumsum() - streak_lengths = t.groupby('Loss_Streak')['Is_Loss'].sum() - return streak_lengths[t.groupby('Loss_Streak')['Is_Loss'].first()].max() + assert Type_.upper() in ["L", "W"], f"Invalid type: {Type_}. Must be 'L' or 'W'." + t = trades_df.copy(deep=True) + t["Is_Loss"] = t["ReturnPct"] <= 0 if Type_.upper() == "L" else t["ReturnPct"] > 0 + t["Loss_Streak"] = (t["Is_Loss"] != t["Is_Loss"].shift()).cumsum() + streak_lengths = t.groupby("Loss_Streak")["Is_Loss"].sum() + return streak_lengths[t.groupby("Loss_Streak")["Is_Loss"].first()].max() class AggregatorParent(ABC): - def __init__(self): self._equity = None self.__port_stats = None @@ -584,15 +637,24 @@ def __init__(self): @copy_doc_from(dates_) def dates_(self, start: bool): try: - overwrite = pd.to_datetime(getattr(self, 'start_overwrite')).date() + overwrite = pd.to_datetime(getattr(self, "start_overwrite")).date() except AttributeError: overwrite = None + port_stats = self.get_port_stats() + if port_stats is None: + raise NotImplementedError( + "Subclasses must implement the dates_ method when port_stats is not available." + ) + if overwrite: - return overwrite if start else dates_(self.get_port_stats(), start).date() + return overwrite if start else dates_(port_stats, start).date() else: - return dates_(self.get_port_stats(), start).date() if start else dates_(self.get_port_stats(), start).date() - + return ( + dates_(port_stats, start).date() + if start + else dates_(port_stats, start).date() + ) @abstractmethod def get_port_stats(self): @@ -631,11 +693,16 @@ def rtrn(self) -> float: Returns: float: Returns returns of portfolio from initial date to final date """ - assert self._equity is not None, f'Portfolio Equity is empty' + assert self._equity is not None, f"Portfolio Equity is empty" return rtrn(self._equity) def buyNhold(self) -> float: - return buyNhold(self.get_port_stats()) + port_stats = self.get_port_stats() + if port_stats is None: + raise NotImplementedError( + "Subclasses must implement the buyNhold method when port_stats is not available." + ) + return buyNhold(port_stats) def cagr(self) -> float: """ @@ -644,11 +711,13 @@ def cagr(self) -> float: """ return cagr(self._equity) - def vol_annualized(self, downside: Optional[bool] = False, MAR: Optional[Union[int, float]] = 0) -> float: + def vol_annualized( + self, downside: Optional[bool] = False, MAR: Optional[Union[int, float]] = 0 + ) -> float: """ Returns the annualized volatility of the portfolio, which is calculated from the Portfolio Timeseries Value - Parameters: + Parameters: downside (Optional[bool]): False for regular volatility, True to calculate downside volatility MAR (Optional[Union[int, float]]): Minimum Acceptable Return @@ -669,7 +738,7 @@ def daily_rtrns(self) -> pd.Series: def sharpe(self, risk_free_rate: float = 0.055) -> float: """ Returns the Sharpe ratio of the portfolio - Parameters: + Parameters: risk_free_rate (float): A single value representing the risk free rate. This should be an annualized value equity_timeseries (pd.DataFrame): This is the timeseries of the periodic equity values @@ -684,7 +753,7 @@ def sortino(self, risk_free_rate: float, MAR: Optional[float] = None) -> float: """ Returns the Sortino ratio of the portfolio - Parameters: + Parameters: risk_free_rate (float): A single value representing the risk free rate. This should be an annualized value MAR: Minimum Acceptable Return. A Value to compare with returns to ascertain true downside returns. Eg can be inflation rate, to show that real returns can be negative even though nominal is positive @@ -743,6 +812,7 @@ def mdd_duration(self, full: bool = False) -> pd.Timedelta: maximum drawdown duration """ from datetime import timedelta + return mdd_duration(self._equity, full) def avg_dd_duration(self) -> pd.Timedelta: @@ -756,7 +826,12 @@ def trades(self) -> pd.DataFrame: Returns a dataframe containing all trades taken """ - return trades(self.get_port_stats()) + port_stats = self.get_port_stats() + if port_stats is None: + raise NotImplementedError( + "Subclasses must implement the trades method when port_stats is not available." + ) + return trades(port_stats) def numOfTrades(self) -> int: """ @@ -781,13 +856,13 @@ def trade_percentile(self, percentile: float) -> float: float: Corresponding PnL or ReturnPct value at the given percentile """ return trade_percentile(self._trades, percentile) - + def avgPnL(self, Type_: str, value=True) -> float: """ params: Type_ (str): 'W', 'L', 'A'. Win, Loss or All - value (bool): True to return + value (bool): True to return """ return avgPnL(self._trades, Type_, value) @@ -819,8 +894,8 @@ def Expectancy(self) -> float: # total_cash = cash # else: # raise TypeError(f"Cash attribute must be of type dict, int or float. Current type is {type(cash)}") - - pct_expectancy = Expectancy(self._trades, cash_expectancy = False) + + pct_expectancy = Expectancy(self._trades, cash_expectancy=False) return pct_expectancy def SQN(self) -> float: @@ -843,12 +918,12 @@ def yearly_retrns(self) -> dict: return yearly_retrns(self._equity) - def holding_period(self, aggfunc: Callable, Type_: str = 'W') -> pd.Timedelta: - """ + def holding_period(self, aggfunc: Callable, Type_: str = "W") -> pd.Timedelta: + """ Returns the average or max holding period of the portfolio based on Trades data Parameters: - aggfunc (Callable): A callable that performs an arithmetic calculation. Eg: Mean, Median, Mode, Max, Min + aggfunc (Callable): A callable that performs an arithmetic calculation. Eg: Mean, Median, Mode, Max, Min Type_ (float): Which holding period are we looking for. Available options are 'W': For winning holding period @@ -862,8 +937,8 @@ def holding_period(self, aggfunc: Callable, Type_: str = 'W') -> pd.Timedelta: return holding_period(self._trades, aggfunc, Type_) - def streak(self, Type_: str = 'W') -> int: - """ + def streak(self, Type_: str = "W") -> int: + """ Returns the Losing/Winning Streak based on Trades data Parameters: @@ -876,11 +951,23 @@ def streak(self, Type_: str = 'W') -> int: """ return streak(self._trades, Type_) -## FIXME: This is ridiculously slow. - def aggregate(self, - risk_free_rate: float = 0.0, - MAR: float = 0) -> pd.Series: - """ + def get_strategy_class(self): + port_stats = self.get_port_stats() + if port_stats is None: + raise NotImplementedError( + "Subclasses must implement the get_strategy_class method when port_stats is not available." + ) + try: + strategy = list(port_stats.values())[0]["_strategy"] + return strategy + except AttributeError: + raise AttributeError( + "Port Stats must have a '_strategy' key to obtain strategy class. Please ensure your backtesting implementation includes this in the port stats output." + ) + + ## FIXME: This is ridiculously slow. + def aggregate(self, risk_free_rate: float = 0.0, MAR: float = 0) -> pd.Series: + """ Returns aggregated Data for the Porftolio @@ -891,50 +978,50 @@ def aggregate(self, Returns: int: Corresponding Value """ - assert self.get_port_stats(), f"Run Portfolio Backtest before aggregating" + is_run = self._equity is not None and self._trades is not None + if not is_run: + raise Exception( + "Portfolio must be run before aggregation. Please run the backtest before calling aggregate." + ) + # assert port_stats, "Run Portfolio Backtest before aggregating" MAR = 0.0 if not MAR else MAR - + ## Extending to ensure useability in other places. It was originally designed for PTBacktest, ## But can be used in other places ## Re-implement dates_. This is very specific to PTBacktest + assert isinstance(MAR, float), ( + f"Recieved MAR of type {type(MAR)} instead of Type float" + ) - assert isinstance( - MAR, float), f"Recieved MAR of type {type(MAR)} instead of Type float" - - try: - start_overwrite = getattr(self, 'start_overwrite') - except AttributeError: - start_overwrite = None + start_overwrite = getattr(self, "start_overwrite", None) + strategy = self.get_strategy_class() try: - strategy = list(self.get_port_stats().values())[0]['_strategy'] - except AttributeError: - strategy = None - - try: equity = self.pf_value_ts() except AttributeError: equity = self._equity except Exception: - raise Exception('Either implement pf_value_ts method or self._equity') - + raise Exception("Either implement pf_value_ts method or self._equity") try: ## Function call for trades trades = self.trades() - except TypeError: - ## If trades is not implemented, we can use the self._trades attribute - trades = self._trades except Exception: - raise Exception('Either implement trades method or self._trades') + ## If trades is not implemented, we can use the self._trades attribute + try: + trades = self._trades + except Exception: + raise Exception("Either implement trades method or self._trades") try: tickers = [dataset.name for dataset in self.datasets] except AttributeError: tickers = self.symbol_list except Exception: - raise Exception('Either implement datasets attribute with PTDataset or self.symbol_list') + raise Exception( + "Either implement datasets attribute with PTDataset or self.symbol_list" + ) rtrn_ = self.rtrn() series1 = pd.Series( @@ -991,15 +1078,17 @@ def aggregate(self, rtrn_dict = self.yearly_retrns() rtrn_series = pd.Series( - {f"{year} Return [%]": value for year, value in rtrn_dict.items()}) - - series3 = pd.Series({ - 'Winning Streak': self.streak('W'), - 'Losing Streak': self.streak('L'), - '_strategy': strategy, - 'equity_curve': equity, - '_trades': trades, - '_tickers': tickers - - }) - return pd.concat([series1, rtrn_series, series3]) \ No newline at end of file + {f"{year} Return [%]": value for year, value in rtrn_dict.items()} + ) + + series3 = pd.Series( + { + "Winning Streak": self.streak("W"), + "Losing Streak": self.streak("L"), + "_strategy": strategy, + "equity_curve": equity, + "_trades": trades, + "_tickers": tickers, + } + ) + return pd.concat([series1, rtrn_series, series3]) diff --git a/trade/helpers/helper_types.py b/trade/helpers/helper_types.py index 9cc6350..56c7012 100644 --- a/trade/helpers/helper_types.py +++ b/trade/helpers/helper_types.py @@ -1,4 +1,4 @@ -from dataclasses import fields + from typing import Iterable, TypedDict, Any from enum import Enum from datetime import datetime @@ -6,13 +6,55 @@ 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 +from typing import get_origin, get_args, Union, get_type_hints, Literal, Type, Dict import types from trade.helpers.Logging import setup_logger +from dataclasses import dataclass, fields +from functools import lru_cache +from typeguard import check_type + logger = setup_logger(__name__) DATE_HINT = Union[datetime, str] + + +@lru_cache(maxsize=None) +def _hints(cls: Type[Any]) -> Dict[str, Any]: + return get_type_hints(cls) + + +class TypeValidatedMixin: + def _validate_field(self, name: str, value: Any) -> None: + hint = _hints(type(self)).get(name) + if hint is not None: + check_type(value, hint) + + def _validate_all_fields(self) -> None: + for f in fields(self): + self._validate_field(f.name, getattr(self, f.name)) + + def __post_init__(self) -> None: + self._validate_all_fields() + + +@dataclass +class MutableValidated(TypeValidatedMixin): + def __setattr__(self, name: str, value: Any) -> None: + self._validate_field(name, value) + super().__setattr__(name, value) + + +@dataclass(frozen=True) +class FrozenValidated(TypeValidatedMixin): + pass + + +# frozen update pattern: +# new_obj = replace(old_obj, some_field=new_value) + + + class IncorrectTypeError(Exception): """Custom exception for incorrect type errors in configuration validation.""" From 491aa174ba62b346306f5d2fefa3c3f46100d725 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:29:00 -0400 Subject: [PATCH 03/81] Add delta-aware risk manager order selection and sizing hooks --- EventDriven/configs/core.py | 25 +++- EventDriven/dataclasses/orders.py | 4 +- EventDriven/riskmanager/_orders.py | 42 ++++-- EventDriven/riskmanager/new_base.py | 18 ++- EventDriven/riskmanager/picker/__init__.py | 46 +++---- EventDriven/riskmanager/picker/builder.py | 64 ++++++++- EventDriven/riskmanager/picker/iv_helper.py | 106 +++++++++++++++ .../riskmanager/picker/naked_option.py | 71 ++++++++-- .../riskmanager/picker/order_picker.py | 55 ++++---- EventDriven/riskmanager/picker/utils.py | 47 +++++++ .../riskmanager/picker/vertical_spread.py | 103 +++++++++------ EventDriven/riskmanager/position/analyzer.py | 11 ++ EventDriven/riskmanager/position/base.py | 14 +- .../riskmanager/position/cogs/limits.py | 15 ++- .../position/cogs/mean_reversion.py | 124 ++++++++++++++++++ EventDriven/types.py | 35 ++++- 16 files changed, 631 insertions(+), 149 deletions(-) create mode 100644 EventDriven/riskmanager/picker/iv_helper.py create mode 100644 EventDriven/riskmanager/picker/utils.py create mode 100644 EventDriven/riskmanager/position/cogs/mean_reversion.py diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 96d8358..98698fa 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -10,7 +10,9 @@ BaseConfigs, _CustomFrozenBaseConfigs, ) -from EventDriven._vars import OPTION_TIMESERIES_START_DATE +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE +from trade.helpers.Logging import setup_logger +logger = setup_logger("EventDriven.configs.core") @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @@ -22,6 +24,7 @@ class ChainConfig(BaseConfigs): max_pct_width: numbers.Number = None min_oi: numbers.Number = None + enable_delta_filter: bool = False @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @@ -38,6 +41,7 @@ class OrderSchemaConfigs(BaseConfigs): min_moneyness: numbers.Number = 0.65 max_moneyness: numbers.Number = 1 min_total_price: numbers.Number = 0.95 + max_attempts: int = 3 @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @@ -218,6 +222,8 @@ class BacktesterConfig(BaseConfigs): raise_errors: bool = False min_slippage_pct: float = 0.075 max_slippage_pct: float = 0.15 + commission_per_contract_in_units: float = 0.0065 + @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @@ -254,8 +260,21 @@ class RiskManagerConfig(BaseConfigs): Configuration class for Risk Manager related settings. """ - min_slippage_pct: float = 0.25 - max_slippage_pct: float = 0.16 cache_orders: bool = False cache_position_analysis: bool = False cache_order_requests: bool = False + + +@pydantic_dataclass +class MeanReversionSizerConfigs(BaseCogConfig): + beta: float = 0.5 + name: str = "custom_mean_reversion_sizer" + min_scale: float = 0.5 + max_scale: float = 2.0 + sizing_lev: int = 3 + default_dte: int = 10 + enabled_limits: StrategyLimitsEnabled = Field( + default_factory=lambda: StrategyLimitsEnabled(delta=False, dte=True, moneyness=False) + ) + # Minimum z-score threshold to trigger scaling adjustments + min_zscore: float = 2.5 diff --git a/EventDriven/dataclasses/orders.py b/EventDriven/dataclasses/orders.py index b85ee46..b1196b4 100644 --- a/EventDriven/dataclasses/orders.py +++ b/EventDriven/dataclasses/orders.py @@ -1,6 +1,6 @@ from pydantic.dataclasses import dataclass from pydantic import ConfigDict -from typing import Union, Literal +from typing import Optional, Union, Literal from datetime import datetime, date import numbers @@ -23,3 +23,5 @@ class OrderRequest: spot: numbers.Number = None chain_spot: numbers.Number = None is_tick_cash_scaled: bool = False + delta_lmt: Optional[numbers.Number] = None + diff --git a/EventDriven/riskmanager/_orders.py b/EventDriven/riskmanager/_orders.py index 7cbb8e5..c30d370 100644 --- a/EventDriven/riskmanager/_orders.py +++ b/EventDriven/riskmanager/_orders.py @@ -68,7 +68,7 @@ - Consider relaxing initial constraints if failures are frequent """ -from typing import Dict, Any, TYPE_CHECKING, Tuple +from typing import Dict, Any, TYPE_CHECKING, Optional, Tuple from datetime import datetime import pandas as pd from trade.helpers.Logging import setup_logger @@ -104,7 +104,10 @@ def resolve_schema( itm_moneyness_width: float, max_close: float, max_tries: int = 6, - tick_cash: int = 10 + tick_cash: int = 10, + *, + starting_min_moneyness: float = None, + starting_max_moneyness: float = None, ) -> Tuple[OrderSchema, int]: """ Resolving schema by order of importance @@ -119,18 +122,29 @@ def resolve_schema( schema (OrderSchema): The schema to resolve. tries (int): The number of tries already made. max_dte_tolerance (int): The maximum DTE tolerance to allow. - moneyness_width (float): The moneyness width to allow. + otm_moneyness_width (float): In future iteration, this serve as the max width min moneyness can grow from starting_min_moneyness. It is the max width btwn new min moneyness and starting_min_moneyness. It is the max width btwn OTM strikes and 1 + itm_moneyness_width (float): In future iteration, this serve as the max width max moneyness can grow from starting_max_moneyness. It is the max width btwn new max moneyness and starting_max_moneyness. It is the max width btwn ITM strikes and 1. max_close (float): The maximum close price to allow. max_tries (int): The maximum number of tries allowed. Returns: tuple: A tuple containing the resolved schema or False if no schema was found, and the number of tries made. """ + if starting_min_moneyness is None: + raise ValueError("Missing starting_min_moneyness. This is required to ensure we do not relax constraints too much and to serve as a reference point for how much we can relax the min moneyness.") + + if starting_max_moneyness is None: + raise ValueError("Missing starting_max_moneyness. This is required to ensure we do not relax constraints too much and to serve as a reference point for how much we can relax the max moneyness.") + current_min_moneyness = schema["min_moneyness"] + current_max_moneyness = schema["max_moneyness"] + current_min_moneyness_width = starting_min_moneyness - current_min_moneyness + current_max_moneyness_width = current_max_moneyness - starting_max_moneyness tick = schema["tick"] ##0). Max schema tries if tries >= max_tries: return False, tries + # 1). DTE Resolve tries += 1 if schema["dte_tolerance"] <= max_dte_tolerance: @@ -141,19 +155,19 @@ def resolve_schema( return schema, tries # 2). Min Moneyness Resolve - elif 1 - schema["min_moneyness"] <= otm_moneyness_width: + elif current_min_moneyness_width < otm_moneyness_width: logger.info( - f"Resolving Schema ({tick}): {1 - schema['min_moneyness']} <= {otm_moneyness_width}, decreasing Min Moneyness by 0.1 from {schema['min_moneyness']} to {schema['min_moneyness'] - 0.1}" + f"Resolving Schema ({tick}): {current_min_moneyness_width} <= {otm_moneyness_width}, decreasing Min Moneyness by 0.1 from {schema['min_moneyness']} to {schema['min_moneyness'] - 0.1}" ) - schema["min_moneyness"] -= 0.1 + schema["min_moneyness"] -= 0.05 return schema, tries # 3). Max Moneyness Resolve - elif schema["max_moneyness"] - 1 <= itm_moneyness_width: + elif current_max_moneyness_width < itm_moneyness_width: logger.info( - f"Resolving Schema ({tick}): {schema['max_moneyness'] - 1} <= {itm_moneyness_width}, increasing Max Moneyness by 0.1 from {schema['max_moneyness']} to {schema['max_moneyness'] + 0.1}" + f"Resolving Schema ({tick}): {current_max_moneyness_width} <= {itm_moneyness_width}, increasing Max Moneyness by 0.1 from {schema['max_moneyness']} to {schema['max_moneyness'] + 0.1}" ) - schema["max_moneyness"] += 0.1 + schema["max_moneyness"] += 0.05 return schema, tries # 4). Close Resolve @@ -184,6 +198,7 @@ def order_resolve_loop( picker: "OrderPicker", request: OrderRequest = None, tick_cash: int = 10, + delta_lmt: Optional[float] = None, ): """ Attempt to resolve an order schema until a successful order is produced or maximum tries are exceeded. @@ -222,6 +237,8 @@ def order_resolve_loop( ) schema_as_tuple = tuple(schema.data.items()) use_request = True + min_moneyness = schema.get("min_moneyness", None) + max_moneyness = schema.get("max_moneyness", None) while order_failed(order): logger.info(f"Failed to produce order with schema: {schema}, trying to resolve schema, on try {tries}") @@ -233,7 +250,9 @@ def order_resolve_loop( max_tries=max_tries, otm_moneyness_width=otm_moneyness_width, itm_moneyness_width=itm_moneyness_width, - tick_cash=tick_cash + tick_cash=tick_cash, + starting_min_moneyness=min_moneyness, + starting_max_moneyness=max_moneyness, ) schema, tries = pack @@ -254,9 +273,10 @@ def order_resolve_loop( spot=request.spot, chain_spot=request.chain_spot, print_url=False, + delta_lmt=delta_lmt, ) else: - order = picker.get_order_new(schema, date, spot, print_url=False) ## Get the order from the OrderPicker + order = picker.get_order_new(schema, date, spot, print_url=False, delta_lmt=delta_lmt) ## Get the order from the OrderPicker schema_cache.setdefault(date, {}).update({signalID: schema}) return order diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index a1af844..f3b039c 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -207,6 +207,7 @@ from EventDriven.types import ResultsEnum, Order from EventDriven.configs.core import RiskManagerConfig from EventDriven._vars import CONTRACT_MULTIPLIER, load_riskmanager_cache +from pprint import pprint logger = setup_logger("EventDriven.riskmanager.new_base", stream_log_level="WARNING") @@ -280,11 +281,6 @@ def __init__( - Loads configuration from RiskManagerConfig with overrides from kwargs - Sets up caching infrastructure for market data and position analytics """ - ## For testing override the passed args - # bkt_start = '2025-01-01' - # bkt_end = '2025-06-30' - # symbol_list = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'NVDA', 'META', 'JNJ', 'V', 'WMT'] - ## Backtest Window self.start_date = bkt_start self.end_date = bkt_end @@ -314,7 +310,8 @@ def __init__( ## Initialize on disk caches. These will be cleared on exit. ##Order Cache - self.order_cache = load_riskmanager_cache(target="order_cache", create_on_missing=True, clear_on_exit=True) + # self.order_cache = load_riskmanager_cache(target="order_cache", create_on_missing=True, clear_on_exit=True) + self.order_cache = {} if len(self.order_cache.values()) > 0: logger.info(f"Order cache loaded with {len(self.order_cache.values())} orders") @@ -378,6 +375,12 @@ def get_order(self, req: OrderRequest) -> NewPositionState: ## Update request with data req.spot = spot req.chain_spot = chain_spot + req.delta_lmt = self.position_analyzer.get_delta_limit( + tick_cash=req.tick_cash, + chain_spot=chain_spot, + date=req.date, + ticker=req.symbol + ) ## Get order print(f"Generating order for request: {req}") @@ -390,7 +393,8 @@ def get_order(self, req: OrderRequest) -> NewPositionState: ## Process order if not order_failed(order): - print(f"\nOrder Received: {order}\n") + print(f"\nOrder Received:\n") + pprint(order) position_id = order["data"]["trade_id"] else: print(f"\nOrder Failed: {order}\n") diff --git a/EventDriven/riskmanager/picker/__init__.py b/EventDriven/riskmanager/picker/__init__.py index 245761d..7f7bd5b 100644 --- a/EventDriven/riskmanager/picker/__init__.py +++ b/EventDriven/riskmanager/picker/__init__.py @@ -132,7 +132,9 @@ def __post_init__(self): "min_moneyness": 0.9, "max_moneyness": 1.1, "max_attempts": 3, - "increment": 0.25, + "increment": 0.05, + "max_pct_width": 0.20, + "min_oi": 100, } for key, default in optional.items(): if key not in self.data: @@ -162,10 +164,13 @@ def filter_contracts( spot: float, min_moneyness: float = 0.5, max_moneyness: float = 1.5, - increment=0.25, + increment=0.05, ) -> pd.DataFrame: + df = df.copy() df = df[df["right"].str.lower() == schema["option_type"].lower()] + if df.empty: + raise ValueError(f"No contracts found for {schema['option_type']} in the provided chain.") ## Calculate Moneyness if schema["option_type"].lower() == "c": @@ -176,43 +181,22 @@ def filter_contracts( dte_tol = schema["dte_tolerance"] filtered = pd.DataFrame() attempt = 0 - max_attempts = schema.get("max_attempts", 3) min_moneyness = schema.get("min_moneyness", min_moneyness) max_moneyness = schema.get("max_moneyness", max_moneyness) - max_pct_width = schema.get("max_pct_width", 0.10) ## NOTE: Add to schema - min_oi = schema.get("min_open_interest", 25) ## NOTE: Add to schema increment = schema.get("increment", increment) - while filtered.empty and attempt < max_attempts: - _filter = pd.Series([True] * len(df), index=df.index) - ## Add DTE filter - _filter &= df["dte"].between(target_dte - dte_tol, target_dte + dte_tol) + _filter = pd.Series([True] * len(df), index=df.index) - ## Add Moneyness filter. Convert to bounds based on increment + ## Add DTE filter + _filter &= df["dte"].between(target_dte - dte_tol, target_dte + dte_tol) - logger.info( - f"Filtering contracts with strike range [{min_moneyness:.2f}, {max_moneyness:.2f}] on attempt {attempt + 1}" - ) - _filter &= df["moneyness"].between(min_moneyness, max_moneyness) - - ## Update moneyness bounds for next attempt - min_moneyness *= 1 - increment - max_moneyness *= 1 + increment - - ## Add Open Interest filter if specified - if min_oi is not None: - logger.info(f"Applying minimum open interest filter: {min_oi}") - _filter &= df["open_interest"] >= min_oi - - ## Add Percentage Spread filter if specified - if max_pct_width is not None: - logger.info(f"Applying maximum percentage spread filter: {max_pct_width}") - _filter &= df["pct_spread"] <= max_pct_width + ## Add Moneyness filter. Convert to bounds based on increment + logger.info(f"Filtering contracts with strike range [{min_moneyness:.2f}, {max_moneyness:.2f}], dte range [{target_dte - dte_tol}, {target_dte + dte_tol}] on attempt {attempt + 1}") + _filter &= df["moneyness"].between(min_moneyness, max_moneyness) + filtered = df[_filter].copy() + logger.info(f"Number of contracts after filtering: {len(filtered)}") - filtered = df[_filter].copy() - logger.info(f"Number of contracts after filtering: {len(filtered)}") - attempt += 1 if filtered.empty: logger.critical( f"Failed to filter contracts: No contracts found for {schema['option_type']} with DTE {target_dte} ± {dte_tol} and strike range [{min_moneyness:.2f}, {max_moneyness:.2f}] after {attempt} attempts." diff --git a/EventDriven/riskmanager/picker/builder.py b/EventDriven/riskmanager/picker/builder.py index e4960ad..2ea1bea 100644 --- a/EventDriven/riskmanager/picker/builder.py +++ b/EventDriven/riskmanager/picker/builder.py @@ -1,9 +1,16 @@ -from .vertical_spread import vertical_spread_order_builder -from .naked_option import naked_option_order_builder -from ...types import ResultsEnum, OrderData + +from typing import Optional from trade.helpers.helper import parse_option_tick from EventDriven.riskmanager.picker import filter_contracts, OrderSchema import pandas as pd +from trade.helpers.Logging import setup_logger +import numpy as np +from .vertical_spread import vertical_spread_order_builder +from .naked_option import naked_option_order_builder +from ...types import ResultsEnum, OrderData +from .iv_helper import _add_greeks_and_iv_to_chain + +logger = setup_logger("EventDriven.riskmanager.picker.builder") BUILDER_FACTORY = { "vertical": vertical_spread_order_builder, @@ -11,7 +18,7 @@ } -def validate_order(order: dict) -> bool: +def validate_order(order: dict, date: pd.Timestamp, spot: Optional[float] = None) -> bool: """ Validates the order dictionary structure and contents. Raises ValueError if validation fails. @@ -38,12 +45,38 @@ def validate_order(order: dict) -> bool: parse_option_tick(leg) except Exception as e: raise ValueError(f"Invalid short leg opttick: {leg}. Error: {e}") from e + + ## Add min_dte to order + data = order.get("data", {}) + + ## No data to validate, so we can skip DTE calculation. + if not data: + return True + + ## If there is data, we want to calculate DTE for risk management purposes. + trade_id = order["data"].get("trade_id", None) + dt = [] + moneyness = [] + if trade_id is not None and hasattr(trade_id, "meta"): + for _, meta in trade_id.meta.items(): + for m in meta: + dt.append((pd.to_datetime(m["exp_date"]) - pd.to_datetime(date)).days) + if spot is not None: + moneyness.append(m["strike"] / spot if m["put_call"].lower() == "p" else spot / m["strike"]) + + order["metrics"]["min_dte"] = min(dt) if dt else None + order["metrics"]["max_dte"] = max(dt) if dt else None + order["metrics"]["min_moneyness"] = min(moneyness) if moneyness else None + order["metrics"]["max_moneyness"] = max(moneyness) if moneyness else None return True + def order_builder( unfiltered_chain: pd.DataFrame, schema: OrderSchema, spot: float, + date: pd.Timestamp, + delta_lmt: Optional[float] = None, ) -> OrderData: """ Build an order based on the unfiltered option chain and the provided schema. @@ -59,22 +92,39 @@ def order_builder( schema=schema, spot=spot, ) + logger.info(f"Recieved {len(unfiltered_chain)} contracts from data source. Delta limit for this order: {delta_lmt}") + + ## If delta filter is enabled, calculate Greeks and IV for the filtered chain to apply delta-based filtering in the builder functions. + ## If the chain is empty after initial filtering, we can skip this step to save computation. + ## If delta_lmt is None, it means the position manager did not provide a delta limit, so we should skip delta-based filtering in the builder functions as well. + if schema.get("enable_delta_filter", False) and not filtered_chain.empty and delta_lmt is not None: + logger.info(f"Calculating Greeks and IV for {len(filtered_chain)} contracts to apply delta filter...") + filtered_chain = _add_greeks_and_iv_to_chain(filtered_chain, date, spot) + else: + logger.info("Delta filter not enabled, skipping Greeks and IV calculation.") + filtered_chain[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = np.inf # Ensure these columns exist for builder functions, even if we are not calculating them. + + + logger.info(f"Filtered chain size: {len(filtered_chain)} contracts after applying schema filters.") # Step 2: Build order using the appropriate builder function structure_type = schema.get("strategy") if structure_type not in BUILDER_FACTORY: - raise ValueError(f"Unsupported structure type: {structure_type}. Supported types are: {list(BUILDER_FACTORY.keys())}") + raise ValueError( + f"Unsupported structure type: {structure_type}. Supported types are: {list(BUILDER_FACTORY.keys())}" + ) builder_function = BUILDER_FACTORY[structure_type] order = builder_function( filtered_chain=filtered_chain, schema=schema, + delta_lmt=delta_lmt, ) # Step 3: Validate the constructed order try: - validate_order(order) + validate_order(order, date, spot) except AssertionError as e: raise ValueError(f"Order validation failed: {e}") from e - return order \ No newline at end of file + return order diff --git a/EventDriven/riskmanager/picker/iv_helper.py b/EventDriven/riskmanager/picker/iv_helper.py new file mode 100644 index 0000000..e357472 --- /dev/null +++ b/EventDriven/riskmanager/picker/iv_helper.py @@ -0,0 +1,106 @@ + +from EventDriven._vars import load_riskmanager_cache +from trade.optionlib.assets.forward import vectorized_forward_continuous +from trade.helpers.helper import time_distance_helper +from trade.optionlib.vol.implied_vol import vector_bsm_iv_estimation +from trade.optionlib.greeks.analytical.black_scholes import black_scholes_analytic_greeks_vectorized +from trade.datamanager import RatesDataManager +import pandas as pd + +CHAIN_GREEKS_CACHE = load_riskmanager_cache(target="chain_greeks_cache", create_on_missing=True, clear_on_exit=False) +rates_cache = {} + + +def get_rates_on_date(date): + string_date = pd.to_datetime(date).strftime("%Y-%m-%d") + if string_date in rates_cache: + return rates_cache[string_date] + rates_dm = RatesDataManager() + rate = rates_dm.get_rate(date).timeseries.values[0] + rates_cache[string_date] = rate + return rate + + +def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chain_spot: float) -> pd.DataFrame: + date = pd.to_datetime(date).date() + ## get rates data for the date + at_time = get_rates_on_date(date) + + ## Filter for contracts with NaN iv to start adding greeks and iv + nan_iv_chain = filtered[filtered["iv"].isna()] + + ## Check cache for existing iv and greeks before calculating + cached_data = {} + for idx, row in nan_iv_chain.iterrows(): + contract_key = (row["root"], row["expiration"], row["strike"], row["right"], date) + if contract_key in CHAIN_GREEKS_CACHE: + cached_data[idx] = CHAIN_GREEKS_CACHE[contract_key] + + ## If there are cached values, add them to the filtered DataFrame and return early + if cached_data: + for idx, data in cached_data.items(): + filtered.loc[idx, "iv"] = data["iv"] + filtered.loc[idx, "delta"] = data["delta"] + filtered.loc[idx, "gamma"] = data["gamma"] + filtered.loc[idx, "vega"] = data["vega"] + filtered.loc[idx, "theta"] = data["theta"] + filtered.loc[idx, "rho"] = data["rho"] + filtered.loc[idx, "volga"] = data["volga"] + + ## Filter out the contracts that were found in the cache to avoid redundant calculations + if cached_data: + nan_iv_chain = nan_iv_chain[~nan_iv_chain.index.isin(cached_data.keys())] + + ## Calculate forward price for the contracts with NaN iv + t = time_distance_helper([date] * len(nan_iv_chain["expiration"].values), nan_iv_chain["expiration"].values) + f = vectorized_forward_continuous( + S=[chain_spot] * len(nan_iv_chain["expiration"].values), + r=[at_time] * len(nan_iv_chain["expiration"].values), + q_factor=[1] * len(nan_iv_chain["expiration"].values), + T=t, + ) + + ## Calculate iv for the contracts with NaN iv + iv = vector_bsm_iv_estimation( + F=f, + K=nan_iv_chain["strike"].values, + T=t, + r=[at_time] * len(nan_iv_chain["expiration"].values), + market_price=nan_iv_chain["midpoint"].values, + right=nan_iv_chain["right"].str.lower().values, + ) + + ## Calculate greeks for the contracts with NaN iv + greeks = black_scholes_analytic_greeks_vectorized( + F=f, + K=nan_iv_chain["strike"].values, + T=t, + r=[at_time] * len(nan_iv_chain["expiration"].values), + sigma=iv, + option_type=nan_iv_chain["right"].str.lower().values, + ) + + ## Add the calculated iv and greeks back to the filtered chain + greeks_df = pd.DataFrame(greeks, index=nan_iv_chain.index) + filtered.loc[nan_iv_chain.index, "iv"] = iv + filtered.loc[nan_iv_chain.index, "delta"] = greeks_df["delta"] + filtered.loc[nan_iv_chain.index, "gamma"] = greeks_df["gamma"] + filtered.loc[nan_iv_chain.index, "vega"] = greeks_df["vega"] + filtered.loc[nan_iv_chain.index, "theta"] = greeks_df["theta"] + filtered.loc[nan_iv_chain.index, "rho"] = greeks_df["rho"] + filtered.loc[nan_iv_chain.index, "volga"] = greeks_df["volga"] + + ## Store the calculated iv and greeks in the CHAIN_GREEKS_CACHE + for _, row in filtered.iterrows(): + contract_key = (row["root"], row["expiration"], row["strike"], row["right"], date) + CHAIN_GREEKS_CACHE[contract_key] = { + "iv": row["iv"], + "delta": row["delta"], + "gamma": row["gamma"], + "vega": row["vega"], + "theta": row["theta"], + "rho": row["rho"], + "volga": row["volga"], + } + + return filtered \ No newline at end of file diff --git a/EventDriven/riskmanager/picker/naked_option.py b/EventDriven/riskmanager/picker/naked_option.py index e8b7829..bda3b43 100644 --- a/EventDriven/riskmanager/picker/naked_option.py +++ b/EventDriven/riskmanager/picker/naked_option.py @@ -1,15 +1,23 @@ +from typing import Optional import numpy as np import pandas as pd from EventDriven.riskmanager.picker import _order_formatting, create_trade_id from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema +from trade.helpers.Logging import setup_logger +from .utils import _verify_delta_in_chain, _delta_lmt +logger = setup_logger("EventDriven.riskmanager.picker.naked_option") +## Utility function to pair naked option legs and calculate spread metrics by expiration date. def naked_option_by_exp( row: pd.Series, min_total_price: float = 0.5, max_total_price: float = 1.0, + max_pct_width: float = np.inf, + min_oi: int = 0, + delta_lmt: Optional[float] = None, ) -> pd.DataFrame: """ For a given row (option contract), find the corresponding leg of the naked option based on the spread_tick. @@ -24,20 +32,31 @@ def naked_option_by_exp( Returns: pd.DataFrame: A DataFrame containing the paired legs of the vertical spread and their calculated metrics, filtered by the total spread mid price. """ - tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest"] + logger.debug(f"naked_option_by_exp recieved delta_lmt: {delta_lmt}") ## DEBUG + tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest", "delta"] long_leg_details = row[tgt_details].reset_index(drop=True) ## Produce relevant spread information. spread_bid = long_leg_details["closebid"] spread_ask = long_leg_details["closeask"] spread_mid = long_leg_details["midpoint"] + spread_delta = long_leg_details["delta"] bid_ask_spread = spread_ask - spread_bid spread_pct_ratio = abs(spread_bid - spread_ask) / spread_mid.replace(0, np.nan) # Avoid division by zero. spread_oi = abs(long_leg_details["open_interest"]) ## Combine into a DataFrame for analysis. paired_opttick = pd.concat( - (long_leg_details["opttick"], spread_mid, spread_bid, spread_ask, bid_ask_spread, spread_pct_ratio, spread_oi), + ( + long_leg_details["opttick"], + spread_mid, + spread_bid, + spread_ask, + bid_ask_spread, + spread_pct_ratio, + spread_oi, + spread_delta, + ), axis=1, ) paired_opttick.columns = [ @@ -48,31 +67,53 @@ def naked_option_by_exp( "bid_ask_spread", "spread_pct_ratio", "spread_oi", + "spread_delta", ] - return paired_opttick[paired_opttick["spread_mid"].between(min_total_price, max_total_price)].reset_index(drop=True) - + mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) + spread_oi_mask = paired_opttick["spread_oi"] >= min_oi + pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width + spread_bid_mask = paired_opttick["spread_bid"] > 0 + spread_ask_mask = paired_opttick["spread_ask"] > 0 + delta_mask = paired_opttick["spread_delta"].abs() <= abs(_delta_lmt(delta_lmt)) + full_mask = mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask + logger.debug(f"Number of naked options after applying all filters: {full_mask.sum()}") ## DEBUG + logger.debug( + f"mid_mask: {mid_mask.sum()}, spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" + ) ## DEBUG + return paired_opttick[full_mask].reset_index(drop=True) + + +## Finder function to identify the best naked def _naked_option_finder( filtered_chain: pd.DataFrame, schema: OrderSchema, + delta_lmt: Optional[float] = None, ) -> pd.DataFrame: """ For a given filtered option chain, find the best option contract based on the spread_tick. Args: filtered_chain (pd.DataFrame): The filtered option chain DataFrame. schema (OrderSchema): The order schema containing parameters for building the naked option. + delta_lmt (Optional[float]): Optional delta limit for the naked option. Returns: pd.DataFrame: A Series containing the picked naked option contract details. """ + logger.debug(f"_naked_option_finder recieved delta_lmt: {delta_lmt}") ## DEBUG + if filtered_chain.empty: + return pd.Series() # Return empty Series if no contracts are available after filtering. # Start by ordering by strike, from ITM to OTM. # For calls, ITM is lower strike, for puts, ITM is higher strike. - + max_pct_width = schema.get("max_pct_width", 0.10) ## NOTE: Add to schema + min_oi = schema.get("min_oi", 25) ## NOTE: Add to schema is_call = schema["option_type"].lower() == "c" sorted_chain = filtered_chain.sort_values( by="strike", ascending=is_call, # Calls: Ascending (lower strike = ITM). Puts: Descending (higher strike = ITM). ).reset_index(drop=True) + if sorted_chain.empty: + return pd.Series() # Return empty Series if no contracts are available after filtering. naked_option_chain = ( sorted_chain.groupby("expiration") @@ -80,6 +121,9 @@ def _naked_option_finder( naked_option_by_exp, min_total_price=schema["min_total_price"], max_total_price=schema["max_total_price"], + min_oi=min_oi, + max_pct_width=max_pct_width, + delta_lmt=delta_lmt, ) .reset_index(level=1, drop=True) .sort_index() @@ -96,6 +140,7 @@ def _naked_option_finder( return picked_spread +## Extract order details from the picked spread and format for order construction. def _extract_order_for_naked_option( picked_spread: pd.Series, schema: OrderSchema, @@ -113,7 +158,7 @@ def _extract_order_for_naked_option( if not picked_spread.empty: ## Determine leg info for order formatting long = [{"opttick": picked_spread["long_leg_opttick"]}] if is_long else [] - short = [{"opttick": picked_spread["short_leg_opttick"]}] if not is_long else [] + short = [{"opttick": picked_spread["long_leg_opttick"]}] if not is_long else [] ## Determine leg info for order formatting leg_info = [] @@ -151,21 +196,29 @@ def _extract_order_for_naked_option( return order +## Final API function that is called by builder.py def naked_option_order_builder( filtered_chain: pd.DataFrame, schema: OrderSchema, + delta_lmt: Optional[float] = None, ) -> OrderDict: """ - Build a vertical spread order based on the filtered option chain and the provided schema. + Build a naked option order based on the filtered option chain and the provided schema. Args: filtered_chain (pd.DataFrame): The filtered option chain DataFrame. - schema (OrderSchema): The order schema containing parameters for building the vertical spread. + schema (OrderSchema): The order schema containing parameters for building the naked option. + delta_lmt (Optional[float]): Optional delta limit for the naked option. Returns: OrderDict: A dictionary containing the result status and order data. """ + logger.debug(f"naked_option_order_builder recieved delta_lmt: {delta_lmt}") ## DEBUG + filtered_chain = _verify_delta_in_chain( + filtered_chain + ) # Ensure delta column exists before proceeding to finder function. picked_spread = _naked_option_finder( filtered_chain=filtered_chain, schema=schema, + delta_lmt=delta_lmt, ) order = _extract_order_for_naked_option(picked_spread, schema=schema) - return order \ No newline at end of file + return order diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index 2e41fc8..73bf198 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -182,6 +182,7 @@ from datetime import datetime import pandas as pd from EventDriven.riskmanager._order_validator import OrderInputs +from trade.datamanager.market_data import Optional from ..utils import ( LOOKBACKS, populate_cache_with_chain, @@ -196,7 +197,7 @@ ) from .builder import order_builder from trade.helpers.Logging import setup_logger -from trade.helpers.decorators import timeit +from trade.helpers.decorators import timeit # noqa from EventDriven.riskmanager.picker import OrderSchema, _order_formatting from EventDriven.dataclasses.orders import OrderRequest from EventDriven.riskmanager._orders import order_resolve_loop, order_failed @@ -298,6 +299,7 @@ def get_order_schema(self, ticker: str, option_type: str = "P", max_total_price: "min_total_price": self._order_schema_config.min_total_price, "option_type": option_type, # Default to Put options "max_total_price": max_total_price, + "max_attempts": self._order_schema_config.max_attempts, } ) schema.data.update(self._chain_config.__dict__) @@ -318,10 +320,11 @@ def get_order_new( spot, chain_spot: float = None, print_url: bool = False, + delta_lmt: Optional[float] = None, ): schema = tuple(schema.data.items()) chain_spot = spot - return self._get_order(schema, date, spot, chain_spot, print_url=print_url) + return self._get_order(schema, date, spot, chain_spot, print_url=print_url, delta_lmt=delta_lmt) # @dynamic_memoize def _get_order( @@ -331,6 +334,7 @@ def _get_order( spot: float, chain_spot: float = None, print_url: bool = False, + delta_lmt: Optional[float] = None, ) -> dict: """ Get the order for the given schema, date, and spot price. @@ -338,35 +342,10 @@ def _get_order( 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 - # logger.info( - # f"Call Option Detected, Pre-Adjustment Moneyness: {schema['min_moneyness']} - {schema['max_moneyness']}" - # ) - # min_m, max_m = 2 - schema["min_moneyness"], 2 - schema["max_moneyness"] - # schema["min_moneyness"] = min(min_m, max_m) ## For Calls, we want the min moneyness to be 2 - min_moneyness - # schema["max_moneyness"] = max(min_m, max_m) - # logger.info( - # f"Call Option Detected, Adjusting Moneyness: {schema['min_moneyness']} - {schema['max_moneyness']}" - # ) - # elif ( - # schema["option_type"].lower() == "p" - # ): ## This ensures that both call and put OTM are < 1.0 and ITM are > 1.0 - # logger.info( - # f"Put Option Detected, Pre-Adjustment Moneyness: {schema['min_moneyness']} - {schema['max_moneyness']}" - # ) - # else: - # raise ValueError(f"Invalid option type: {schema['option_type']}. Must be 'c' or 'p'.") - chain = populate_cache_with_chain(schema["tick"], date, chain_spot, print_url=print_url) + return order_builder(unfiltered_chain=chain.copy(deep=True), schema=schema, spot=chain_spot, date=date, delta_lmt=delta_lmt) - # cache = get_cache("spot") - # cache = {k: v for k, v in cache.items()} - - # raw_order = build_strategy(chain, schema, chain_spot, cache) - # return extract_order(raw_order) - return order_builder(unfiltered_chain=chain, schema=schema, spot=chain_spot) - - @timeit + # @timeit def get_order(self, request: OrderRequest) -> Order: """ Get the order based on the request. @@ -378,6 +357,11 @@ def get_order(self, request: OrderRequest) -> Order: inputs = self.construct_inputs( request=request, schema=schema, order_resolution_config=self._order_resolution_config ) + if not self._chain_config.enable_delta_filter: + logger.info("Delta filter not enabled in chain config. Setting delta_lmt to None for order builder.") + request.delta_lmt = None # Ensure delta_lmt is None if delta filtering is not enabled + else: + logger.info(f"Delta filter enabled in chain config. Using delta_lmt {request.delta_lmt} for order builder.") return _get_open_order_backtest( picker=self, request=request, @@ -438,6 +422,7 @@ def _get_open_order_backtest( returns: Order: The resolved order object. """ + delta_lmt = request.delta_lmt 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.") @@ -446,7 +431,12 @@ def _get_open_order_backtest( ) 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 + schema=schema_as_tuple, + date=request.date, + spot=request.spot, + chain_spot=request.chain_spot, + delta_lmt=delta_lmt, + print_url=False ) ## Resolve order if failed and resolution is enabled @@ -455,7 +445,7 @@ def _get_open_order_backtest( order=order, schema=schema, date=inputs.date, - spot=inputs.spot, + spot=request.chain_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, @@ -465,7 +455,8 @@ def _get_open_order_backtest( signalID=inputs.signal_id, schema_cache={}, picker=picker, - tick_cash=request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash/100 + tick_cash=request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash / 100, + delta_lmt=delta_lmt, ) else: logger.info(f"Preset order found for signal_id {inputs.signal_id} on date {inputs.date}. Using preset order.") diff --git a/EventDriven/riskmanager/picker/utils.py b/EventDriven/riskmanager/picker/utils.py new file mode 100644 index 0000000..5fa126d --- /dev/null +++ b/EventDriven/riskmanager/picker/utils.py @@ -0,0 +1,47 @@ +import pandas as pd +import numpy as np +from typing import Union +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.picker.utils") +def _verify_delta_in_chain(chain: pd.DataFrame) -> pd.DataFrame: + """ + Verify if the 'delta' column exists in the option chain DataFrame. If it does not exist, add it with NaN values. + This ensures that downstream functions that expect a 'delta' column can operate without errors. + Args: + chain (pd.DataFrame): The option chain DataFrame to verify. + Returns: + pd.DataFrame: The option chain DataFrame with a 'delta' column (either existing or newly added with NaN values). + """ + if "delta" not in chain.columns: + logger.warning("Delta column not found in option chain. Adding 'delta' column with NaN values.") + ## Opting for np.inf instead of NaN to avoid potential issues with NaN values in calculations later on. This way, if delta is not available, it will be treated as infinitely far from any reasonable delta limit, effectively excluding those contracts if delta filtering is applied. + chain["delta"] = np.inf + return chain + + +def _delta_lmt(f: float) -> Union[float, np.float64]: + """ + Utility function to convert a delta limit percentage (e.g., 0.10 for 10%) into an absolute delta limit value. + This is based on the assumption that the delta of an option ranges from -1 to 1, where -1 represents a deep ITM put, 0 represents an ATM option, and 1 represents a deep ITM call. + Args: + f (float): The delta limit percentage (e.g., 0.10 for 10%). + Returns: + Union[float, np.float64]: The absolute delta limit value corresponding to the provided percentage. + """ + if f is None: + return np.inf # If no delta limit percentage is provided, we set it to infinity to not filter based on delta. + + if isinstance(f, str): + try: + f = float(f) + except ValueError as e: + raise ValueError(f"Invalid delta limit percentage string: {f}. Error: {e}") from e + + if not isinstance(f, (float, int)): + raise ValueError(f"Delta limit percentage must be a float or int. Received type {type(f)} with value: {f}") + + if np.isnan(f): + return np.inf # If no delta limit percentage is provided, we set it to infinity to not filter based on delta. + + return np.float64(f) \ No newline at end of file diff --git a/EventDriven/riskmanager/picker/vertical_spread.py b/EventDriven/riskmanager/picker/vertical_spread.py index f499ee7..95ef2ed 100644 --- a/EventDriven/riskmanager/picker/vertical_spread.py +++ b/EventDriven/riskmanager/picker/vertical_spread.py @@ -1,9 +1,13 @@ - import numpy as np import pandas as pd +from typing import Optional from EventDriven.riskmanager.picker import _order_formatting, create_trade_id +from EventDriven.riskmanager.picker.utils import _delta_lmt, _verify_delta_in_chain from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.picker.vertical_spread") def vertical_spread_pairer_by_exp( @@ -11,7 +15,9 @@ def vertical_spread_pairer_by_exp( spread_tick: int = 1, min_total_price: float = 0.5, max_total_price: float = 1.0, - add_spread_filters: bool = True, + max_pct_width: float = np.inf, + min_oi: int = 0, + delta_lmt: Optional[float] = None, ) -> pd.DataFrame: """ For a given row (option contract), find the corresponding leg of the vertical spread based on the spread_tick. @@ -23,12 +29,13 @@ def vertical_spread_pairer_by_exp( spread_tick (int): The number of ticks between the legs of the spread. min_total_price (float): Minimum total price of the spread to filter the results. max_total_price (float): Maximum total price of the spread to filter the results. + delta_lmt (Optional[float]): Optional delta limit for the spread. Returns: pd.DataFrame: A DataFrame containing the paired legs of the vertical spread and their calculated metrics, filtered by the total spread mid price. """ - tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest"] - if "volume" in row.columns: - tgt_details.append("volume") + logger.debug(f"vertical_spread_pairer_by_exp recieved delta_lmt: {delta_lmt}") ## DEBUG + tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest", "delta"] + long_leg_details = row[tgt_details].reset_index(drop=True) short_leg_details = row[tgt_details].shift(-spread_tick).reset_index(drop=True) @@ -41,6 +48,9 @@ def vertical_spread_pairer_by_exp( spread_bid = long_leg_details["closebid"] - short_leg_details["closeask"] spread_ask = long_leg_details["closeask"] - short_leg_details["closebid"] spread_mid = long_leg_details["midpoint"] - short_leg_details["midpoint"] + spread_delta = (long_leg_details["delta"] - short_leg_details["delta"]).fillna( + np.inf + ) # If delta is missing, set to infinity to fail delta filter. bid_ask_spread = spread_ask - spread_bid spread_pct_ratio = abs(spread_bid - spread_ask) / spread_mid.replace(0, np.nan) # Avoid division by zero. spread_oi = abs(long_leg_details["open_interest"] + short_leg_details["open_interest"]) @@ -62,6 +72,7 @@ def vertical_spread_pairer_by_exp( spread_pct_ratio, spread_oi, spread_volume, + spread_delta, ), axis=1, ) @@ -75,55 +86,67 @@ def vertical_spread_pairer_by_exp( "spread_pct_ratio", "spread_oi", "spread_volume", + "spread_delta", ] - ## Ensure bid, ask > 0 - ## Ensure spread_pct <= 1.0 (we want tight spreads relative to the mid price) - if add_spread_filters: - paired_opttick = paired_opttick[ - (paired_opttick["spread_bid"] > 0) - & (paired_opttick["spread_ask"] > 0) - & (paired_opttick["spread_pct_ratio"] <= 1.25) - ].reset_index(drop=True) - return paired_opttick[paired_opttick["spread_mid"].between(min_total_price, max_total_price)].reset_index(drop=True) + + mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) + spread_oi_mask = paired_opttick["spread_oi"] >= min_oi + pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width + spread_bid_mask = paired_opttick["spread_bid"] > 0 + spread_ask_mask = paired_opttick["spread_ask"] > 0 + delta_mask = paired_opttick["spread_delta"].abs() <= abs(_delta_lmt(delta_lmt)) + full_mask = mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask + logger.debug(f"Number of spreads after applying all filters: {full_mask.sum()}") ## DEBUG + logger.debug( + f"spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" + ) ## DEBUG + return paired_opttick[full_mask].reset_index(drop=True) def _vertical_spread_pairer( filtered_chain: pd.DataFrame, schema: OrderSchema, -) -> pd.DataFrame: + delta_lmt: Optional[float] = None, +) -> pd.Series: """ For a given filtered option chain, find the best option contract based on the spread_tick. Args: filtered_chain (pd.DataFrame): The filtered option chain DataFrame. schema (OrderSchema): The order schema containing parameters for building the vertical spread. + delta_lmt (Optional[float]): The delta limit for filtering options. Defaults to None. Returns: - pd.DataFrame: A Series containing the picked vertical spread contract details. + pd.Series: A Series containing the picked vertical spread contract details. """ + logger.debug(f"_vertical_spread_pairer recieved delta_lmt: {delta_lmt}") ## DEBUG + if filtered_chain.empty: + return pd.Series() # Return empty Series if no contracts are available after filtering. # Start by ordering by strike, from ITM to OTM. # For calls, ITM is lower strike, for puts, ITM is higher strike. - + max_pct_width = schema.get("max_pct_width", 0.10) ## NOTE: Add to schema + min_oi = schema.get("min_oi", 25) ## NOTE: Add to schema spread_tick = schema["spread_ticks"] is_call = schema["option_type"].lower() == "c" sorted_chain = filtered_chain.sort_values( by="strike", ascending=is_call, # Calls: Ascending (lower strike = ITM). Puts: Descending (higher strike = ITM). ).reset_index(drop=True) + if sorted_chain.empty: + return pd.Series() # Return empty Series if no contracts are available after filtering. # spread_ticks is the number of ticks between the legs of the spread. # For a call spread with spread_ticks=1, we buy the ITM call and sell the next lower strike call. # For a put spread with spread_ticks=1, we buy the ITM put and sell the next higher strike put. # For vertical spreads it is important that the legs are paired to expiration. - vertical_chain = ( - sorted_chain.groupby("expiration") - .apply( - vertical_spread_pairer_by_exp, - spread_tick=spread_tick, - min_total_price=schema["min_total_price"], - max_total_price=schema["max_total_price"], - ) - .reset_index(level=1, drop=True) - .sort_index() + vertical_chain = sorted_chain.groupby("expiration").apply( + vertical_spread_pairer_by_exp, + spread_tick=spread_tick, + min_total_price=schema["min_total_price"], + max_total_price=schema["max_total_price"], + max_pct_width=max_pct_width, + min_oi=min_oi, + delta_lmt=delta_lmt, ) + vertical_chain = vertical_chain.reset_index(level=1, drop=True).sort_index() ## Now we have our vertical spread chain with paired optticks and spread metrics for analysis. ## We pick the spread we want based on specific criteria. We sort based on (this is by priority): @@ -136,13 +159,13 @@ def _vertical_spread_pairer( def _extract_order_for_vertical_spread(picked_spread: pd.Series, schema: OrderSchema) -> OrderDict: - """ - Extract order details for a vertical spread based on the picked spread and the provided schema. - Args: - picked_spread (pd.Series): A Series containing the details of the picked vertical spread contract. - schema (OrderSchema): The order schema containing parameters for building the vertical spread. - Returns: - "OrderResult": A dictionary containing the result status and order data. + """ + Extract order details for a vertical spread based on the picked spread and the provided schema. + Args: + picked_spread (pd.Series): A Series containing the details of the picked vertical spread contract. + schema (OrderSchema): The order schema containing parameters for building the vertical spread. + Returns: + "OrderResult": A dictionary containing the result status and order data. """ ## Extract order if not picked_spread.empty: @@ -160,9 +183,7 @@ def _extract_order_for_vertical_spread(picked_spread: pd.Series, schema: OrderSc "short": short, } ) - data = _order_formatting( - trade_id=trade_id, legs=leg_info, close=close_price, dir=schema["structure_direction"] - ) + data = _order_formatting(trade_id=trade_id, legs=leg_info, close=close_price, dir=schema["structure_direction"]) order = { "result": ResultsEnum.SUCCESSFUL.value, "data": data, @@ -183,7 +204,8 @@ def _extract_order_for_vertical_spread(picked_spread: pd.Series, schema: OrderSc def vertical_spread_order_builder( filtered_chain: pd.DataFrame, schema: dict, -) -> OrderDict: + delta_lmt: Optional[float] = None, +) -> OrderDict: """ Build a vertical spread order based on the filtered option chain and the provided schema. Args: @@ -192,9 +214,14 @@ def vertical_spread_order_builder( Returns: dict: A dictionary containing the result status and order data. """ + logger.debug(f"vertical_spread_order_builder recieved delta_lmt: {delta_lmt}") ## DEBUG picked_spread = _vertical_spread_pairer( filtered_chain=filtered_chain, schema=schema, + delta_lmt=delta_lmt, ) + filtered_chain = _verify_delta_in_chain( + filtered_chain + ) # Ensure delta column exists before proceeding to finder function. order = _extract_order_for_vertical_spread(picked_spread, schema=schema) return order diff --git a/EventDriven/riskmanager/position/analyzer.py b/EventDriven/riskmanager/position/analyzer.py index d5c9707..bb03d80 100644 --- a/EventDriven/riskmanager/position/analyzer.py +++ b/EventDriven/riskmanager/position/analyzer.py @@ -204,6 +204,7 @@ """ from typing import Iterable, List, Dict +import pandas as pd from trade.helpers.Logging import setup_logger from .base import ( BaseCog, @@ -350,3 +351,13 @@ def on_new_position(self, new_position_state: NewPositionState) -> NewPositionSt for cog in self._cogs.values(): cog.on_new_position(new_position_state) return new_position_state + + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: + """ + Utility method to calculate delta limits based on cash and spot price. + This can be used by cogs to determine position sizing constraints. + """ + lmts = [] + for cog in self._cogs.values(): + lmts.append(cog.get_delta_limit(tick_cash=tick_cash, chain_spot=chain_spot, date=date, ticker=ticker)) + return min(lmts) if lmts else float("inf") diff --git a/EventDriven/riskmanager/position/base.py b/EventDriven/riskmanager/position/base.py index 483dfd0..01b0760 100644 --- a/EventDriven/riskmanager/position/base.py +++ b/EventDriven/riskmanager/position/base.py @@ -219,6 +219,10 @@ def on_new_position(self, order, request): PositionAnalysisContext, CogActions, ) +from trade.helpers.Logging import setup_logger +import pandas as pd + +logger = setup_logger("EventDriven.riskmanager.position.base") ### RUNTIME DATACLASSES @@ -266,7 +270,7 @@ class BaseCog: default_config_class_attr_name = "default_config" def __init__(self, config: BaseCogConfig): - assert isinstance(config, BaseCogConfig), "config must be an instance of BaseCogConfig or its subclass" + assert isinstance(config, BaseCogConfig), f"config must be an instance of BaseCogConfig or its subclass, got {type(config)}" self._config = config def __init_subclass__(cls): @@ -330,3 +334,11 @@ def on_new_position(self, order: Order, request: OrderRequest) -> None: Subclasses can override this to perform any initialization or logging. """ raise NotImplementedError("Subclasses must implement on_new_position().") + + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: + """ + Hook method to provide delta limits for position sizing. + Subclasses can override this to return specific limits based on their logic. + By default, returns infinity (no limit). + """ + return float("inf") diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index bb3b66c..79a78f8 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -221,6 +221,7 @@ - Config changes trigger sizer reload """ +import numpy as np import pandas as pd from dataclasses import dataclass from datetime import datetime @@ -231,7 +232,7 @@ from typing import List, Optional, Union, Dict from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.position.base import BaseCog -from EventDriven.riskmanager.sizer._sizer import DefaultSizer, BaseSizer, ZscoreRVolSizer +from EventDriven.riskmanager.sizer._sizer import DefaultSizer, BaseSizer, ZscoreRVolSizer, default_delta_limit # noqa from EventDriven.configs.core import ZscoreSizerConfigs, DefaultSizerConfigs from EventDriven.dataclasses.limits import PositionLimits from EventDriven.dataclasses.states import ( @@ -395,7 +396,7 @@ def _calculate_limits(self, new_pos_state: NewPositionState) -> float: current_cash=request.tick_cash, underlier_price_at_time=undl_data.chain_spot["close"], ) - logger.info(f"Calculated limits for position {order['data']['trade_id']}: {limits}") + logger.info(f"Calculated limits for position {order['data']['trade_id']}: {limits}. Details: cash={request.tick_cash}, undl_price={undl_data.chain_spot['close']}") pos_lmts = PositionLimits( delta=limits, dte=self.config.default_dte, @@ -515,4 +516,12 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction action.verbose_info = None position.action = action opinions.append(position) - return CogActions(opinions=opinions, strategy_id="", date=portfolio_context.date, source_cog=self.name) + return CogActions(opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name) + + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: + """ + Override to provide delta limits based on mean reversion logic. + This can be used by the position manager to enforce sizing constraints. + """ + + return np.inf # No limit by default, can be overridden by specific logic in cogs like mean reversion cog \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/mean_reversion.py b/EventDriven/riskmanager/position/cogs/mean_reversion.py new file mode 100644 index 0000000..fcb5935 --- /dev/null +++ b/EventDriven/riskmanager/position/cogs/mean_reversion.py @@ -0,0 +1,124 @@ +from EventDriven.riskmanager.position.base import BaseCog +from EventDriven.configs.core import MeanReversionSizerConfigs +from typing import Optional +from EventDriven.dataclasses.states import NewPositionState, PositionAnalysisContext, CogActions +import numpy as np +from EventDriven.riskmanager.sizer._utils import default_delta_limit, delta_position_sizing +from EventDriven.riskmanager.position.cogs.limits import _LimitsMetaData +from trade.backtester_._multi_asset_strategy import MultiAssetStrategy +from dataclasses import dataclass +import pandas as pd +from trade.helpers.Logging import setup_logger +logger = setup_logger("EventDriven.riskmanager.position.cogs.mean_reversion") + + +@dataclass +class _MRLimitsMetaData(_LimitsMetaData): + zscore: float = None + zexcess: float = None + +class MeanReversionSizerCog(BaseCog): + default_config: MeanReversionSizerConfigs = MeanReversionSizerConfigs() + + def __init__(self, eq_strategy: MultiAssetStrategy, config: Optional[MeanReversionSizerConfigs] = None): + if config is None: + config: MeanReversionSizerConfigs = self.default_config + super().__init__(config) + self.eq_strategy = eq_strategy + self.position_metadata = {} + + def on_new_position(self, state: NewPositionState): + order = state.order + requet = state.request + ticker = state.symbol + undl_data = state.undl_at_time_data + option_chain = state.at_time_data + cash_available = requet.tick_cash + opt_price = option_chain.get_price() + date = order["date"] + info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) + z_raw = (info["indicators"]["zscore"]) + + ## scale z-score on the remainder of the distance to the minimum z-score threshold. + ## if z-score is below the minimum threshold, no scaling is applied + z_excess = max(0, abs(z_raw) - self.config.min_zscore) + + + ## Calculate scaler based on z-score and config parameters + scaler_raw = 1 + self.config.beta * z_excess + scaler = np.clip(scaler_raw, self.config.min_scale, self.config.max_scale) + + ## Update order quantity based on scaler + limit = self.get_delta_limit( + tick_cash=cash_available, + chain_spot=undl_data.chain_spot["close"], + date=date, + ticker=ticker + ) + + delta = state.at_time_data.delta + q = delta_position_sizing( + cash_available=cash_available, + option_price_at_time=opt_price, + delta=delta, + delta_limit=limit, + ) + + ## Update order quantity and log metadata + order["data"]["quantity"] = q + metadata = _MRLimitsMetaData( + trade_id=order["data"]["trade_id"], + date=order["date"], + signal_id=order["signal_id"], + scalar=scaler, + sizing_lev=self.config.sizing_lev, + delta_per_contract=delta, + option_price=opt_price, + undl_price=undl_data.chain_spot["close"], + delta_lmt=limit, + new_quantity=q, + zscore=z_raw, + zexcess=z_excess, + ) + logger.info(f"Storing metadata for trade_id {order['data']['trade_id']}: {metadata}") + self.position_metadata[order["data"]["trade_id"]] = metadata + + def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: + return CogActions( + opinions=[], strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name + ) + + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: + """ + Calculate delta limits based on cash, spot price, and z-score for mean reversion scaling. + Principle: The more extreme the z-score (beyond the minimum threshold), the larger the position we can take, up to a maximum scaling factor. + Asumes that the base delta limit is calculated using the default method, and then scaled based on the z-score excess. + + Args: + tick_cash (float): Available cash for the position. + chain_spot (float): Spot price of the underlying at the time of order. + date (pd.Timestamp): Current date for fetching strategy info. + ticker (str): Ticker symbol for fetching strategy info. + Returns: + float: Adjusted delta limit based on mean reversion scaling. + """ + + info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) + z_raw = info["indicators"]["zscore"] + + ## scale z-score on the remainder of the distance to the minimum z-score threshold. + ## if z-score is below the minimum threshold, no scaling is applied + z_excess = max(0, abs(z_raw) - self.config.min_zscore) + base_delta = default_delta_limit( + cash_available=tick_cash, + underlier_price_at_time=chain_spot, + sizing_lev=self.config.sizing_lev, + ) + + ## Calculate scaler based on z-score and config parameters + scaler_raw = 1 + self.config.beta * z_excess + scaler = np.clip(scaler_raw, self.config.min_scale, self.config.max_scale) + + ## Update order quantity based on scaler + limit = base_delta * scaler + return limit diff --git a/EventDriven/types.py b/EventDriven/types.py index 1986c5a..be2e662 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -3,14 +3,30 @@ import pandas as pd import numpy as np from dataclasses import dataclass -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from typing_extensions import TypedDict from EventDriven.helpers import parse_signal_id, generate_signal_id, parse_position_id +from trade.helpers.Logging import setup_logger +logger = setup_logger("EventDriven.types") + +class OptionFloat(float): + """Custom float type for option-related values to allow for future extensions or validations.""" + + def __new__(cls, value): + return super().__new__(cls, value) + + def __init__(self, value, dollar_normalized=False): + super().__init__() + self.dollar_normalized = dollar_normalized class Metrics(TypedDict): - spread_pct_ratio: float - spread_oi: float + spread_pct_ratio: Optional[float] + spread_oi: Optional[float] + min_dte: Optional[int] + max_dte: Optional[int] + min_moneyness: Optional[float] + max_moneyness: Optional[float] class SignalID(str): @@ -41,7 +57,7 @@ def generate(underlier: str, date: pd.Timestamp, signal_type: str) -> "SignalID" def __repr__(self) -> str: return f"SignalID({str(self)})" - + def __str__(self): return super().__str__() @@ -63,7 +79,7 @@ def __init__(self, trade_id: str) -> None: def __repr__(self) -> str: return f"TradeID({str(self)})" - + def __str__(self): return super().__str__() @@ -327,7 +343,14 @@ def from_dict(d: Dict[str, Any]) -> "Order": data_dict = {"trade_id": None, "long": None, "short": None, "close": None, "quantity": None} if metrics is not None: - d["metrics"] = Metrics(spread_pct_ratio=metrics["spread_pct_ratio"], spread_oi=metrics["spread_oi"]) + d["metrics"] = Metrics( + spread_pct_ratio=metrics["spread_pct_ratio"], + spread_oi=metrics["spread_oi"], + min_dte=metrics.get("min_dte", None), + max_dte=metrics.get("max_dte", None), + min_moneyness=metrics.get("min_moneyness", None), + max_moneyness=metrics.get("max_moneyness", None), + ) else: d["metrics"] = None From e44cc11d390278a699b8008187ea3ad97e6c61d3 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:29:10 -0400 Subject: [PATCH 04/81] Refine EventDriven backtest, portfolio, and execution flow --- EventDriven/backtest.py | 106 ++++++++++++++++----- EventDriven/event.py | 172 ++++++++++++++++++++++------------ EventDriven/eventScheduler.py | 114 ++++++++++++---------- EventDriven/execution.py | 35 ++++++- EventDriven/new_portfolio.py | 74 +++++++++++---- EventDriven/trade.py | 168 +++++++++++++++++---------------- EventDriven/tradeLedger.py | 114 ++++++++++++++-------- 7 files changed, 511 insertions(+), 272 deletions(-) diff --git a/EventDriven/backtest.py b/EventDriven/backtest.py index 5f1b139..3a6dcdd 100644 --- a/EventDriven/backtest.py +++ b/EventDriven/backtest.py @@ -1,5 +1,5 @@ from queue import Empty as emptyEventQueue -from typing import Optional, cast +from typing import Dict, Optional, cast import pandas as pd from EventDriven.data import HistoricTradeDataHandler from EventDriven.event import Event @@ -10,16 +10,15 @@ from EventDriven.eventScheduler import EventScheduler from trade.backtester_._multi_asset_strategy import MultiAssetStrategy from trade.helpers.Logging import setup_logger -from trade.helpers.helper import change_to_last_busday +from trade.helpers.helper import change_to_last_busday, is_USholiday from EventDriven.helpers import generate_signal_id -from copy import deepcopy +from copy import deepcopy # noqa import traceback from pandas.tseries.offsets import BDay from EventDriven.types import EventTypes, SignalTypes from EventDriven.configs.core import BacktesterConfig -from .exceptions import EVBacktestError -LOGGER = setup_logger("OptionSignalBacktest", stream_log_level="WARNING") +LOGGER = setup_logger("EventDriven.backtest", stream_log_level="WARNING") class OptionSignalBacktest: @@ -195,6 +194,12 @@ def __init__( self.is_eq_strategy = eq_strategy is not None self.eq_strategy: Optional[MultiAssetStrategy] = eq_strategy self.strategy: Optional[OptionSignalStrategy] = None + self.initial_capital = initial_capital + self.trades = trades + self.symbol_list = symbol_list + + ## Tracker for eq_strategy runs, to ensure we only run the strategy once per date + self.run_dates: Dict[pd.Timestamp, bool] = {} if config is not None and not isinstance(config, BacktesterConfig): raise TypeError("config must be an instance of BacktesterConfig or None") @@ -227,20 +232,20 @@ def __init__with_equity_strategy( EVBacktestError: If tplusn value of the equity strategy does not match the backtest configuration. """ assert eq_strategy is not None, "Equity strategy must be provided for this initialization method" - assert isinstance( - eq_strategy, MultiAssetStrategy - ), f"eq_strategy must be an instance of MultiAssetStrategy, got {type(eq_strategy)}" + assert isinstance(eq_strategy, MultiAssetStrategy), ( + f"eq_strategy must be an instance of MultiAssetStrategy, got {type(eq_strategy)}" + ) assert self.end_date is not None, "end_date must be provided for this initialization method" - if eq_strategy.tplusn != self.config.t_plus_n: - raise EVBacktestError(f"tplusn value of the equity strategy does not match the backtest configuration. eq_strategy.tplusn: {eq_strategy.tplusn}, config.t_plus_n: {self.config.t_plus_n}") ## We will not use trades dataframe in this process. self.start_date = pd.to_datetime(eq_strategy.start_date).date() + if is_USholiday(self.start_date): + self.logger.warning(f"Start date {self.start_date} is a US holiday. Adjusting to previous business day.") + self.start_date = change_to_last_busday(self.start_date, -1).date() + start_date, end_date = self.start_date, self.end_date self.eq_strategy.reset_strategies() - - ## Initialize critical components self.eventScheduler = EventScheduler(start_date, end_date) self.bars = HistoricTradeDataHandler( @@ -362,19 +367,66 @@ def __handle_t_plus_n(self, trades: pd.DataFrame) -> pd.DataFrame: ## Only adjust ExitTime if it is not NaT trades["ExitTime"] = trades["ExitTime"].apply( - lambda x: change_to_last_busday(pd.to_datetime(x) + BDay(self.config.t_plus_n), -1).replace(hour=0) - if pd.notna(x) - else x + lambda x: ( + change_to_last_busday(pd.to_datetime(x) + BDay(self.config.t_plus_n), -1).replace(hour=0) + if pd.notna(x) + else x + ) ) ## Adjust ExitTime by t_plus_n business days, and offseting to next business day if holiday return trades + + def reset(self): + """ + Resets the backtest to its initial state, allowing for a fresh run with the original trades and configuration. + This method reinitializes all components and clears any generated events or portfolio state. + """ + self.logger.info("Resetting backtest to initial state.") + self.__init__( + trades=self.trades, + initial_capital=self.initial_capital, + eq_strategy=self.eq_strategy, + config=self.config, + end_date=self.end_date, + symbol_list=self.symbol_list, + ) + def _pre_signal_analysis(self): + """ + Placeholder for any analysis or operations that need to be performed before signal processing in each loop iteration. + This can include things like checking for roll conditions, updating market data, or any other pre-signal logic. + """ + + has_run_strategy = self.run_dates.get(self.eventScheduler.current_date, False) + if self.is_eq_strategy and self.eq_strategy.tplusn == 0 and not has_run_strategy: + ## For equity strategy, we want to run the strategy at the beginning of the loop before processing any events, + ## to ensure that we capture signals generated for the current date in the same loop iteration. If we put it after get_nowait, + ## we might miss signals generated for the current date until the next loop iteration, which could lead to delayed signal processing and execution + self.logger.info(f"Running equity strategy with T+0 settlement on {self.eventScheduler.current_date}") + self.portfolio.analyze_multiasset_strategy() + self.run_dates[self.eventScheduler.current_date] = True + self.logger.info(f"Completed running equity strategy for {self.eventScheduler.current_date}") + + def _post_signal_analysis(self): + """ + Placeholder for any analysis or operations that need to be performed after signal processing in each loop iteration. + This can include things like analyzing positions, updating portfolio metrics, or any other post-signal logic. + """ + meta = self.portfolio.analyze_positions() # noqa + self.logger.info(f"Position Analysis Meta: {meta}") + + ## For equity strategy with T+n (n>=1), we want to run the strategy after analyzing positions, + ## to ensure that we are using the most up-to-date position information for the strategy analysis. + ## This is especially important for T+1 strategies, where the signals generated on the current date will only be actionable on the next business day. + ## By running the strategy after position analysis, we can ensure that any new signals generated based on the current positions and market data are captured and processed in a timely manner in the next loop iteration. + if self.is_eq_strategy and self.eq_strategy.tplusn >= 1: + self.logger.info(f"Running equity strategy with T+{self.eq_strategy.tplusn} settlement on {self.eventScheduler.current_date}") + self.portfolio.analyze_multiasset_strategy() def run(self): ## Runtime configurations changes self.portfolio.t_plus_n = self.config.t_plus_n self.executor.max_slippage_pct = self.config.max_slippage_pct self.executor.min_slippage_pct = self.config.min_slippage_pct - self.risk_manager.config.min_slippage_pct = self.config.min_slippage_pct - self.risk_manager.config.max_slippage_pct = self.config.max_slippage_pct + self.executor.commission_rate = self.config.commission_per_contract_in_units ## Begin backtest by looping through event scheduler dates while True: @@ -387,15 +439,25 @@ def run(self): self.logger.info(f"Processing events for {self.eventScheduler.current_date}") current_event_queue = self.eventScheduler.get_current_queue() event_count = 0 + _post_signal_ran = False # Process events for the current bar # Avoid blocking. Loops through the event queue while True: + self._pre_signal_analysis() # Placeholder for any pre-signal processing logic + try: + # ## Placing before get_nowait because I want to check for roll, and if there is no roll, I want to break out of the loop + # if len(list(deepcopy(current_event_queue.queue))) == 0: + # meta = self.portfolio.analyze_positions() # noqa + # # print(f"Position Analysis Meta: {meta}") + ## Placing before get_nowait because I want to check for roll, and if there is no roll, I want to break out of the loop - if len(list(deepcopy(current_event_queue.queue))) == 0: - meta = self.portfolio.analyze_positions() # noqa - # print(f"Position Analysis Meta: {meta}") + if current_event_queue.empty() and not _post_signal_ran: + self.logger.info(f"Event queue is empty, processed {event_count} event(s)") + + self._post_signal_analysis() + _post_signal_ran = True event = current_event_queue.get_nowait() @@ -405,10 +467,6 @@ def run(self): # Update portfolio time index after processing all events self.portfolio.update_timeindex() - # Analyze eq_strategy if applicable - if self.is_eq_strategy: - self.portfolio.analyze_multiasset_strategy() - # advance scheduler queue to next date self.eventScheduler.advance_date() break diff --git a/EventDriven/event.py b/EventDriven/event.py index 57d5049..4b33684 100644 --- a/EventDriven/event.py +++ b/EventDriven/event.py @@ -1,22 +1,27 @@ -#Event indicate a change in the state of the strategy, market, portfolio or execution system. +# Event indicate a change in the state of the strategy, market, portfolio or execution system. from datetime import datetime from EventDriven.types import EventTypes, SignalTypes -from trade.helpers.helper import parse_option_tick # noqa +from trade.helpers.helper import parse_option_tick # noqa +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.event") + + class Event(object): """ - Event is base class providing an interface for all subsequent - (inherited) events, that will trigger further events in the - trading infrastructure. + Event is base class providing an interface for all subsequent + (inherited) events, that will trigger further events in the + trading infrastructure. """ - pass + pass class MarketEvent(Event): """ - Handles the event of receiving a new market update with + Handles the event of receiving a new market update with corresponding bars. """ @@ -24,19 +29,29 @@ def __init__(self, datetime): """ Initialises the MarketEvent. """ - self.type = 'MARKET' + self.type = "MARKET" self.datetime = datetime - + def __str__(self): return f"MarketEvent date:{self.datetime}" + class SignalEvent(Event): """ Handles the event of sending a Signal from a Strategy object. This is received by a Portfolio object and acted upon. """ - - def __init__(self, symbol, datetime, signal_type: SignalTypes, signal_id: str = None, max_contract_price:int = None, order_settings = None, parent_event: Event = None): + + def __init__( + self, + symbol, + datetime, + signal_type: SignalTypes, + signal_id: str = None, + max_contract_price: int = None, + order_settings=None, + parent_event: Event = None, + ): """ Initialises the SignalEvent. @@ -44,9 +59,9 @@ def __init__(self, symbol, datetime, signal_type: SignalTypes, signal_id: str = symbol - The ticker symbol, e.g. 'GOOG'. datetime - The timestamp at which the signal was generated. signal_type - 'LONG' or 'SHORT'. - signal_id- A unique identifier for the signal + signal_id- A unique identifier for the signal max_contract_price - The maximum price for the contract - order_settings - specifically for Order signals, to specify the kind of contract to generate, + order_settings - specifically for Order signals, to specify the kind of contract to generate, example: {'type': 'naked', 'specifics': [{'direction': 'long', 'rel_strike': .900, @@ -60,8 +75,8 @@ def __init__(self, symbol, datetime, signal_type: SignalTypes, signal_id: str = 'name': 'vertical_spread'} parent_event - The parent event that triggered this signal, if any. """ - - self.type = 'SIGNAL' + + self.type = "SIGNAL" self.symbol = symbol self.datetime = datetime self.signal_type = signal_type @@ -69,10 +84,11 @@ def __init__(self, symbol, datetime, signal_type: SignalTypes, signal_id: str = self.max_contract_price = max_contract_price self.order_settings = order_settings self.parent_event = parent_event - + def __str__(self): return f"SignalEvent type:{self.signal_type}, symbol={self.symbol}, date:{self.datetime}, Order Settings={self.order_settings},Max Contract Price:{self.max_contract_price} , signal_id:{self.signal_id}, parent_event:{self.parent_event}" + class OrderEvent(Event): """ Handles the event of sending an Order to an execution system. @@ -80,7 +96,18 @@ class OrderEvent(Event): quantity and a direction. """ - def __init__(self, symbol, datetime, order_type, direction, cash:int | float = None ,quantity: int |float = None,position = None, signal_id: str = None, parent_event: Event = None): + def __init__( + self, + symbol, + datetime, + order_type, + direction, + cash: int | float = None, + quantity: int | float = None, + position=None, + signal_id: str = None, + parent_event: Event = None, + ): """ Initialises the order type, setting whether it is a Market order ('MKT') or Limit order ('LMT'), has @@ -98,15 +125,15 @@ def __init__(self, symbol, datetime, order_type, direction, cash:int | float = signal_id - A unique identifier for the signal that generated the order parent_event - The parent event that triggered this order, if any. """ - - self.type = 'ORDER' + + self.type = "ORDER" self.datetime = datetime self.symbol = symbol self.order_type = order_type self.cash = cash self.quantity = quantity self.direction = direction - self.position = position #a dict with 'long' and 'short' keys + self.position = position # a dict with 'long' and 'short' keys self.signal_id = signal_id self.parent_event = parent_event @@ -115,7 +142,8 @@ def __str__(self): Outputs the values within the Order. """ return f"OrderEvent type={self.order_type}, symbol={self.symbol}, date:{self.datetime}, cash:{self.cash}, quantity={self.quantity}, direction={self.direction}, position={self.position}, signal_id={self.signal_id}, parent_event={self.parent_event}" - + + class FillEvent(Event): """ Encapsulates the notion of a Filled Order, as returned @@ -124,11 +152,24 @@ class FillEvent(Event): the commission of the trade from the brokerage. """ - def __init__(self, datetime : datetime | str, symbol : str, exchange : str, quantity : int, - direction: str, fill_cost: float, market_value: float = None, commission : float =None, slippage : float = None , position = None, signal_id: str = None, parent_event: Event = None): + def __init__( + self, + datetime: datetime | str, + symbol: str, + exchange: str, + quantity: int, + direction: str, + fill_cost: float, + market_value: float = None, + commission: float = None, + slippage: float = None, + position=None, + signal_id: str = None, + parent_event: Event = None, + ): """ Initialises the FillEvent object. Sets the symbol, exchange, - quantity, direction, cost of fill and an optional + quantity, direction, cost of fill and an optional commission. If commission is not provided, the Fill object will @@ -149,8 +190,8 @@ def __init__(self, datetime : datetime | str, symbol : str, exchange : str, quan position - A dict with 'long' and 'short' keys, just long if position is a naked option parent_event - The parent event that triggered this fill, if any. """ - - self.type = 'FILL' + + self.type = "FILL" self.datetime = datetime self.symbol = symbol self.exchange = exchange @@ -170,27 +211,31 @@ def __init__(self, datetime : datetime | str, symbol : str, exchange : str, quan self.commission = commission def __str__(self): - return f"FillEvent symbol={self.symbol}, date:{self.datetime}, exchange={self.exchange}, quantity={self.quantity}, direction={self.direction}, fill_cost={self.fill_cost}, commission={self.commission}, market_value={self.market_value}, slippage={self.slippage}, position={self.position}, signal_id={self.signal_id}" -class ExerciseEvent(Event): + return f"FillEvent symbol={self.symbol}, date:{self.datetime}, exchange={self.exchange}, quantity={self.quantity}, direction={self.direction}, fill_cost={self.fill_cost}, commission={self.commission}, market_value={self.market_value}, slippage={self.slippage}, position={self.position}, signal_id={self.signal_id}" + + +class ExerciseEvent(Event): """ - Encapsulates the notion of an exercise event, as returned from a brokerage. + Encapsulates the notion of an exercise event, as returned from a brokerage. Stores the quantity of an instrument actually exercised and at what price. In addition, stores the commission of the trade from the brokerage. """ - - def __init__(self, - datetime : datetime | str, - symbol : str, - quantity : int, - entry_date: datetime| str, - spot: float, - long_premiums: dict, - short_premiums:dict, - position = None, - signal_id: str = None, - parent_event: Event = None): + + def __init__( + self, + datetime: datetime | str, + symbol: str, + quantity: int, + entry_date: datetime | str, + spot: float, + long_premiums: dict, + short_premiums: dict, + position=None, + signal_id: str = None, + parent_event: Event = None, + ): """ Initialises the ExerciseEvent object. Sets the symbol, exchange, quantity, direction, cost of fill and an optional commission. - + Parameters: datetime - The bar-resolution when the order was filled. symbol - The instrument which was filled. @@ -198,13 +243,13 @@ def __init__(self, position - A dict with 'long' and 'short' keys, just long if position is a naked option signal_id - A unique identifier for the signal that generated the order entry_date - The date when the position was entered - spot - The current spot price of the underlying asset + spot - The current spot price of the underlying asset long_premiums - A dict of option_id: premium for long options in the position short_premiums - A dict of option_id: premium for short options in the position parent_event - The parent event that triggered this exercise, if any. """ - - self.type = 'EXERCISE' + + self.type = "EXERCISE" self.datetime = datetime self.symbol = symbol self.quantity = quantity @@ -215,20 +260,28 @@ def __init__(self, self.short_premiums = short_premiums self.entry_date = entry_date self.parent_event = parent_event - + def __str__(self): return f"ExerciseEvent symbol={self.symbol}, date:{self.datetime} quantity={self.quantity}, long_premiums={self.long_premiums}, short_premiums={self.short_premiums}, position={self.position}, , entry_date={self.entry_date}, spot={self.spot}, signal_id={self.signal_id}, parent_event={self.parent_event}" - - -class RollEvent(Event): + + +class RollEvent(Event): """ Encapsulates the notion of a roll event, this event simply tells the portfolio to close its current position and open anew position """ - - def __init__(self, datetime: datetime | str, symbol: str, signal_type: SignalTypes, position: dict, signal_id: str = None, parent_event: Event = None): + + def __init__( + self, + datetime: datetime | str, + symbol: str, + signal_type: SignalTypes, + position: dict, + signal_id: str = None, + parent_event: Event = None, + ): """ Initialises the RollEvent object. Sets the symbol, exchange, signal_type, direction, cost of fill and an optional commission. - + Parameters: datetime - The bar-resolution when the order was filled. symbol - The instrument which was filled. @@ -237,20 +290,19 @@ def __init__(self, datetime: datetime | str, symbol: str, signal_type: SignalTyp signal_id - A unique identifier for the signal that generated the order parent_event - The parent event that triggered this roll, if any. """ - - self.type = 'ROLL' + + self.type = "ROLL" self.datetime = datetime self.symbol = symbol self.signal_type = signal_type self.position = position self.signal_id = signal_id self.parent_event = parent_event - + def __str__(self): - return f"RollEvent symbol={self.symbol}, date:{self.datetime}, signal_type={self.signal_type}, position={self.position}, signal_id={self.signal_id}, parent_event={self.parent_event}" - - - + return f"RollEvent symbol={self.symbol}, date:{self.datetime}, signal_type={self.signal_type}, position={self.position}, signal_id={self.signal_id}, parent_event={self.parent_event}" + + def get_event_ancestor(event: Event, target_ancestor_event_type: EventTypes) -> Event | None: """ get if the event has a parent event of the specified type. @@ -261,4 +313,4 @@ def get_event_ancestor(event: Event, target_ancestor_event_type: EventTypes) -> if current_event.type == target_ancestor_event_type: return current_event current_event = current_event.parent_event - return None \ No newline at end of file + return None diff --git a/EventDriven/eventScheduler.py b/EventDriven/eventScheduler.py index 6722b7b..e37eeeb 100644 --- a/EventDriven/eventScheduler.py +++ b/EventDriven/eventScheduler.py @@ -1,4 +1,3 @@ - import pandas as pd from pandas import DatetimeIndex from queue import Queue @@ -9,21 +8,23 @@ from EventDriven.types import SignalTypes from trade.helpers.Logging import setup_logger -logger = setup_logger("OptionSignalEventQueue") +logger = setup_logger("EventDriven.eventScheduler") + + class EventQueue(Queue): """ - A custom queue class that only accepts event types, and enforces the order of events to handle close events before open event of the same signal id on the same day(queue) + A custom queue class that only accepts event types, and enforces the order of events to handle close events before open event of the same signal id on the same day(queue) """ + def __init__(self, maxsize=0): super().__init__(maxsize) self.events_list = [] - + @property def logger(self): global logger return logger - def put(self, item: Event): """Overrides put to ensure only Event objects are added.""" if not isinstance(item, Event): @@ -31,53 +32,74 @@ def put(self, item: Event): super().put(item) self.events_list.append(item) - def get_nowait(self) -> Event: """Overrides get_nowait to ensure only Event objects are consumed.""" item = super().get_nowait() self.events_list.pop(self.events_list.index(item)) conflict_events = self.conflicts(item) if len(conflict_events) > 0: - self.logger.warning(f"Pushing {item} to back of queue because conflicting events were found: {[str(e) for e in conflict_events]}") + self.logger.warning( + f"Pushing {item} to back of queue because conflicting events were found: {[str(e) for e in conflict_events]}" + ) self.put(item) return self.get_nowait() - + return item - - def conflicts(self, event: Event) -> list: + + def conflicts(self, event: Event) -> list: """ List of conflicting events within the same queue. - Cases: + Cases: - SignalEvent to close a name should take precedence over SignalEvent to open the same name, this is to make sure cash and position update before opening. - Before a signalEvent, any orderEvent/FillEvent/ExerciseEvent already in the queue for the same symbol should take precedence. - - Before an OrderEvent or ExerciseEvent, any FillEvent already in the queue for the same symbol,should take precedence. - + - Before an OrderEvent or ExerciseEvent, any FillEvent already in the queue for the same symbol,should take precedence. + OrderEvents & ExerciseEvents go straight to the execution handler, affecting position and cash, so they must take precedence over SignalEvents. FillEvents affect position and cash, so they must take precedence over SignalEvents and OrderEvents and ExerciseEvents. """ - + if not isinstance(event, Event): raise ValueError("Queue can only check for Event objects. Received: {}".format(type(event))) - + events_list_copy = self.events_list.copy() if event in events_list_copy: events_list_copy.remove(event) - if isinstance(event, SignalEvent): if event.signal_type != SignalTypes.CLOSE.value: - return [e for e in events_list_copy if e.symbol == event.symbol and (isinstance(e, SignalEvent) and e.signal_type == SignalTypes.CLOSE.value or isinstance(e, OrderEvent) or isinstance(e, FillEvent) or isinstance(e, ExerciseEvent))] + return [ + e + for e in events_list_copy + if e.symbol == event.symbol + and ( + isinstance(e, SignalEvent) + and e.signal_type == SignalTypes.CLOSE.value + or isinstance(e, OrderEvent) + or isinstance(e, FillEvent) + or isinstance(e, ExerciseEvent) + ) + ] else: - return [e for e in events_list_copy if e.symbol == event.symbol and (isinstance(e, OrderEvent) or isinstance(e,FillEvent) or isinstance(e, ExerciseEvent))] - + return [ + e + for e in events_list_copy + if e.symbol == event.symbol + and (isinstance(e, OrderEvent) or isinstance(e, FillEvent) or isinstance(e, ExerciseEvent)) + ] + elif isinstance(event, OrderEvent) or isinstance(event, ExerciseEvent): return [e for e in events_list_copy if e.symbol == event.symbol and (isinstance(e, FillEvent))] - + return [] + + def empty(self) -> bool: + """Checks if the queue is empty.""" + return super().empty() + class EventScheduler: - def __init__(self, start_date, end_date ): + def __init__(self, start_date, end_date): """ Initializes the event scheduler with a range of dates.sss start_date: can be date or date string format @@ -88,27 +110,24 @@ def __init__(self, start_date, end_date ): self.end_date = pd.to_datetime(end_date) self.market_dates = pd.bdate_range(start=self.start_date, end=self.end_date) self.market_dates = self.market_dates[~self.market_dates.isin(HOLIDAY_SET)] - self.events_map: Dict[str, Queue] = { - self.clean_date(d): EventQueue(maxsize=0) for d in self.market_dates - } + self.events_map: Dict[str, Queue] = {self.clean_date(d): EventQueue(maxsize=0) for d in self.market_dates} self.current_date = self.clean_date(self.start_date) self.events_dict = [] - self.logger = setup_logger('OptionSignalEventScheduler') - - + self.logger = setup_logger("EventDriven.eventScheduler") + @property def events(self): - return pd.DataFrame(self.events_dict).set_index('datetime').sort_index() - + return pd.DataFrame(self.events_dict).set_index("datetime").sort_index() + def get_queue(self, date) -> Optional[Queue]: """Returns the queue for a specific date.""" return self.events_map[self.clean_date(date)] - + def get_current_queue(self) -> Queue | None: """Returns the queue for the current date.""" return self.events_map.get(self.current_date, None) - def empty(self, date = None) -> bool: + def empty(self, date=None) -> bool: """Checks if the current day's queue has any events.""" if date is not None: return self.get_queue(date).empty() @@ -122,8 +141,8 @@ def get_next_nonempty_queue(self) -> Optional[Queue]: """Returns the next non-empty queue, if available.""" for date, queue in self.events_map.items(): if not queue.empty(): - return (date,queue) - + return (date, queue) + def advance_date(self, date=None) -> Optional[DatetimeIndex | None]: """Moves to the next available date in the sequence, skipping over missing ones.""" try: @@ -131,7 +150,11 @@ def advance_date(self, date=None) -> Optional[DatetimeIndex | None]: next_date = self.clean_date(date + pd.offsets.BDay(1)) else: current_date_idx = list(self.events_map.keys()).index(self.current_date) - next_date = list(self.events_map.keys())[current_date_idx + 1] if current_date_idx + 1 < len(self.events_map) else None + next_date = ( + list(self.events_map.keys())[current_date_idx + 1] + if current_date_idx + 1 < len(self.events_map) + else None + ) if next_date: self.current_date = next_date @@ -140,12 +163,12 @@ def advance_date(self, date=None) -> Optional[DatetimeIndex | None]: else: self.current_date = None self.logger.info("No more dates left.") - return None + return None except (ValueError, IndexError): self.logger.error("Error advancing date. Possibly out of range.") return None - + def put(self, event: Event): """Adds an event to the queue of the current date.""" self.get_current_queue().put(event) @@ -157,8 +180,8 @@ def schedule_event(self, event_date, event: Event): Schedules an event for a specific future date. date: can be any date string format Event: Event object to be scheduled - - return boolean on successful + + return boolean on successful """ event_date = pd.to_datetime(event_date) event_date_str = self.clean_date(event_date) @@ -166,29 +189,24 @@ def schedule_event(self, event_date, event: Event): if event_date < pd.to_datetime(current_date): self.logger.error(f"Cannot schedule event to past date {event}.") return False - + if event_date_str not in self.events_map: self.logger.error(f"Event date {event_date_str} not found in backtest range") return False - + self.logger.info(f"Scheduling event for {event_date_str} queue: {event}") self.events_map[event_date_str].put(event) self.store_event(event) return True - - + def clean_date(self, date): """ Cleans date string to ensure it is in the correct format """ - return pd.to_datetime(date).strftime('%Y%m%d') - + return pd.to_datetime(date).strftime("%Y%m%d") + def store_event(self, event: Event): """ Stores an event in the events dictionary. """ self.events_dict.append(event.__dict__) - - - - \ No newline at end of file diff --git a/EventDriven/execution.py b/EventDriven/execution.py index 393d76b..7dcfe45 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -54,7 +54,7 @@ class SimulatedExecutionHandler(ExecutionHandler): handler. """ - def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : float = 0.00279): + def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : float = 0.0065): """ Initialises the handler, setting the event queues up internally. @@ -62,7 +62,9 @@ def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : f Parameters: events - The Queue of Event objects. max_slippage_pct - The slippage range for the market default is 0.002 - commission_rate - The commission rate for the market default is 0.00279 per contract: https://robinhood.com/us/en/support/articles/trading-fees-on-robinhood/#Tradingactivityfee + commission_rate - The commission rate for the market default is 35 cents per contract. + Option price is in contract units, so commission is also in contract units. For example, if commission_rate is 0.0035, then scaled commission for 1 contract is 0.0035 * 100 = $0.35, + """ self.events = events self.max_slippage_pct = max_slippage_pct @@ -102,6 +104,7 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): elif order_event.direction == 'SELL': ## We want to decrease the price for sells by slippage slippage_pct = np.random.uniform(-self.max_slippage_pct, -self.min_slippage_pct) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct + #slippage may increase or decrease intended price price = order_event.position['close'] * (1 + slippage_pct) @@ -153,7 +156,20 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): # - Market value != fill cost, as market value is based on the position's close price, not the slippage adjusted price. slippage_diff = (price - order_event.position['close'] ) * quantity - fill_event = FillEvent(order_event.datetime, order_event.symbol, 'ARCA', quantity, order_event.direction, fill_cost=fill_cost, market_value=market_value, commission=commission, position=order_event.position, slippage=slippage_diff, signal_id=order_event.signal_id, parent_event=order_event) + fill_event = FillEvent( + order_event.datetime, + order_event.symbol, + 'ARCA', + quantity, + order_event.direction, + fill_cost=fill_cost, + market_value=market_value, + commission=commission, + position=order_event.position, + slippage=slippage_diff, + signal_id=order_event.signal_id, + parent_event=order_event + ) exec_cache['fill'][f'{order_event.signal_id}_{order_event.datetime.strftime("%Y-%m-%d")}_{order_event.direction}'] = deepcopy(fill_event) self.events.put(fill_event) @@ -181,7 +197,18 @@ def execute_exercise(self, exercise_event: ExerciseEvent): total_pnl = long_pnl + short_pnl market_value = exercise_event.spot * exercise_event.quantity - fill_event = FillEvent(exercise_event.datetime, exercise_event.symbol, 'ARCA', exercise_event.quantity, 'EXERCISE', fill_cost=total_pnl, position=exercise_event.position,market_value=market_value, signal_id=exercise_event.signal_id, parent_event=exercise_event) + fill_event = FillEvent( + exercise_event.datetime, + exercise_event.symbol, + 'ARCA', + exercise_event.quantity, + 'EXERCISE', + fill_cost=total_pnl, + position=exercise_event.position, + market_value=market_value, + signal_id=exercise_event.signal_id, + parent_event=exercise_event + ) self.events.put(fill_event) def __calculate_premium_pnl(self, option_meta, spot, premium): diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index e55de06..627b49d 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -12,12 +12,12 @@ from EventDriven.dataclasses.orders import OrderRequest from EventDriven.eventScheduler import EventScheduler from EventDriven.trade import Trade -from EventDriven.types import EventTypes, FillDirection, ResultsEnum, SignalTypes, OrderData +from EventDriven.types import EventTypes, FillDirection, ResultsEnum, SignalTypes, OrderData, Order from EventDriven.riskmanager.new_base import RiskManager, order_failed from EventDriven.riskmanager.utils import parse_position_id from trade.helpers.Logging import setup_logger from trade.helpers.helper import to_datetime -from trade.assets import Stock +from trade.assets.Stock import Stock from EventDriven.event import ( ExerciseEvent, # noqa FillEvent, @@ -38,10 +38,10 @@ from EventDriven.dataclasses.states import StrategyChangeMeta from EventDriven.configs.core import PortfolioManagerConfig, CashAllocatorConfig from EventDriven.portfolio_utils import extract_events -from EventDriven.exceptions import BacktestNotImplementedError +from EventDriven.exceptions import BacktestNotImplementedError, EVBacktestError from trade.backtester_._multi_asset_strategy import MultiAssetStrategy -LOGGER = setup_logger("OptionSignalPortfolio", stream_log_level=logging.INFO) +LOGGER = setup_logger("EventDriven.new_portfolio", stream_log_level=logging.WARNING) class Portfolio(AggregatorParent): @@ -238,6 +238,7 @@ def __construct_weight_map(self, weight_map): def __construct_max_contract_price(self): # Try config-driven buckets first; fallback to legacy behavior + ## TODO: Decommision this method. Rely solely on cash allocator config for max contract price. if self.cash_allocator_config is not None: try: max_cash_map = self.cash_allocator_config.build_max_cash_map( @@ -294,6 +295,7 @@ def _equity(self): equity_curve["commission"] = -equity_curve["commission"] equity_curve["total"] = equity_curve.sum(axis=1) ##NOTE: Temp fix till calcs work equity_curve.rename(columns={"total": "Total"}, inplace=True) + equity_curve.index = pd.to_datetime(equity_curve.index) self.__equity = equity_curve return self.__equity @@ -333,12 +335,10 @@ def dates_(self, start: bool = True): def buyNhold(self): stock_ts = pd.DataFrame() for stock in self.symbol_list: - stock_ts[stock] = ( - self.underlier_list_data.get(stock, self.__get_underlier_data(stock)).spot( - ts=True, ts_start=self.dates_(), ts_end=self.dates_(start=False) - )["close"] - * self.__weight_map[stock] - ) + data = self.risk_manager.market_data.market_timeseries.get_timeseries( + sym=stock, factor="spot", start_date=self.start_date, end_date=self.eventScheduler.end_date + ).spot["close"].rename(stock) + stock_ts = pd.concat([stock_ts, data], axis=1) stock_ts["Total"] = stock_ts.sum(axis=1) self.stock_equity = stock_ts @@ -452,11 +452,30 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type date_str = signal_event.datetime.strftime("%Y-%m-%d") position_type = "c" if signal_event.signal_type == "LONG" else "p" cash_at_hand = self.__normalize_dollar_amount_to_decimal(self.allocated_cash_map[signal_event.symbol] * 1) + + ## TODO: Decommision self.__max_contract_price and rely solely on cash allocator config for max contract price. + # max_contract_price = ( + # self.__max_contract_price[signal_event.symbol] + # if signal_event.max_contract_price is None + # else signal_event.max_contract_price + # ) + + ## Determine max contract price based on cash allocator config or default to 50% of allocated cash + max_contract_dict = ( + self.cash_allocator_config.build_max_cash_map( + weights=self.__weight_map, cash=self.initial_capital + ) + if self.cash_allocator_config is not None + else {} + ) + + ## If max_contract_price is not set or is greater than cash at hand, use cash at hand as max contract price max_contract_price = ( - self.__max_contract_price[signal_event.symbol] + max_contract_dict.get(signal_event.symbol) if signal_event.max_contract_price is None else signal_event.max_contract_price ) + max_contract_price = max_contract_price if max_contract_price <= cash_at_hand else cash_at_hand if self.logger.isEnabledFor(logging.DEBUG): self.logger.info( @@ -486,9 +505,10 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type self.logger.warning(f"Order failed to be created for signal: {signal_event}, Order: {order}") self.position_cache.pop(cache_key, None) self.position_cache.pop(signal_event.signal_id, None) - self._process_failed_order(signal_event) + self._process_failed_order(signal_event, order_result=order) return None + return OrderEvent( signal_event.symbol, signal_event.datetime, @@ -501,21 +521,28 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type parent_event=signal_event, ) - def _process_failed_order(self, signal_event: SignalEvent): + def _process_failed_order(self, signal_event: SignalEvent, order_result:Order=None): """ Process a failed order by either rolling it forward or logging it. """ + reason = "Order failed" if self.config.roll_failed_orders: next_trading_day = signal_event.datetime + pd.offsets.BusinessDay(1) self.logger.critical(f"Rolling failed signal {signal_event} to next trading day {next_trading_day}") self._roll_signal_to_next_day(signal_event) + reason += " and rolling forward as per config" else: self.logger.critical( f"Failed to process signal for {signal_event.signal_id} on {signal_event.datetime}, not rolling forward as per config." ) - unprocess_dict = signal_event.__dict__ - unprocess_dict["reason"] = "Order failed and rolling is disabled" - self.unprocessed_signals.append(unprocess_dict) + + ## Log unprocessed signal for analysis + unprocess_dict = signal_event.__dict__ + unprocess_dict["reason"] = reason + unprocess_dict["result_message"] = "Order failed" + if order_result is not None: + unprocess_dict["order_result"] = order_result + self.unprocessed_signals.append(unprocess_dict) def _roll_signal_to_next_day(self, signal_event: SignalEvent): """ @@ -607,6 +634,11 @@ def analyze_multiasset_strategy(self, dt: Optional[pd.Timestamp] = None): """ Analyze the multi-asset strategy and generate signals if necessary """ + + if self.eq_strategy.tplusn != self.t_plus_n: + raise EVBacktestError( + f"tplusn value of the equity strategy does not match the backtest configuration. eq_strategy.tplusn: {self.eq_strategy.tplusn}, config.t_plus_n: {self.t_plus_n}" + ) if self.eq_strategy is None: return dt = to_datetime(dt or self.eventScheduler.current_date) @@ -621,6 +653,10 @@ def analyze_multiasset_strategy(self, dt: Optional[pd.Timestamp] = None): ) opens = signals.open_signals closes = signals.close_signals + if not opens: + self.logger.info(f"No open signals generated for multi-asset strategy on {dt}") + if not closes: + self.logger.info(f"No close signals generated for multi-asset strategy on {dt}") for ticker, signal in opens.items(): if signal.signal_id is None: raise ValueError( @@ -1111,3 +1147,9 @@ def plot_portfolio( tr["Size"] = tr["Quantity"] return plot_portfolio(tr, eq, dd, _bnch, plot_bnchmk=plot_bnchmk, return_plot=return_plot, **kwargs) + + def get_strategy_class(self) -> Optional[MultiAssetStrategy]: + """ + Returns the multi-asset strategy class if it exists, else returns None + """ + return self.eq_strategy.strategy_class if self.eq_strategy is not None else None \ No newline at end of file diff --git a/EventDriven/trade.py b/EventDriven/trade.py index 289697b..b7ee8cb 100644 --- a/EventDriven/trade.py +++ b/EventDriven/trade.py @@ -1,69 +1,73 @@ """ This module defines the Trade class for tracking buy and sell transactions for a trade. """ + import pandas as pd from EventDriven.tradeLedger import TradeLedger from EventDriven.event import FillEvent +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.trade") + class Trade: """ Class to track buy and sell transactions for a specific trade. Has separate ledgers for buy and sell events. """ - - def __init__(self, trade_id:str, symbol:str, signa_id: str = None): + + def __init__(self, trade_id: str, symbol: str, signa_id: str = None): self.trade_id = trade_id - self.symbol= symbol - self.buy_ledger= TradeLedger(f"{trade_id}_buy") + self.symbol = symbol + self.buy_ledger = TradeLedger(f"{trade_id}_buy") self.sell_ledger = TradeLedger(f"{trade_id}_sell") self.entry_date = None self.exit_date = None self.current_price = None self.stats = None self.signal_id = signa_id - + def __getitem__(self, key): """ Allows access to the stats dictionary using the key. """ if self.stats is not None and key in self.stats.columns: return self.stats[key].iloc[0] - elif key in ['trade_id', 'symbol', 'entry_date', 'exit_date', 'current_price']: + elif key in ["trade_id", "symbol", "entry_date", "exit_date", "current_price"]: return getattr(self, key) else: raise KeyError(f"Key '{key}' not found in stats.") - + def __setitem__(self, key, value): """ Allows setting values in the stats dictionary using the key. """ - if key in ['trade_id', 'symbol', 'entry_date', 'exit_date', 'current_price']: + if key in ["trade_id", "symbol", "entry_date", "exit_date", "current_price"]: setattr(self, key, value) else: raise KeyError(f"Key '{key}' not found in stats.") - - + def update(self, fill_event: FillEvent): """ Update the appropriate ledger based on the fill event direction """ - if fill_event.direction == 'BUY': + if fill_event.direction == "BUY": self.buy_ledger.add_entry(fill_event) if self.entry_date is None: self.entry_date = fill_event.datetime - - elif fill_event.direction in ['SELL', 'EXERCISE']: + + elif fill_event.direction in ["SELL", "EXERCISE"]: self.sell_ledger.add_entry(fill_event) # Update exit date if position is fully closed if self.is_closed(): self.exit_date = fill_event.datetime - + self.stats = pd.DataFrame([self.aggregate()]) def _update_kw(self, **entry_kwargs): """ Update the appropriate ledger using keyword args compatible with TradeLedger._add_entry_kw. - + Expected keys: - entry_time: datetime of the fill (required) - direction: 'BUY'/'SELL'/'EXERCISE' or aliases 'LONG'/'SHORT' (required) @@ -77,137 +81,137 @@ def _update_kw(self, **entry_kwargs): - signal_id: overrides the Trade.signal_id (optional, defaults to this trade) - normalize: whether to normalize monetary values before storing (default True) """ - direction = entry_kwargs.get('direction') - if direction.upper() not in ['BUY', 'SELL', 'EXERCISE', 'LONG', 'SHORT']: + direction = entry_kwargs.get("direction") + if direction.upper() not in ["BUY", "SELL", "EXERCISE", "LONG", "SHORT"]: raise ValueError("Keyword updates must include direction of BUY, SELL, or EXERCISE.") - if direction.upper() in ['LONG', 'SHORT']: - direction = 'BUY' if direction.upper() == 'LONG' else 'SELL' - entry_time = entry_kwargs.get('entry_time') + if direction.upper() in ["LONG", "SHORT"]: + direction = "BUY" if direction.upper() == "LONG" else "SELL" + entry_time = entry_kwargs.get("entry_time") if entry_time is None: raise ValueError("Keyword updates must include entry_time.") - entry_kwargs.setdefault('trade_id', self.trade_id) - entry_kwargs.setdefault('signal_id', self.signal_id) + entry_kwargs.setdefault("trade_id", self.trade_id) + entry_kwargs.setdefault("signal_id", self.signal_id) - if direction == 'BUY': + if direction == "BUY": self.buy_ledger._add_entry_kw(**entry_kwargs) if self.entry_date is None: self.entry_date = entry_time - elif direction in ['SELL', 'EXERCISE']: + elif direction in ["SELL", "EXERCISE"]: self.sell_ledger._add_entry_kw(**entry_kwargs) if self.is_closed(): self.exit_date = entry_time self.stats = pd.DataFrame([self.aggregate()]) - - + def is_closed(self): """ Check if the trade is closed (buy quantity - sell quantity = 0) """ return self.buy_ledger.quantity - self.sell_ledger.quantity == 0 - + def get_position_size(self): """ Get the current position size (buy - sell) """ return self.buy_ledger.quantity - self.sell_ledger.quantity - + def aggregate(self): """ Generate aggregated statistics for the trade with standardized key naming that matches the trades_data objects. """ stats = {} - stats['TradeID'] = self.trade_id - stats['SignalID'] = self.signal_id - stats['Ticker'] = self.symbol - stats['EntryTime'] = self.entry_date - stats['ExitTime'] = self.exit_date - + stats["TradeID"] = self.trade_id + stats["SignalID"] = self.signal_id + stats["Ticker"] = self.symbol + stats["EntryTime"] = self.entry_date + stats["ExitTime"] = self.exit_date + # Calculate metrics for buy transactions - stats['EntryPrice'] = self.buy_ledger.avg_price - stats['EntryCommission'] = self.buy_ledger.commission - stats['EntrySlippage'] = self.buy_ledger.slippage - stats['EntryQuantity'] = self.buy_ledger.quantity - stats['EntryAuxilaryCost'] = self.buy_ledger.aux_cost - stats['TotalEntryCost'] = self.buy_ledger.avg_total_cost - + stats["EntryPrice"] = self.buy_ledger.avg_price + stats["EntryCommission"] = self.buy_ledger.commission + stats["EntrySlippage"] = self.buy_ledger.slippage + stats["EntryQuantity"] = self.buy_ledger.quantity + stats["EntryAuxilaryCost"] = self.buy_ledger.aux_cost + stats["TotalEntryCost"] = self.buy_ledger.avg_total_cost + # Calculate metrics for sell transactions - stats['ExitPrice'] = self.sell_ledger.avg_price - stats['ExitCommission'] = self.sell_ledger.commission - stats['ExitSlippage'] = self.sell_ledger.slippage - stats['ExitQuantity'] = self.sell_ledger.quantity - stats['ExitAuxilaryCost'] = self.sell_ledger.aux_cost - stats['TotalExitCost'] = self.sell_ledger.avg_total_cost - - stats['Quantity'] = stats['ExitQuantity'] + stats["ExitPrice"] = self.sell_ledger.avg_price + stats["ExitCommission"] = self.sell_ledger.commission + stats["ExitSlippage"] = self.sell_ledger.slippage + stats["ExitQuantity"] = self.sell_ledger.quantity + stats["ExitAuxilaryCost"] = self.sell_ledger.aux_cost + stats["TotalExitCost"] = self.sell_ledger.avg_total_cost + + stats["Quantity"] = stats["ExitQuantity"] # Calculate PnL metrics if we have both buy and sell transactions - if stats['EntryQuantity'] > 0 and stats['ExitQuantity'] > 0: + if stats["EntryQuantity"] > 0 and stats["ExitQuantity"] > 0: # Calculate realized PnL for closed portion - stats['ClosedQuantity'] = stats['ExitQuantity'] - stats['ClosedPnL'] = (stats['ExitPrice'] - stats['EntryPrice']) * stats['ExitQuantity'] - + stats["ClosedQuantity"] = stats["ExitQuantity"] + stats["ClosedPnL"] = (stats["ExitPrice"] - stats["EntryPrice"]) * stats["ExitQuantity"] + # Calculate commission and slippage impact - stats['TotalCommission'] = stats['EntryCommission'] + stats['ExitCommission'] - stats['TotalSlippage'] = stats['EntrySlippage'] + stats['ExitSlippage'] - stats['TotalAuxilaryCost'] = -abs(stats['TotalCommission']) - abs(stats['TotalSlippage']) - + stats["TotalCommission"] = stats["EntryCommission"] + stats["ExitCommission"] + stats["TotalSlippage"] = stats["EntrySlippage"] + stats["ExitSlippage"] + stats["TotalAuxilaryCost"] = -abs(stats["TotalCommission"]) - abs(stats["TotalSlippage"]) + # Calculate unrealized PnL for open position - open_quantity = stats['EntryQuantity'] - stats['ExitQuantity'] - stats['OpenQuantity'] = open_quantity - + open_quantity = stats["EntryQuantity"] - stats["ExitQuantity"] + stats["OpenQuantity"] = open_quantity + if open_quantity > 0 and self.current_price is not None: - stats['UnrealizedPnL'] = (self.current_price - stats['EntryPrice']) * open_quantity + stats["UnrealizedPnL"] = (self.current_price - stats["EntryPrice"]) * open_quantity else: - stats['UnrealizedPnL'] = 0 - + stats["UnrealizedPnL"] = 0 + # Calculate total PnL ## Entry Price already includes commission and slippage costs - stats['PnL'] = stats['ClosedPnL'] + stats['UnrealizedPnL'] #- abs(stats['TotalSlippage']) - abs(stats['TotalCommission']) - + stats["PnL"] = ( + stats["ClosedPnL"] + stats["UnrealizedPnL"] + ) # - abs(stats['TotalSlippage']) - abs(stats['TotalCommission']) + # Calculate return percentage - if stats['TotalEntryCost'] > 0: - stats['ReturnPct'] = (stats['PnL'] / stats['TotalEntryCost']) + if stats["TotalEntryCost"] > 0: + stats["ReturnPct"] = stats["PnL"] / stats["TotalEntryCost"] else: - stats['ReturnPct'] = 0 - + stats["ReturnPct"] = 0 + # Calculate duration in days if trade is closed if self.is_closed() and self.exit_date and self.entry_date: - stats['Duration'] = (self.exit_date - self.entry_date).days + stats["Duration"] = (self.exit_date - self.entry_date).days else: - stats['Duration'] = None - - return stats + stats["Duration"] = None + return stats def update_current_price(self, price): """ Update the current market price for calculating unrealized PnL """ self.current_price = price - + def entries(self): """ Return a combined dataframe of buy and sell transactions """ buy_df = self.buy_ledger.ledger_df sell_df = self.sell_ledger.ledger_df - + combined_df = pd.DataFrame() - + if buy_df is not None and not buy_df.empty: buy_df = buy_df.copy() - buy_df['transaction_type'] = 'BUY' + buy_df["transaction_type"] = "BUY" combined_df = pd.concat([combined_df, buy_df], ignore_index=True) - + if sell_df is not None and not sell_df.empty: sell_df = sell_df.copy() - sell_df['transaction_type'] = 'SELL' + sell_df["transaction_type"] = "SELL" combined_df = pd.concat([combined_df, sell_df], ignore_index=True) - + if not combined_df.empty: # Sort by datetime - combined_df.sort_values('datetime', inplace=True) - + combined_df.sort_values("datetime", inplace=True) + return combined_df diff --git a/EventDriven/tradeLedger.py b/EventDriven/tradeLedger.py index ceb8100..07367e2 100644 --- a/EventDriven/tradeLedger.py +++ b/EventDriven/tradeLedger.py @@ -1,16 +1,21 @@ """ This module serves as a ledger for a trade, containing critical information on the trade """ -import pandas as pd -from EventDriven.helpers import normalize_dollar_amount_to_decimal, normalize_dollar_amount +from typing import List +import pandas as pd +from EventDriven.helpers import normalize_dollar_amount_to_decimal, normalize_dollar_amount ## noqa from EventDriven.event import FillEvent +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.tradeLedger") + class TradeLedger: """ This class serves as a ledger for a trade, containing critical information on the trade. Stores fill event data with datetime as keys. - + Attributes: id (str): Unique identifier for the trade ledger. avg_price (float): The average price of all trades. This is an average value. @@ -23,25 +28,24 @@ class TradeLedger: avg_total_cost (float): The average total cost of all trades. This is an average value. aux_cost (float): The total auxiliary costs (commission + slippage) incurred. This is a running sum. """ - - def __init__(self, id:str) -> None: + + def __init__(self, id: str) -> None: self.id = id self.avg_price = 0.0 self.quantity = 0 - self.commission= 0.0 - self.slippage= 0.0 + self.commission = 0.0 + self.slippage = 0.0 self.ledger = [] self.ledger_df = None self.market_value = 0.0 self.avg_total_cost = 0.0 self.aux_cost = 0.0 - - + def add_entry(self, fill_event: FillEvent): """ Adds an entry to the ledger using datetime as key """ - trade_id = fill_event.position['trade_id'] + trade_id = fill_event.position["trade_id"] self._add_entry_common( entry_time=fill_event.datetime, trade_id=trade_id, @@ -103,43 +107,77 @@ def _add_entry_common( direction: str, normalize: bool, ): - uid = f'{trade_id}_{signal_id}_{entry_time}' + """Common logic for adding an entry to the ledger, used by both add_entry and _add_entry_kw + + Everything is scaled to dollar amounts and by quantity.""" + + uid = f"{trade_id}_{signal_id}_{entry_time}" # Normalize monetary fields unless explicitly disabled price_val = normalize_dollar_amount(fill_cost / quantity) if normalize else fill_cost / quantity - commission_val = 0.0 if direction == 'EXERCISE' else (normalize_dollar_amount(commission) if normalize else commission) + + # Commission fill event doesn't scale to dollar amounts. We scale it here + commission_val = (0.0 if direction == "EXERCISE" else normalize_dollar_amount(commission) if normalize else commission) market_value_val = normalize_dollar_amount(market_value) if normalize else market_value - slippage_val = 0.0 if direction == 'EXERCISE' else (normalize_dollar_amount(slippage) if normalize else slippage) - per_unit_slippage_val = 0.0 if direction == 'EXERCISE' else ( - normalize_dollar_amount_to_decimal(slippage / quantity) if normalize else slippage / quantity + slippage_val = ( + 0.0 if direction == "EXERCISE" else (normalize_dollar_amount(slippage) if normalize else slippage) ) + + per_unit_slippage_val = 0.0 if direction == "EXERCISE" else slippage_val / quantity total_cost_val = normalize_dollar_amount(fill_cost) if normalize else fill_cost - aux_cost_val = 0.0 if direction == 'EXERCISE' else ( - normalize_dollar_amount(abs(commission) + abs(slippage)) if normalize else abs(commission) + abs(slippage) + aux_cost_val = ( + 0.0 + if direction == "EXERCISE" + else ( + normalize_dollar_amount(abs(commission) + abs(slippage)) + if normalize + else abs(commission) + abs(slippage) + ) ) entry = { - 'datetime': entry_time, - 'uid': uid, - 'price': price_val, - 'quantity': quantity, - 'symbol': symbol, - 'commission': commission_val, - 'market_value': market_value_val, - 'slippage': slippage_val, - 'per_unit_slippage': per_unit_slippage_val, - 'total_cost': total_cost_val, - 'aux_cost': aux_cost_val, - 'direction': direction + "datetime": entry_time, + "uid": uid, + "price": price_val, + "quantity": quantity, + "symbol": symbol, + "commission": commission_val, + "market_value": market_value_val, + "slippage": abs(slippage_val), + "per_unit_slippage": per_unit_slippage_val, + "per_unit_commission": 0.0 if direction == "EXERCISE" else (commission_val / quantity if quantity != 0 else 0.0), + "per_unit_market_value": market_value_val / quantity if quantity != 0 else 0.0, + "total_cost": total_cost_val, + "aux_cost": aux_cost_val, + "direction": direction, } - self.avg_price = ((self.avg_price * self.quantity) + - (entry['price'] * quantity)) / (self.quantity + quantity) - self.avg_total_cost = ((self.avg_total_cost * self.quantity) + - (entry['total_cost'] * quantity)) / (self.quantity + quantity) - self.aux_cost += entry['aux_cost'] - self.quantity += entry['quantity'] - self.commission += entry['commission'] - self.slippage += entry['slippage'] - self.market_value += entry['market_value'] + self.avg_price = ((self.avg_price * self.quantity) + (entry["price"] * quantity)) / (self.quantity + quantity) + self.avg_total_cost = ((self.avg_total_cost * self.quantity) + (entry["total_cost"] * quantity)) / ( + self.quantity + quantity + ) + self.aux_cost += entry["aux_cost"] + self.quantity += entry["quantity"] + self.commission += entry["commission"] + self.slippage += entry["slippage"] + self.market_value += entry["market_value"] self.ledger.append(entry) self.ledger_df = pd.DataFrame(self.ledger) + + def get_ledger(self) -> List[dict]: + """ + Return the ledger as a list of entries. Each entry is a dictionary containing details of a trade fill. + Dictionary keys include: + - datetime: The timestamp of the fill event. + - uid: A unique identifier for the fill, typically a combination of trade_id, signal_id, and entry_time. + - price: The price per unit for the fill. + - quantity: The quantity filled in the event. + - symbol: The ticker symbol of the asset traded. + - commission: The commission incurred for the fill. This is total commission for the fill, not per unit. + - market_value: The total market value of the fill excluding slippage and commission. + - slippage: The total slippage incurred for the fill. This is total slippage for the fill, not per unit. + - per_unit_slippage: The slippage incurred per unit for the fill. + - total_cost: The total cost of the fill including slippage and commission. This is total cost for the fill, not per unit. + - aux_cost: The total auxiliary cost of the fill, which is the sum of absolute commission and absolute slippage. This is total auxiliary cost for the fill, not per unit. + - direction: The direction of the fill, either 'BUY', 'SELL', or 'EXERCISE'. + """ + return self.ledger From df2ca427049bfc843adb9dc01dee51b026507097 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:29:23 -0400 Subject: [PATCH 05/81] Migrate rates and time-to-expiry handling to DataManager v2 --- EventDriven/_vars.py | 2 +- EventDriven/riskmanager/market_data.py | 964 ------------------- EventDriven/riskmanager/market_timeseries.py | 62 +- EventDriven/riskmanager/utils.py | 46 +- module_test/raw_code/DataManagers/utils.py | 358 +++---- trade/__init__.py | 8 +- trade/assets/OptionChain.py | 173 ++-- trade/assets/Stock.py | 6 +- trade/assets/helpers/DataManagers.py | 1 - trade/assets/rates.py | 37 +- trade/datamanager/base.py | 5 + trade/datamanager/greeks.py | 19 +- trade/datamanager/loaders.py | 2 +- trade/datamanager/market_data.py | 37 +- trade/datamanager/result.py | 3 +- trade/datamanager/timeseries.py | 7 +- trade/datamanager/utils/cache.py | 9 +- trade/datamanager/utils/date.py | 46 + trade/datamanager/utils/logging.py | 7 + trade/datamanager/vars.py | 2 - trade/datamanager/vol.py | 17 +- trade/helpers/helper.py | 17 +- trade/optionlib/config/defaults.py | 3 +- trade/optionlib/pricing/binomial.py | 7 +- trade/optionlib/vol/ssvi/utils.py | 4 +- 25 files changed, 500 insertions(+), 1342 deletions(-) delete mode 100644 EventDriven/riskmanager/market_data.py diff --git a/EventDriven/_vars.py b/EventDriven/_vars.py index f0db389..af62f52 100644 --- a/EventDriven/_vars.py +++ b/EventDriven/_vars.py @@ -8,6 +8,7 @@ import yaml from trade.helpers.helper import CustomCache from trade.helpers.Logging import setup_logger +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE logger = setup_logger('EventDriven._vars') CONTRACT_MULTIPLIER = 100 @@ -32,7 +33,6 @@ def add_columns(series: pd.Series, col_to_add: int, factory=ADD_COLUMNS_FACTORY) HOME_BASE = Path(os.environ["WORK_DIR"]) / ".cache" BASE.mkdir(exist_ok=True) -OPTION_TIMESERIES_START_DATE: str|datetime = '2017-01-01' Y1_LAGGED_START_DATE: str|datetime = (pd.to_datetime(OPTION_TIMESERIES_START_DATE) - relativedelta(years=1)).strftime('%Y-%m-%d') Y2_LAGGED_START_DATE: str|datetime = (pd.to_datetime(OPTION_TIMESERIES_START_DATE) - relativedelta(years=2)).strftime('%Y-%m-%d') Y3_LAGGED_START_DATE: str|datetime = (pd.to_datetime(OPTION_TIMESERIES_START_DATE) - relativedelta(years=3)).strftime('%Y-%m-%d') diff --git a/EventDriven/riskmanager/market_data.py b/EventDriven/riskmanager/market_data.py deleted file mode 100644 index b0eb2f4..0000000 --- a/EventDriven/riskmanager/market_data.py +++ /dev/null @@ -1,964 +0,0 @@ -"""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 -""" - -from datetime import datetime, timedelta -from dataclasses import dataclass, field -from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple -import pandas as pd -from pandas.tseries.offsets import BDay -from openbb import obb -from dbase.DataAPI.ThetaData import resample -from trade.helpers.helper import retrieve_timeseries, ny_now, CustomCache, get_missing_dates, YFinanceEmptyData -from trade.helpers.decorators import timeit -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 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 - -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) -_SANITIZED_ON_EXIT: bool = False - - -@dataclass -class AtIndexResult: - """Dataclass to hold the result of retrieving market data at a specific index (date).""" - - 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: - """Class to hold timeseries data for a specific symbol.""" - - 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: - """Class to manage market timeseries data for equities.""" - - 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 - - def __post_init__(self): - register_signal(signum=15, signal_func=self._on_exit_sanitize) - register_signal(signum=0, signal_func=self._on_exit_sanitize) - - @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): - """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): - return isinstance(d, pd.DataFrame) or isinstance(d, pd.Series) - - for sym in self._spot.keys(): - d = self._spot[sym] - if not _check_instance(d): - logger.critical( - "Data for symbol %s in spot cache is not a DataFrame or Series. Skipping sanitization. Data: %s", - sym, - d, - ) - del self._spot[sym] - continue - d = d[d.index < self._today] - self._spot[sym] = d - for sym in self._chain_spot.keys(): - d = self._chain_spot[sym] - - if not _check_instance(d): - logger.critical( - "Data for symbol %s in chain_spot cache is not a DataFrame or Series. Skipping sanitization. Data: %s", - sym, - d, - ) - del self._chain_spot[sym] - continue - - d = d[d.index < self._today] - self._chain_spot[sym] = d - for sym in self._dividends.keys(): - d = self._dividends[sym] - if not _check_instance(d): - logger.critical( - "Data for symbol %s in dividends cache is not a DataFrame or Series. Skipping sanitization. Data: %s", - sym, - d, - ) - del self._dividends[sym] - continue - - d = d[d.index < self._today] - self._dividends[sym] = d - 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 - def _already_loaded( - self, sym: str, interval: str = "1d", start: str | datetime = None, end: str | datetime = None - ) -> Tuple[bool, List[pd.Timestamp]]: - """ - Check if the timeseries for a given symbol and interval is already loaded. - Hidden method that also returns missing dates if not fully loaded. - """ - start = start or self._start - end = end or self._end - sym_available = sym in self._spot - all_dates_present = False - - 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, 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) - - 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: - ## If missing dates is empty, return start and end - if not return_dates: - return_dates = [pd.Timestamp(start), pd.Timestamp(end)] - else: - return_dates = [min(return_dates), max(return_dates)] - - ## If all dates present, return empty list - else: - 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. - """ - ## Remove today's data before caching - 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( - self, sym: str, interval: str = "1d", start: str | datetime = None, end: str | datetime = None - ) -> bool: - """ - Public method to check if the timeseries for a given symbol and interval is already loaded. - """ - already_loaded, _ = self._already_loaded(sym, interval, start, end) - return already_loaded - - @timeit - def _remove_today_data(self, data: pd.DataFrame | pd.Series) -> pd.DataFrame | pd.Series: - """Remove today's data from the given DataFrame or Series.""" - today_str = ny_now().strftime("%Y-%m-%d") - if isinstance(data, pd.DataFrame): - return data[data.index < today_str] - elif isinstance(data, pd.Series): - return data[data.index < today_str] - else: - raise ValueError("Data must be a pandas DataFrame or Series. Got type: {}".format(type(data))) - - @timeit - 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]) - - ## 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: - """ - Sanitize all stored timeseries data by removing today's data. - Dropping duplicates, ensuring datetime index, and sorting. - """ - self._sanitize_today_data() - - for sym in self._spot.keys(): - sym = sym.upper() - data = self._spot[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._spot[sym] = data - - for sym in self._chain_spot.keys(): - sym = sym.upper() - data = self._chain_spot[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._chain_spot[sym] = data - - for sym in self._dividends.keys(): - sym = sym.upper() - data = self._dividends[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._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, - start_date: str | datetime = None, - end_date: str | datetime = None, - interval="1d", - force: bool = False, - ) -> None: - """ - Pre-sanitization before loading timeseries data for a given symbol and interval. - """ - sym = sym.upper() - if start_date is None: - start_date = self._start - if end_date is None: - end_date = self._end - already_loaded, dt_range = self._already_loaded(sym, interval, start_date, end_date) - if already_loaded and not force: - logger.info("Timeseries for %s already loaded. Use force=%s to reload.", sym, force) - return - - start_date = min(dt_range) - end_date = max(dt_range) - - try: - spot = retrieve_timeseries(sym, start_date, end_date, interval) - except YFinanceEmptyData: - logger.error("Failed to retrieve spot data for symbol %s. Skipping load.", sym) - return - - try: - chain_spot = retrieve_timeseries(sym, start_date, end_date, interval, spot_type="chain_price") - except YFinanceEmptyData: - logger.error("Failed to retrieve chain spot data for symbol %s. Skipping load.", sym) - return - try: - divs = obb.equity.fundamental.dividends(symbol=sym, provider="yfinance").to_df() - divs.set_index("ex_dividend_date", inplace=True) - except Exception: - 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) - 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: - spot = pd.concat([current_spot, spot]).sort_index() - spot = spot[~spot.index.duplicated(keep="last")] - else: - logger.info("No previous spot data for symbol %s, adding new data.", sym) - if current_chain_spot is not None: - chain_spot = pd.concat([current_chain_spot, chain_spot]).sort_index() - chain_spot = chain_spot[~chain_spot.index.duplicated(keep="last")] - 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 - ## This ensures only historical data is stored. - self._spot[sym] = spot - self._chain_spot[sym] = chain_spot - self._dividends[sym] = divs - self._split_factor[sym] = split_factor - - def load_timeseries( - self, - sym: str, - start_date: str | datetime = None, - end_date: str | datetime = None, - interval="1d", - force: bool = False, - ) -> None: - """ - Public method to load timeseries data for a given symbol and interval. - """ - self._pre_sanitize_load_timeseries(sym, start_date, end_date, interval, force) - - def _is_date_in_index(self, sym: str, date: pd.Timestamp, interval: str = "1d") -> bool: - """ - Check if a specific date is present in the timeseries index for a given symbol and interval. - Args: - sym (str): The stock symbol. - date (pd.Timestamp or str): The date to check. - interval (str): The interval of the timeseries data. Defaults to '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), - self._split_factor.get(sym), - ] - - for data in all_data: - date = pd.to_datetime(date).date() - if data is not None and date in data.index.date: - continue - else: - return False - return True - - def get_at_index(self, sym: str, index: pd.Timestamp, interval: str = "1d") -> AtIndexResult: - """ - Retrieve the spot price, chain spot price, and dividends 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 data. - Returns: - AtIndexResult: A dataclass containing spot price, chain spot price, and dividends.""" - - ## Only load date if not available. Not loading all unavailable dates - already_available = self._is_date_in_index(sym, index, interval) - - if not already_available: - logger.critical("Reloading timeseries data for symbol %s.", sym) - prev_day = (pd.Timestamp(index) - BDay(1)).strftime("%Y-%m-%d") - self._pre_sanitize_load_timeseries( - sym=sym, start_date=prev_day, end_date=index, interval=interval, force=True - ) - - ## OPTIMIZATION: Consolidate type checks and conversions (Task #3) - if not isinstance(index, pd.Timestamp): - index = pd.Timestamp(index) - - if sym not in self._spot: - raise ValueError(f"Symbol {sym} not found in timeseries data.") - - index_str = index.strftime("%Y-%m-%d") - 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 = 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, - 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, - ) -> None: - """ - 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. - It manipulates the timeseries data for the specified factor and appends the result to the additional_data dictionary. - The schema of additional_data: {additional_data_name: {sym: pd.Series}} - - Args: - 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. - column (Optional[str]): The column to use from the factor data. Defaults to 'close'. - force_add (bool): If True, will overwrite existing additional data for the given name and symbol. - - Raises: - ValueError: If the factor is not recognized or if the symbol is not found in the timeseries data. - """ - - ## 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, - 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', '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() - 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": - 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: - factor = "_" + factor - if factor in ["_spot", "_chain_spot", "_dividends", "_split_factor"]: - data = getattr(self, factor).get(sym) - elif factor == "_dividend_yield": - divs = self._dividends.get(sym) - if divs is None: - raise ValueError(f"No dividend data found for symbol {sym} to calculate dividend yield.") - spot = self._spot.get(sym) - if spot is None: - raise ValueError(f"No spot data found for symbol {sym} to calculate dividend yield.") - dividend_yield = divs / spot["close"] - data = dividend_yield - 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] - 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}.") - - elif factor is None: - spot = self._spot.get(sym) - 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 - 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] - 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] - 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 - - def __repr__(self) -> str: - return f"MarketTimeseries(symbols: {list(self._spot.keys())}, intervals: {list(self._spot.keys())})" - - -def get_timeseries_obj(live: bool = False) -> MarketTimeseries: - 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() -> 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 a559a54..2d0c9d0 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -148,18 +148,13 @@ from dateutil.relativedelta import relativedelta 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, - add_skip_columns, - load_position_data_new -) +from EventDriven.riskmanager.utils import parse_position_id, swap_ticker, add_skip_columns, load_position_data_new from trade.helpers.decorators import timeit -from trade.helpers.threads import runThreads # noqa -from trade.helpers.pools import runProcesses # noqa -from trade.helpers.helper import compare_dates, parse_option_tick, generate_option_tick_new +from trade.helpers.threads import runThreads # noqa +from trade.helpers.pools import runProcesses # noqa +from trade.helpers.helper import compare_dates, parse_option_tick, generate_option_tick_new, to_datetime from EventDriven.configs.core import SkipCalcConfig, UndlTimeseriesConfig, OptionPriceConfig -from trade.assets.rates import get_risk_free_rate_helper +from trade.assets.rates import get_risk_free_rate_helper_v2 from threading import Lock from typing import Dict, Any, Union import pandas as pd @@ -188,7 +183,7 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): self.special_dividends = load_riskmanager_cache(target="special_dividend") self.splits = load_riskmanager_cache(target="splits_raw") self.adjusted_strike_cache = load_riskmanager_cache(target="adjusted_strike_cache") - self.rf_timeseries = get_risk_free_rate_helper()["annualized"] + self.rf_timeseries = get_risk_free_rate_helper_v2()["annualized"] self.undl_timeseries_config = UndlTimeseriesConfig() self.option_price_config = OptionPriceConfig() self.lock = Lock() @@ -351,7 +346,7 @@ def get_timeseries(_id, direction): 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 - + start_time = time.time() runThreads( get_timeseries, thread_input_list, block=True @@ -445,10 +440,15 @@ def _skip_columns_adjustment( ) continue position_data[f"{col[0]}_{col[1]}".capitalize()] = func(position_data[col[0]]) + + ## Final clean up to ensure no infinite or NaN values in Midpoint after adjustments, and forward fill any missing values to maintain continuity for skip calculations + ## NOTE: RETURN TO THIS. IDK IF THIS IS A GOOD IDEA + position_data["Midpoint"] = position_data["Midpoint"].replace(0, np.nan).ffill() + return position_data # self.position_data[position_id] = position_data - def load_position_data(self, opttick) -> pd.DataFrame: # noqa + def load_position_data(self, opttick) -> pd.DataFrame: # noqa """ Load position data for a given option tick. @@ -458,12 +458,10 @@ def load_position_data(self, opttick) -> pd.DataFrame: # noqa ## Get Meta meta = parse_option_tick(opttick) self.market_timeseries.load_timeseries(sym=meta["ticker"]) - + return load_position_data_new( - opttick=opttick, - processed_option_data=self.options_cache, - start=self.start_date, - end=self.end_date) + 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: """ @@ -474,6 +472,10 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: """ meta = parse_option_tick(opttick) + exp = to_datetime(meta["exp_date"]) + effective_end = min( + to_datetime(self.end_date), exp + ) ## Ensure that we load data at least until the expiration date of the option, even if it is after the backtest end date, to account for any splits or dividends that might happen until expiration. ## Check if there's any split/special dividend splits = self.splits.get(meta["ticker"], []) @@ -486,7 +488,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: pack[0], # self.start_date, check_date, - self.end_date, + effective_end, ): pack = list(pack) ## Convert to list to append later pack.append("SPLIT") @@ -497,7 +499,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: pack[0], # self.start_date, check_date, - self.end_date, + effective_end, ): pack = list(pack) pack.append("DIVIDEND") @@ -518,7 +520,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: self.adjusted_strike_cache[opttick] = data["adj_strike"] return data[ (data.index >= pd.to_datetime(self.start_date) - relativedelta(months=3)) - & (data.index <= pd.to_datetime(self.end_date) + relativedelta(months=3)) + & (data.index <= pd.to_datetime(effective_end) + relativedelta(months=3)) ] # If there are splits, we need to load the data for each tick after adjusting strikes @@ -527,22 +529,6 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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() - - # ## If check_date is before the first split date, first_set_data is only up to the first split date - # 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]] - - # ## If check_date is after the first split date, first_set_data is only from the first split date onwards - # else: - # first_set_data = first_set_data[first_set_data.index >= to_adjust_split[0][0]] - - # ## Add Strike to keep track of adjustments - # print(f"Initial adj_strike for {opttick}: {adj_strike}") - # first_set_data['adj_strike'] = adj_strike - # first_set_data['factor'] = 1.0 segments = [] for event_date, factor, event_type in to_adjust_split: @@ -611,7 +597,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: ## Leave residual data outside the PM date range final_data = final_data[ (final_data.index >= pd.to_datetime(self.start_date) - relativedelta(months=3)) - & (final_data.index <= pd.to_datetime(self.end_date) + relativedelta(months=3)) + & (final_data.index <= pd.to_datetime(effective_end) + relativedelta(months=3)) ] self.adjusted_strike_cache[opttick] = final_data["adj_strike"] return final_data diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 96d3c40..3404dc5 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -183,8 +183,9 @@ import functools from trade.assets.helpers.utils import swap_ticker 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.datamanager._enums import DivType, OptionPricingModel from trade.datamanager.utils.date import sync_date_index 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 @@ -202,7 +203,7 @@ import signal from EventDriven._vars import get_use_temp_cache from .config import ffwd_data -from .._vars import OPTION_TIMESERIES_START_DATE +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE ## Vars @@ -216,8 +217,8 @@ location = Path(os.environ["GEN_CACHE_PATH"]) ## Allows users to set a custom cache location ## Loggers -logger = setup_logger("QuantTools.EventDriven.riskmanager") -time_logger = setup_logger("QuantTools.EventDriven.riskmanager.time") +logger = setup_logger("EventDriven.riskmanager.utils") +time_logger = setup_logger("EventDriven.riskmanager.utils.time") logger.info("RISK MANAGER is Using New DataManager") ## Caches @@ -251,18 +252,6 @@ def set_use_temp_cache(use_temp_cache: bool) -> None: # noqa ) -# def get_use_temp_cache() -> bool: -# """ -# Returns the current value of USE_TEMP_CACHE. - -# Returns: -# bool: The current value of USE_TEMP_CACHE. -# """ -# raise AttributeError("get_use_temp_cache has been moved to EventDriven._vars. Please update your imports.") -# global USE_TEMP_CACHE -# return USE_TEMP_CACHE - - # 2a) Create persistent cache or temp def get_persistent_cache() -> CustomCache: """ @@ -327,8 +316,8 @@ def register_info_stack(id, data, data_col, update_kwargs=None): copy_cat["streak_id"] = copy_cat[f"{k}_skip_day"].ne(copy_cat[f"{k}_skip_day"].shift()).cumsum() copy_cat["streak"] = copy_cat.groupby("streak_id").cumcount() + 1 info[f"{k.upper()}_MAX_STREAK"] = ( - copy_cat[copy_cat[f"{k}_skip_day"] == True].streak.max() # noqa - if not copy_cat[copy_cat[f"{k}_skip_day"] == True].streak.empty #noqa + copy_cat[copy_cat[f"{k}_skip_day"] == True].streak.max() # noqa + if not copy_cat[copy_cat[f"{k}_skip_day"] == True].streak.empty # noqa else 0 ) # noqa info["DATA_LEN"] = len(data) @@ -433,13 +422,18 @@ def get_cache(name: str) -> CustomCache: @dynamic_memoize -def populate_cache_with_chain(tick, date, chain_spot=None, print_url=True): +def populate_cache_with_chain( + tick, + date, + chain_spot=None, + print_url=True, + add_greeks=False, +): """ Populate the cache with chain data. """ chain = retrieve_chain_bulk(tick, "", date, date, "16:00", "C", print_url=False) logger.info(f"Retrieved chain for {tick} on {date}") - logger.error(f"Retrieved chain for {tick} on {date}") ## Retrieve OI ## Info: We use the previous business day to get OI @@ -495,7 +489,7 @@ def save_to_cache(id, date, spot): ] ## Filter out extreme moneyness to reduce size chain_clipped.columns = chain_clipped.columns.str.lower() chain_clipped["pct_spread"] = (chain_clipped["closeask"] - chain_clipped["closebid"]) / chain_clipped["midpoint"] - + chain_clipped[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = np.nan # Placeholder for Greeks, to be filled in later when we have the data return chain_clipped @@ -514,6 +508,7 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat It does not apply any splits or adjustments. It will only retrieve the data for the given option tick. """ import time + ## Check if the option tick is already processed if opttick in processed_option_data: return processed_option_data[opttick] @@ -528,10 +523,11 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat right=option_meta["put_call"], start_date=start, end_date=end, - dividend_type=DivType.CONTINUOUS + dividend_type=DivType.CONTINUOUS, + market_model=OptionPricingModel.BSM, ) logger.info(f"Data loading for {opttick} took {time.time() - start_time:.2f} seconds") - + ## Convert to DataFrame for easier comparison greeks = new_data.greek.timeseries option_spot = new_data.option_spot.timeseries @@ -540,7 +536,6 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat r = new_data.rates.timeseries greeks, option_spot, s, y, r = sync_date_index(greeks, option_spot, s, y, r) - ## set names properly start_time = time.time() s.name = "s" @@ -558,6 +553,7 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat processed_option_data[opttick] = data return data + def load_position_data(opttick, processed_option_data, start, end, *args, **kwargs) -> pd.DataFrame: """ Load position data for a given position ID. @@ -573,6 +569,7 @@ def load_position_data(opttick, processed_option_data, start, end, *args, **kwar """ return load_position_data_new(opttick, processed_option_data, start, end) + def enrich_data(data, ticker, s, r, y, s0_close): """ Args: @@ -594,7 +591,6 @@ def enrich_data(data, ticker, s, r, y, s0_close): 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. diff --git a/module_test/raw_code/DataManagers/utils.py b/module_test/raw_code/DataManagers/utils.py index 83762ef..3ce1de4 100644 --- a/module_test/raw_code/DataManagers/utils.py +++ b/module_test/raw_code/DataManagers/utils.py @@ -1,137 +1,143 @@ - import pandas as pd from trade.assets.Stock import Stock -from trade.assets.rates import get_risk_free_rate_helper +from trade.assets.rates import get_risk_free_rate_helper_v2 from trade.helpers.Logging import setup_logger -from dbase.DataAPI.ThetaData import (resample) +from dbase.DataAPI.ThetaData import resample from dateutil.relativedelta import relativedelta from trade import PRICING_CONFIG -logger = setup_logger('DataManagers.utils.py') +logger = setup_logger("DataManagers.utils.py") # Global MarketTimeseries instance for caching underlier data # This allows _ManagerLazyLoader to use cached data instead of hitting APIs _GLOBAL_MARKET_TIMESERIES = None + def set_global_market_timeseries(market_timeseries_instance): """ Set the global MarketTimeseries instance for caching underlier data. - + """ global _GLOBAL_MARKET_TIMESERIES _GLOBAL_MARKET_TIMESERIES = market_timeseries_instance logger.info(f"Global MarketTimeseries instance set: {market_timeseries_instance}") + def get_global_market_timeseries(): """ Get the global MarketTimeseries instance. - + Returns: MarketTimeseries instance or None if not set """ return _GLOBAL_MARKET_TIMESERIES + ## Temp fix to override going thru my sql. -EMPTY_TIMESEIRES_TABLE = pd.DataFrame({'Open': {}, - 'High': {}, - 'Low': {}, - 'Close': {}, - 'Volume': {}, - 'Bid_size': {}, - 'CloseBid': {}, - 'Ask_size': {}, - 'CloseAsk': {}, - 'Strike': {}, - 'Expiration': {}, - 'Put/Call': {}, - 'Underlier_price': {}, - 'RF_rate': {}, - 'RF_rate_name': {}, - 'dividend': {}, - 'OptionTick': {}, - 'Underlier': {}, - 'Datetime': {}, - 'BS_IV': {}, - 'Binomial_IV': {}, - 'Delta': {}, - 'Gamma': {}, - 'Vega': {}, - 'Theta': {}, - 'Rho': {}, - 'Vanna': {}, - 'Volga': {}, - 'Dollar_Delta': {}, - 'midpoint': {}, - 'midpoint_BS_IV': {}, - 'midpoint_Binomial_IV': {}, - 'midpoint_Delta': {}, - 'midpoint_Gamma': {}, - 'midpoint_Vega': {}, - 'midpoint_Theta': {}, - 'midpoint_Rho': {}, - 'midpoint_Vanna': {}, - 'midpoint_Volga': {}, - 'midpoint_Dollar_Delta': {}, - 'weighted_midpoint': {}, - 'weighted_midpoint_BS_IV': {}, - 'weighted_midpoint_Binomial_IV': {}, - 'weighted_midpoint_Delta': {}, - 'weighted_midpoint_Gamma': {}, - 'weighted_midpoint_Vega': {}, - 'weighted_midpoint_Theta': {}, - 'weighted_midpoint_Rho': {}, - 'weighted_midpoint_Vanna': {}, - 'weighted_midpoint_Volga': {}, - 'weighted_midpoint_Dollar_Delta': {}, - 'OpenInterest': {}, - 'bid_bs_iv': {}, - 'ask_bs_iv': {}, - 'bid_binomial_iv': {}, - 'ask_binomial_iv': {}, - 'midpoint_binomial_gamma': {}, - 'midpoint_binomial_vega': {}, - 'midpoint_binomial_delta': {}, - 'midpoint_binomial_rho': {}, - 'midpoint_binomial_vanna': {}, - 'midpoint_binomial_volga': {}, - 'midpoint_binomial_theta': {}, - 'last_updated': {}, - 'binomial_delta': {}, - 'binomial_gamma': {}, - 'binomial_vega': {}, - 'binomial_volga': {}, - 'binomial_vanna': {}, - 'binomial_rho': {}, - 'binomial_theta': {}, - 'midpoint_bs_vol_resolve': {}, - 'midpoint_binomial_vol_resolve': {}, - 'ask_binomial_delta': {}, - 'ask_binomial_gamma': {}, - 'ask_binomial_vega': {}, - 'ask_binomial_rho': {}, - 'ask_binomial_theta': {}, - 'ask_binomial_vanna': {}, - 'ask_binomial_volga': {}, - 'bid_binomial_delta': {}, - 'bid_binomial_gamma': {}, - 'bid_binomial_vega': {}, - 'bid_binomial_rho': {}, - 'bid_binomial_theta': {}, - 'bid_binomial_vanna': {}, - 'bid_binomial_volga': {}}) +EMPTY_TIMESEIRES_TABLE = pd.DataFrame( + { + "Open": {}, + "High": {}, + "Low": {}, + "Close": {}, + "Volume": {}, + "Bid_size": {}, + "CloseBid": {}, + "Ask_size": {}, + "CloseAsk": {}, + "Strike": {}, + "Expiration": {}, + "Put/Call": {}, + "Underlier_price": {}, + "RF_rate": {}, + "RF_rate_name": {}, + "dividend": {}, + "OptionTick": {}, + "Underlier": {}, + "Datetime": {}, + "BS_IV": {}, + "Binomial_IV": {}, + "Delta": {}, + "Gamma": {}, + "Vega": {}, + "Theta": {}, + "Rho": {}, + "Vanna": {}, + "Volga": {}, + "Dollar_Delta": {}, + "midpoint": {}, + "midpoint_BS_IV": {}, + "midpoint_Binomial_IV": {}, + "midpoint_Delta": {}, + "midpoint_Gamma": {}, + "midpoint_Vega": {}, + "midpoint_Theta": {}, + "midpoint_Rho": {}, + "midpoint_Vanna": {}, + "midpoint_Volga": {}, + "midpoint_Dollar_Delta": {}, + "weighted_midpoint": {}, + "weighted_midpoint_BS_IV": {}, + "weighted_midpoint_Binomial_IV": {}, + "weighted_midpoint_Delta": {}, + "weighted_midpoint_Gamma": {}, + "weighted_midpoint_Vega": {}, + "weighted_midpoint_Theta": {}, + "weighted_midpoint_Rho": {}, + "weighted_midpoint_Vanna": {}, + "weighted_midpoint_Volga": {}, + "weighted_midpoint_Dollar_Delta": {}, + "OpenInterest": {}, + "bid_bs_iv": {}, + "ask_bs_iv": {}, + "bid_binomial_iv": {}, + "ask_binomial_iv": {}, + "midpoint_binomial_gamma": {}, + "midpoint_binomial_vega": {}, + "midpoint_binomial_delta": {}, + "midpoint_binomial_rho": {}, + "midpoint_binomial_vanna": {}, + "midpoint_binomial_volga": {}, + "midpoint_binomial_theta": {}, + "last_updated": {}, + "binomial_delta": {}, + "binomial_gamma": {}, + "binomial_vega": {}, + "binomial_volga": {}, + "binomial_vanna": {}, + "binomial_rho": {}, + "binomial_theta": {}, + "midpoint_bs_vol_resolve": {}, + "midpoint_binomial_vol_resolve": {}, + "ask_binomial_delta": {}, + "ask_binomial_gamma": {}, + "ask_binomial_vega": {}, + "ask_binomial_rho": {}, + "ask_binomial_theta": {}, + "ask_binomial_vanna": {}, + "ask_binomial_volga": {}, + "bid_binomial_delta": {}, + "bid_binomial_gamma": {}, + "bid_binomial_vega": {}, + "bid_binomial_rho": {}, + "bid_binomial_theta": {}, + "bid_binomial_vanna": {}, + "bid_binomial_volga": {}, + } +) EMPTY_TIMESEIRES_TABLE.columns = [col.lower() for col in EMPTY_TIMESEIRES_TABLE.columns] + class _ManagerLazyLoader: def __init__(self, symbol): self.symbol = symbol - self.Stock = Stock(self.symbol, run_chain = False) + self.Stock = Stock(self.symbol, run_chain=False) self._eod = {} self._intra = {} def __reduce__(self): return (self.__class__, (self.symbol,)) - @property def eod(self): """ @@ -139,7 +145,7 @@ def eod(self): """ return EODData(self) - + @property def intra(self): """ @@ -147,116 +153,114 @@ def intra(self): """ return IntraData(self) - def _lazy_load(self, load_name, **kwargs): ## Utilizing the lazy load function to load data on demand, and speed up initialization market_ts = get_global_market_timeseries() - - if load_name == 's0_close': + + if load_name == "s0_close": ## Will use Kwargs to move between intra and EOD. - intra_flag = kwargs.pop('intra_flag') - + intra_flag = kwargs.pop("intra_flag") + # Try to get from MarketTimeseries cache first if market_ts is not None: try: - interval = '5min' if intra_flag else '1d' + interval = "5min" if intra_flag else "1d" logger.debug(f"Attempting to load s0_close for {self.symbol} from MarketTimeseries cache") - + # Get data from MarketTimeseries (factor='spot') - result = market_ts.get_timeseries( - sym=self.symbol, - factor='spot', - interval=interval - ) + result = market_ts.get_timeseries(sym=self.symbol, factor="spot", interval=interval) if result and result.spot is not None and not result.spot.empty: - logger.info(f"✓ Loaded s0_close for {self.symbol} from MarketTimeseries cache ({len(result.spot)} rows)") + logger.info( + f"✓ Loaded s0_close for {self.symbol} from MarketTimeseries cache ({len(result.spot)} rows)" + ) return result.spot except Exception as e: logger.debug(f"MarketTimeseries cache miss for s0_close: {e}, falling back to Stock.spot()") - + # Fall back to Stock.spot() if cache miss - return_item = (self.Stock.spot(ts = True, - ts_start = pd.to_datetime(self.exp) - relativedelta(years=5), - ts_end =pd.to_datetime(self.exp) + relativedelta(years=5), - **kwargs)) + return_item = self.Stock.spot( + ts=True, + ts_start=pd.to_datetime(self.exp) - relativedelta(years=5), + ts_end=pd.to_datetime(self.exp) + relativedelta(years=5), + **kwargs, + ) return return_item - - elif load_name == 's0_chain': - intra_flag = kwargs.pop('intra_flag') - + + elif load_name == "s0_chain": + intra_flag = kwargs.pop("intra_flag") + # Try to get from MarketTimeseries cache first (chain_spot type) if market_ts is not None: try: - interval = '5min' if intra_flag else '1d' + interval = "5min" if intra_flag else "1d" logger.debug(f"Attempting to load s0_chain for {self.symbol} from MarketTimeseries cache") - + # Get chain_spot data from MarketTimeseries - result = market_ts.get_timeseries( - sym=self.symbol, - factor='chain_spot', - interval=interval - ) + result = market_ts.get_timeseries(sym=self.symbol, factor="chain_spot", interval=interval) if result and result.chain_spot is not None and not result.chain_spot.empty: - logger.info(f"✓ Loaded s0_chain for {self.symbol} from MarketTimeseries cache ({len(result.chain_spot)} rows)") + logger.info( + f"✓ Loaded s0_chain for {self.symbol} from MarketTimeseries cache ({len(result.chain_spot)} rows)" + ) return result.chain_spot except Exception as e: logger.debug(f"MarketTimeseries cache miss for s0_chain: {e}, falling back to Stock.spot()") - + # Fall back to Stock.spot() - return_item = (self.Stock.spot(ts = True, - ts_start = pd.to_datetime(self.exp) - relativedelta(years=5), - ts_end =pd.to_datetime(self.exp) + relativedelta(years=5), - spot_type='chain_price', - **kwargs)) + return_item = self.Stock.spot( + ts=True, + ts_start=pd.to_datetime(self.exp) - relativedelta(years=5), + ts_end=pd.to_datetime(self.exp) + relativedelta(years=5), + spot_type="chain_price", + **kwargs, + ) return return_item - - elif load_name == 'r': - intra_flag = kwargs.get('intra_flag', False) - r = (get_risk_free_rate_helper()['annualized']) + + elif load_name == "r": + intra_flag = kwargs.get("intra_flag", False) + r = get_risk_free_rate_helper_v2()["annualized"] if intra_flag: - return resample(r, PRICING_CONFIG['INTRADAY_AGG'], {'risk_free_rate':'ffill'}) + return resample(r, PRICING_CONFIG["INTRADAY_AGG"], {"risk_free_rate": "ffill"}) else: return r - - elif load_name == 'r_name': - intra_flag = kwargs.get('intra_flag', False) - r = (get_risk_free_rate_helper()['name']) + + elif load_name == "r_name": + intra_flag = kwargs.get("intra_flag", False) + r = get_risk_free_rate_helper_v2()["name"] if intra_flag: - return resample(r, PRICING_CONFIG['INTRADAY_AGG'], {'risk_free_rate':'ffill'}) + return resample(r, PRICING_CONFIG["INTRADAY_AGG"], {"risk_free_rate": "ffill"}) else: return r - elif load_name == 'y': + elif load_name == "y": ## Get the dividend yield - intra_flag = kwargs.get('intra_flag', False) - + intra_flag = kwargs.get("intra_flag", False) + # Try to get dividends from MarketTimeseries cache first if market_ts is not None: try: - interval = '5min' if intra_flag else '1d' + interval = "5min" if intra_flag else "1d" logger.debug(f"Attempting to load dividends for {self.symbol} from MarketTimeseries cache") - + # Get dividends data from MarketTimeseries - result = market_ts.get_timeseries( - sym=self.symbol, - factor='dividends', - interval=interval - ) + result = market_ts.get_timeseries(sym=self.symbol, factor="dividends", interval=interval) if result and result.dividends is not None and not result.dividends.empty: - logger.info(f"✓ Loaded dividends for {self.symbol} from MarketTimeseries cache ({len(result.dividends)} rows)") + logger.info( + f"✓ Loaded dividends for {self.symbol} from MarketTimeseries cache ({len(result.dividends)} rows)" + ) return result.dividends except Exception as e: - logger.debug(f"MarketTimeseries cache miss for dividends: {e}, falling back to Stock.div_yield_history()") - + logger.debug( + f"MarketTimeseries cache miss for dividends: {e}, falling back to Stock.div_yield_history()" + ) + # Fall back to Stock.div_yield_history() - y = (self.Stock.div_yield_history(start = pd.to_datetime(self.exp) - relativedelta(years=5))) + y = self.Stock.div_yield_history(start=pd.to_datetime(self.exp) - relativedelta(years=5)) if intra_flag: - return resample(y, PRICING_CONFIG['INTRADAY_AGG'], {'dividend_yield':'ffill'}) + return resample(y, PRICING_CONFIG["INTRADAY_AGG"], {"dividend_yield": "ffill"}) else: return y - ### _ManagerLazyLoader Helpers @@ -265,48 +269,50 @@ def __init__(inner, parent): inner.parent = parent super().__init__() - def __getitem__(inner, key): ## Custom getter for EOD Dict. To initialize the data, if not already done + def __getitem__(inner, key): ## Custom getter for EOD Dict. To initialize the data, if not already done if key not in inner.parent._intra: - if key not in ['s0_close', 's0_chain', 'r', 'y', 'r_name']: - raise KeyError(f"{key} not in intra data, expected one of: ['s0_close', 's0_chain', 'r', 'y', 'r_name']") - inner.parent._intra[key] = inner.parent._lazy_load(key, ts_timewidth = '5', ts_timeframe = 'minute', intra_flag = True) + if key not in ["s0_close", "s0_chain", "r", "y", "r_name"]: + raise KeyError( + f"{key} not in intra data, expected one of: ['s0_close', 's0_chain', 'r', 'y', 'r_name']" + ) + inner.parent._intra[key] = inner.parent._lazy_load( + key, ts_timewidth="5", ts_timeframe="minute", intra_flag=True + ) return inner.parent._intra[key] - + def __contains__(inner, key): return key in inner.parent._intra - + def __repr__(inner): return inner.parent._intra.__repr__() - + def __len__(inner): return len(inner.parent._intra) - + def keys(inner): return inner.parent._intra.keys() class EODData(dict): - def __init__(inner, parent): ## inner is the instance of the class, parent is the instance of the parent class + def __init__(inner, parent): ## inner is the instance of the class, parent is the instance of the parent class inner.parent = parent super().__init__() - def __getitem__(inner, key): ## Custom getter for EOD Dict. To initialize the data, if not already done + def __getitem__(inner, key): ## Custom getter for EOD Dict. To initialize the data, if not already done if key not in inner.parent._eod: - if key not in ['s0_close', 's0_chain', 'r', 'y', 'r_name']: + if key not in ["s0_close", "s0_chain", "r", "y", "r_name"]: raise KeyError(f"{key} not in eod data, expected one of: ['s0_close', 's0_chain', 'r', 'y', 'r_name]") - inner.parent._eod[key] = inner.parent._lazy_load(key, intra_flag = False) + inner.parent._eod[key] = inner.parent._lazy_load(key, intra_flag=False) return inner.parent._eod[key] - + def __contains__(inner, key): return key in inner.parent._eod - + def __repr__(inner): return inner.parent._eod.__repr__() - + def __len__(inner): return len(inner.parent._eod) - + def keys(inner): return inner.parent._eod.keys() - - diff --git a/trade/__init__.py b/trade/__init__.py index 12e2470..fea4064 100644 --- a/trade/__init__.py +++ b/trade/__init__.py @@ -6,7 +6,7 @@ import atexit from zoneinfo import ZoneInfo import pandas as pd -import pandas_market_calendars as mcal +import pandas_market_calendars as mcal # type: ignore from dotenv import load_dotenv from trade.helpers.clear_cache import cleanup_expired_caches from .helpers.Logging import setup_logger @@ -40,6 +40,10 @@ 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_new_years = pd.date_range(start="2000-01-01", end="2040-01-01", freq="AS").strftime("%Y-%m-%d").to_list() +all_christmas = pd.date_range(start="2000-01-01", end="2040-01-01", freq="A-DEC").strftime("%Y-%m-%d").to_list() +holidays.update(all_new_years) +holidays.update(all_christmas) HOLIDAY_SET = set(holidays) DATETIME_HOLIDAY_SET = set(pd.to_datetime(list(HOLIDAY_SET), format="%Y-%m-%d")) @@ -253,6 +257,8 @@ def get_pricing_config() -> dict: logger.warning(f"Missing key {key} in pricing config. Setting default value {value}.") return PRICING_CONFIG +MARKET_CLOSE = pd.Timestamp(get_pricing_config()["MARKET_CLOSE_TIME"]) # noqa +MARKET_OPEN = pd.Timestamp(get_pricing_config()["MARKET_OPEN_TIME"]) # noqa def reload_pricing_config(): """ diff --git a/trade/assets/OptionChain.py b/trade/assets/OptionChain.py index 5ba9773..227840d 100644 --- a/trade/assets/OptionChain.py +++ b/trade/assets/OptionChain.py @@ -1,25 +1,25 @@ import time -start_time = time.time() + +start_time = time.time() import os, sys from trade.models.VolSurface import SurfaceLab from dbase.database.SQLHelpers import DatabaseAdapter -from trade.helpers.helper import (change_to_last_busday, - is_busday, - is_USholiday, - IV_handler, - implied_volatility, - time_distance_helper, - generate_option_tick, - generate_option_tick_new, - setup_logger, - binomial_implied_vol) +from trade.helpers.helper import ( + change_to_last_busday, + is_busday, + is_USholiday, + IV_handler, + implied_volatility, + time_distance_helper, + generate_option_tick, + generate_option_tick_new, + setup_logger, + binomial_implied_vol, +) from trade.helpers.Context import Context -from trade.assets.rates import get_risk_free_rate_helper -from dbase.DataAPI.ThetaData import (retrieve_quote_rt, - retrieve_eod_ohlc, - retrieve_quote, - list_contracts) +from trade.assets.rates import get_risk_free_rate_helper_v2 +from dbase.DataAPI.ThetaData import retrieve_quote_rt, retrieve_eod_ohlc, retrieve_quote, list_contracts from dbase.database.SQLHelpers import store_SQL_data_Insert_Ignore from trade._multiprocessing import ensure_global_start_method, PathosPool @@ -31,7 +31,8 @@ from trade.helpers.decorators import log_time, log_error, log_error_with_stack from trade.helpers.helper_types import OptionModelAttributes import pandas as pd -logger = setup_logger('OptionChain') + +logger = setup_logger("OptionChain") ## To-Do: LQD is not being retrieved. Throwing error on 2024-12-06 @@ -43,97 +44,93 @@ def shutdown(pool): global shutdown_event shutdown_event = True - logger.info('shutdown_event set') + logger.info("shutdown_event set") pool.terminate() -def get_set( - ticker, - date, - exp, - right, - strike, - spot, - r, - q -) -> dict: +def get_set(ticker, date, exp, right, strike, spot, r, q) -> 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(end = exp, start = date), r = r, flag = right.lower(), price = price, q = q) + 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(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) + logger.error(f"Error in get_set: {e}", exc_info=True) raise e - return {'price': price, 'vol': vol} + return {"price": price, "vol": vol} + def get_df_set(split_df, id): if len(split_df) == 0: return pd.DataFrame() - split_df[['expiration', 'build_date']] = split_df[['expiration', 'build_date']].astype(str) - - split_df[['price', 'vol']] = split_df.apply(lambda x: get_set(x['ticker'], x['build_date'], x['expiration'], x['right'], x['strike'], x['Spot'], x['r'], x['q']), axis = 1, result_type = 'expand') + split_df[["expiration", "build_date"]] = split_df[["expiration", "build_date"]].astype(str) + + split_df[["price", "vol"]] = split_df.apply( + lambda x: get_set( + x["ticker"], x["build_date"], x["expiration"], x["right"], x["strike"], x["Spot"], x["r"], x["q"] + ), + axis=1, + result_type="expand", + ) return split_df + def produce_chain_values(chain, date, ticker, stock): results = [] - global shutdown_event # To-do: Why after shut down event set in some situations, it is not being reset? + global shutdown_event # To-do: Why after shut down event set in some situations, it is not being reset? shudown_event = False stk = stock - spot = list(stk.spot(spot_type = OptionModelAttributes.spot_type.value).values())[0] + spot = list(stk.spot(spot_type=OptionModelAttributes.spot_type.value).values())[0] q = stk.div_yield() rf_rate = stk.rf_rate - chain['Spot'] = spot - chain['r'] = rf_rate - chain['q'] = q - chain['build_date'] = date - chain['ticker'] = ticker - + chain["Spot"] = spot + chain["r"] = rf_rate + chain["q"] = q + chain["build_date"] = date + chain["ticker"] = ticker + workers = 25 split_data = np.array_split(chain.reset_index(), workers) - - #Multiprocessing to speed up chain retrieval + # Multiprocessing to speed up chain retrieval ensure_global_start_method() - pool = PathosPool(nodes = workers) + pool = PathosPool(nodes=workers) pool.restart() try: for result in pool.imap(get_df_set, split_data, [i for i in range(workers)]): results.append(result) if shutdown_event: break - - + except KeyboardInterrupt: - logger.error('Interrupted by Keyboard') + logger.error("Interrupted by Keyboard") shutdown(pool) - except Exception as e: - logger.error('Exception:', e) + logger.error("Exception:", e) shutdown(pool) - finally: pool.close() pool.join() - + if len(results) == 0: return None return pd.concat(results) - - - class OptionChain: """ - Class responsible for creating option chains for a given stock. + Class responsible for creating option chains for a given stock. Expected behavior is to return the chain, corresponding vol, price and in select instances, Volatility surface """ - def __init__(self, ticker, build_date, stock, dumas_width = 0.75, **kwargs) -> None: + def __init__(self, ticker, build_date, stock, dumas_width=0.75, **kwargs) -> None: self.ticker = ticker.upper() - logger.info(f'Creating OptionChain for {self.ticker}, on {build_date}') + logger.info(f"Creating OptionChain for {self.ticker}, on {build_date}") self.build_date = build_date self.chain = None self.lab = None @@ -144,10 +141,9 @@ def __init__(self, ticker, build_date, stock, dumas_width = 0.75, **kwargs) -> N self.kwargs = kwargs self.__initiate_chain() - def __repr__(self) -> str: return f"OptionChain({self.ticker}, {self.build_date})" - + def __str__(self) -> str: return f"OptionChain({self.ticker}, {self.build_date})" @@ -158,30 +154,28 @@ def __initiate_chain(self): WHERE ticker = '{self.ticker}' AND build_date = '{self.build_date}'""" - chain = self.dbAdapter.query_database('vol_surface', 'option_chain',query) + chain = self.dbAdapter.query_database("vol_surface", "option_chain", query) if not chain.empty: self.chain = chain self.__option_chain_bool() - return - + return - chain = self.__option_chain_bool() + chain = self.__option_chain_bool() chain_new = produce_chain_values(chain, self.build_date, self.ticker, self.stock) self.chain = chain_new self.save_thread = None - self.save_thread = Thread(target = self._save_chain) + self.save_thread = Thread(target=self._save_chain) self.save_thread.start() return chain_new @log_error_with_stack(logger, False) - def get_chain(self, return_values = False): + def get_chain(self, return_values=False): if return_values: return self.chain - + else: return self.simple_chain - @log_error_with_stack(logger, False) def __option_chain_bool(self): @@ -189,38 +183,39 @@ def __option_chain_bool(self): date = self.build_date contracts = list_contracts(self.ticker, date) - contracts.expiration = pd.to_datetime(contracts.expiration, format='%Y%m%d') + contracts.expiration = pd.to_datetime(contracts.expiration, format="%Y%m%d") ## Producing the DTE for the contracts - contracts['DTE'] = (contracts['expiration'] - pd.to_datetime(date)).dt.days - contracts_v2 = contracts.pivot_table(index=['expiration', 'DTE','strike','right'],values = 'root', aggfunc='count') - + contracts["DTE"] = (contracts["expiration"] - pd.to_datetime(date)).dt.days + contracts_v2 = contracts.pivot_table( + index=["expiration", "DTE", "strike", "right"], values="root", aggfunc="count" + ) + ## Formatting the pivot table - contracts_v2.fillna(0, inplace = True) - contracts_v2.where(contracts_v2 == 1, False, inplace = True) - contracts_v2.where(contracts_v2 == 0, True, inplace = True) + contracts_v2.fillna(0, inplace=True) + contracts_v2.where(contracts_v2 == 1, False, inplace=True) + contracts_v2.where(contracts_v2 == 0, True, inplace=True) self.simple_chain = contracts_v2 return contracts_v2 - - + @log_error_with_stack(logger, False) def initiate_lab(self): - force_build = self.kwargs.get('force_build', False) + force_build = self.kwargs.get("force_build", False) if self.chain is None: self.chain = self.get_chain(True) - self.lab = SurfaceLab(self.ticker, self.build_date, self.chain.copy(),self.dumas_width, force_build = force_build) + self.lab = SurfaceLab( + self.ticker, self.build_date, self.chain.copy(), self.dumas_width, force_build=force_build + ) - @log_error_with_stack(logger, False) def _save_chain(self): if self.chain is None: self.get_chain(True) chain = self.chain.copy() - chain.drop(columns = ['root'], inplace = True) - chain['moneyness'] = chain['strike'] / chain['Spot'] - chain['option_tick'] = chain.apply(lambda x: generate_option_tick_new(x['ticker'],x['right'], x['expiration'], x['strike']), axis = 1) - chain['build_date'] = self.build_date - self.dbAdapter.save_to_database(chain, 'vol_surface', 'option_chain') - - - + chain.drop(columns=["root"], inplace=True) + chain["moneyness"] = chain["strike"] / chain["Spot"] + chain["option_tick"] = chain.apply( + lambda x: generate_option_tick_new(x["ticker"], x["right"], x["expiration"], x["strike"]), axis=1 + ) + chain["build_date"] = self.build_date + self.dbAdapter.save_to_database(chain, "vol_surface", "option_chain") diff --git a/trade/assets/Stock.py b/trade/assets/Stock.py index 842fa67..7e0ffa4 100644 --- a/trade/assets/Stock.py +++ b/trade/assets/Stock.py @@ -24,7 +24,7 @@ from trade.helpers.helper import * from trade.helpers.openbb_helper import * import yfinance as yf -from trade.assets.rates import get_risk_free_rate_helper +from trade.assets.rates import get_risk_free_rate_helper_v2 from dbase.DataAPI.ThetaData import resample from pandas.tseries.offsets import BDay from trade.helpers.helper import change_to_last_busday @@ -333,7 +333,7 @@ def init_rfrate_ts(self): """ Class method that initiates Risk Free rate timeseries across all classes """ - ts = get_risk_free_rate_helper() + ts = get_risk_free_rate_helper_v2() self.__rf_ts = ts def init_risk_free_rate(self): @@ -725,7 +725,7 @@ def details(self): print(f"Market Cap: N/A") else: cap = int(info.get("marketCap", "N/A")) - print(f"Market Cap: {cap/1_000_000_000:.2f}bn") + print(f"Market Cap: {cap / 1_000_000_000:.2f}bn") print(f"PE Ratio: {info.get('trailingPE', 'N/A')}") print(f"Dividend Yield: {info.get('dividendYield', 'N/A')}") print(f"52 Week High: {info.get('fiftyTwoWeekHigh', 'N/A')}") diff --git a/trade/assets/helpers/DataManagers.py b/trade/assets/helpers/DataManagers.py index fce262f..225598b 100644 --- a/trade/assets/helpers/DataManagers.py +++ b/trade/assets/helpers/DataManagers.py @@ -21,7 +21,6 @@ import concurrent.futures from trade.assets.Stock import Stock from trade.helpers.helper import generate_option_tick_new -from trade.assets.rates import get_risk_free_rate_helper from trade.helpers.helper import IV_handler, time_distance_helper, binomial_implied_vol, wait_for_response from trade.helpers.helper import extract_numeric_value, change_to_last_busday, parse_option_tick from trade.helpers.Logging import setup_logger diff --git a/trade/assets/rates.py b/trade/assets/rates.py index 7d75e17..e431ab5 100644 --- a/trade/assets/rates.py +++ b/trade/assets/rates.py @@ -1,9 +1,6 @@ from dotenv import load_dotenv import sys import os - -load_dotenv() - from dbase.database.SQLHelpers import * from dbase.DataAPI.ThetaData import * import datetime @@ -16,6 +13,7 @@ from pandas.tseries.offsets import BDay import time +load_dotenv() warnings.filterwarnings("ignore") logger = setup_logger("rates") rates_thread_logger = setup_logger("rates_thread", stream_log_level=logging.INFO) @@ -58,6 +56,7 @@ def get_risk_free_rate_helper(interval="1d", use="db") -> pd.DataFrame: Source of the data. Default is 'yf', other option is 'db' """ + # from trade.datamanager import RatesDataManager data = _fetch_rates(interval=interval).copy() if interval != "1d": @@ -66,6 +65,30 @@ def get_risk_free_rate_helper(interval="1d", use="db") -> pd.DataFrame: return data ## Not adding the resample schema for now +def get_risk_free_rate_helper_v2(*args, **kwargs) -> pd.Series: + """ + Return the latest risk free rate as a single value. This is used for discounting cash flows and other calculations where we need a single rate. + + Returns + ------- + pd.Series + A series with the latest risk free rate, indexed by datetime. + """ + from trade.datamanager import RatesDataManager + + annualized = ( + RatesDataManager() + .get_risk_free_rate_timeseries(start_date="2017-01-01", end_date=datetime.datetime.now().strftime("%Y-%m-%d")) + .timeseries + ) + daily = (annualized * 100).apply(deannualize) + data = pd.DataFrame({"annualized": annualized, "daily": daily}) + data.index = pd.to_datetime(data.index) + data.index.name = "Date" + data = data[["daily", "annualized"]] + return data + + def _fetch_rates(interval): """ Handles _rates_cache logic picking @@ -74,7 +97,7 @@ def _fetch_rates(interval): 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 @@ -156,7 +179,7 @@ def save_previous_rates_date(): import yfinance as yf print("Saving previous rates date") - max_date = get_risk_free_rate_helper().index.max() + max_date = get_risk_free_rate_helper_v2().index.max() rtes = yf.download( "^IRX", progress=False, @@ -174,7 +197,9 @@ def save_previous_rates_date(): rtes["daily"] = (1 + rtes["annualized"]) ** (1 / 365) - 1 rtes["yf_tick"] = "^IRX" rtes["description"] = "13 WEEK TREASURY BILL" - rtes = rtes[rtes.index > get_risk_free_rate_helper().index.min()][["yf_tick", "description", "annualized", "daily"]] + rtes = rtes[rtes.index > get_risk_free_rate_helper_v2().index.min()][ + ["yf_tick", "description", "annualized", "daily"] + ] rtes.rename(columns={"annualized": "annualized_rate", "daily": "daily_rate", "Date": "datetime"}, inplace=True) rtes.index.name = "datetime" rtes.reset_index(inplace=True) diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py index f4712b5..97d3a74 100644 --- a/trade/datamanager/base.py +++ b/trade/datamanager/base.py @@ -137,6 +137,11 @@ def get_cache(cls) -> CustomCache: @classmethod def clear_all_caches(cls) -> None: """Clears caches for all registered DataManager subclasses.""" + from .market_data import MarketTimeseries + from .market_data_helpers.dividends import DIVIDEND_CACHE + + MarketTimeseries.clear_caches() + DIVIDEND_CACHE.clear() 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() diff --git a/trade/datamanager/greeks.py b/trade/datamanager/greeks.py index f167328..14c501c 100644 --- a/trade/datamanager/greeks.py +++ b/trade/datamanager/greeks.py @@ -87,6 +87,7 @@ 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 +from trade import MARKET_CLOSE logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) @@ -409,11 +410,13 @@ def _get_binomial_greeks( if early_return: result.timeseries = cached_data[greeks_to_compute] return result - + + expiration_ts = to_datetime(expiration) + expiration_ts = expiration_ts.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation request = self._create_load_request( start_date=start_date, end_date=end_date, - expiration=expiration, + expiration=expiration_ts, strike=strike, right=right, dividend_type=dividend_type, @@ -437,13 +440,13 @@ def _get_binomial_greeks( 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"), + end_dates=to_datetime([expiration_ts] * len(S), format="%Y-%m-%d"), ) ## Now compute greeks greeks_res_dict = binomial_tree_greeks( K=[strike] * len(S), - expiration=[expiration] * len(S), + expiration=[expiration_ts] * len(S), sigma=vol, S=S, r=r, @@ -594,20 +597,22 @@ def _get_bsm_greeks( 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) + expiration_ts = to_datetime(expiration) + expiration_ts = expiration_ts.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation ## 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), + _end_dates=[expiration_ts] * 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) + div_rates=d.values, _valuation_dates=f.index.tolist(), _end_dates=[expiration_ts] * len(f) ) ## Now compute greeks @@ -618,7 +623,7 @@ def _get_bsm_greeks( r=r, sigma=vol, valuation_dates=s.index.tolist(), - end_dates=[expiration] * len(s), + end_dates=[expiration_ts] * len(s), option_type=[right.lower()] * len(s), dividend_type=dividend_type.value, div_amount=pv_divs, diff --git a/trade/datamanager/loaders.py b/trade/datamanager/loaders.py index 11fbeba..8bcbbad 100644 --- a/trade/datamanager/loaders.py +++ b/trade/datamanager/loaders.py @@ -217,6 +217,6 @@ def load_full_option_data( 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 index eeb18a7..8ee745b 100644 --- a/trade/datamanager/market_data.py +++ b/trade/datamanager/market_data.py @@ -201,15 +201,16 @@ 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 trade.assets.rates import get_risk_free_rate_helper_v2 +from EventDriven._vars import load_riskmanager_cache +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE from EventDriven.exceptions import UnaccessiblePropertyError from trade.datamanager.utils.cache import ( _cache_it_timeseries_data_structure, _data_structure_cache_check_missing, - _CachedData, # noqa - _extract_data, # noqa - _simple_extract_from_cache # noqa + _CachedData, # noqa + _extract_data, # noqa + _simple_extract_from_cache, # noqa ) from trade import SIGNALS_TO_RUN @@ -413,7 +414,6 @@ 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", "split_factor", "dividend_yield"] _refresh_delta: Optional[timedelta] = timedelta(minutes=30) _last_refresh: Optional[datetime] = field(default_factory=ny_now) @@ -421,6 +421,27 @@ class MarketTimeseries: _end: str = (datetime.now() - BDay(1)).strftime("%Y-%m-%d") _today: str = datetime.now().strftime("%Y-%m-%d") should_refresh: bool = True + _rates: ClassVar[pd.DataFrame] = None + + @property + def rates(self) -> pd.DataFrame: + """Risk-free rates DataFrame with annualized yields. + + Retrieves and caches the risk-free rate curve from the Federal Reserve. Rates + are annualized and indexed by date and tenor (e.g., 1M, 3M, 6M, 1Y). Used for + option pricing and discounting cash flows. + + Returns: + DataFrame with columns ['date', 'tenor', 'rate'] where 'rate' is the + annualized yield for that tenor on that date. + Examples: + >>> mts = MarketTimeseries() + >>> rates = mts.rates + >>> print(rates.head()) + """ + if self._rates is None: + self._rates = get_risk_free_rate_helper_v2() + return self._rates @property def spot(self) -> dict: @@ -540,6 +561,7 @@ def _load_chain_spot_into_cache(self, sym: str, start: str, end: str) -> pd.Data end=end, spot_type="chain_spot", ) + _cache_it_timeseries_data_structure( existing=self._chain_spot.get(sym), key=sym, @@ -733,7 +755,6 @@ def _get_chain_spot_timeseries(self, sym: str, start: str = None, end: str = Non 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: @@ -880,7 +901,7 @@ def get_split_factor_at_index(self, sym: str, index: pd.Timestamp, *args, **kwar split_factor_series = self._split_factor.get(sym) if split_factor_series is None: return 1.0 - + split_factor_series = _extract_data(split_factor_series) # if isinstance(split_factor_series, _CachedData) or split_factor_series.__class__.__name__ == "_CachedData": # split_factor_series = split_factor_series.data diff --git a/trade/datamanager/result.py b/trade/datamanager/result.py index a6a9264..f97bfa4 100644 --- a/trade/datamanager/result.py +++ b/trade/datamanager/result.py @@ -1,6 +1,6 @@ import pandas as pd from dataclasses import dataclass, field -from typing import Any, Dict, Optional, List +from typing import Any, Dict, Optional, List, Union from trade.optionlib.config.types import DivType from ._enums import ( GreekType, @@ -25,6 +25,7 @@ class Result: model_input_keys: Optional[Dict[str, Any]] = None rt: Optional[bool] = False fallback_option: Optional[RealTimeFallbackOption] = None + timeseries: Optional[Union[pd.Series, pd.DataFrame]] = None def __post_init__(self): """Simple formatting""" diff --git a/trade/datamanager/timeseries.py b/trade/datamanager/timeseries.py index a92c63c..498e68c 100644 --- a/trade/datamanager/timeseries.py +++ b/trade/datamanager/timeseries.py @@ -52,6 +52,7 @@ from typing import Any, Optional import inspect +from trade.datamanager.result import Result from trade.helpers.Logging import setup_logger from .spot import SpotDataManager from .vol import VolDataManager @@ -140,21 +141,21 @@ def wrapper(*args, **kwargs): # Set as instance attribute (overrides class method) setattr(self, wrapper_name, wrapper) - def rt(self, *args, **kwargs): + def rt(self, *args, **kwargs) -> Result: """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): + def get_at_time(self, *args, **kwargs) -> Result: """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): + def get_timeseries(self, *args, **kwargs) -> Result: """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) diff --git a/trade/datamanager/utils/cache.py b/trade/datamanager/utils/cache.py index 9bb68fc..6a8478f 100644 --- a/trade/datamanager/utils/cache.py +++ b/trade/datamanager/utils/cache.py @@ -1,15 +1,17 @@ import pandas as pd -from datetime import date +from datetime import date, datetime from typing import Any, List, Optional, Union, Tuple from trade.helpers.Logging import setup_logger from trade.helpers.helper import CustomCache, change_to_last_busday, get_missing_dates, to_datetime +from trade import get_pricing_config from dataclasses import dataclass 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()) - +MARKET_OPEN = pd.Timestamp(get_pricing_config()["MARKET_OPEN_TIME"]).time() +MARKET_CLOSE = pd.Timestamp(get_pricing_config()["MARKET_CLOSE_TIME"]).time() @dataclass class _CachedData: """ @@ -66,6 +68,9 @@ def _comprehensive_cache_check(self, start_dt: DATE_HINT, end_dt: DATE_HINT) -> - 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 """ + if to_datetime(end_dt).date() == date.today() and datetime.now().time() < MARKET_OPEN: + logger.info(f"Requested end date {end_dt} is today but market has not opened yet. Adjusting end date to last business day.") + end_dt = to_datetime(end_dt) - pd.tseries.offsets.BDay(1) if self.is_fully_covered(start_dt, end_dt): logger.info(f"Cache hit for timeseries data structure key: {self.key}") sanitized_data = _data_structure_sanitize( diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index 7b3c309..53661cc 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -19,6 +19,7 @@ import os from typing import Tuple, List, Optional, Union from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME +from trade import MARKET_CLOSE, MARKET_OPEN logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) PATH = Path(os.environ["GEN_CACHE_PATH"]) / "dm_gen_cache" @@ -35,6 +36,51 @@ expire_days=365, ) +def _convert_expiration_to_datetime(expiration: Union[str, datetime]) -> datetime: + """ + Converts an expiration date to a datetime object. If the input is already a datetime, it is returned as is. + + Args: + expiration: The expiration date as a string or datetime. + Returns: + A datetime object representing the expiration date. + """ + if isinstance(expiration, datetime): + return expiration.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation + else: + return to_datetime(expiration).replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) + +def _convert_dates_to_datetime(dates: List[Union[str, datetime]]) -> List[datetime]: + """ + Converts a list of dates to datetime objects. If an element is already a datetime, it is returned as is. + + Args: + dates: A list of dates as strings or datetimes. + Returns: + A list of datetime objects representing the input dates. + """ + converted_dates = [] + for d in dates: + if isinstance(d, datetime): + converted_dates.append(d.replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute)) # Set to market open time for accurate T calculation + else: + converted_dates.append(to_datetime(d).replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute)) + return converted_dates + +def _convert_date_to_datetime(date_input: Union[str, datetime]) -> datetime: + """ + Converts a date to a datetime object. If the input is already a datetime, it is returned as is. + + Args: + date_input: The date as a string or datetime. + Returns: + A datetime object representing the input date. + """ + if isinstance(date_input, datetime): + return date_input.replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute) # Set to market open time for accurate T calculation + else: + return to_datetime(date_input).replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute) + 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): diff --git a/trade/datamanager/utils/logging.py b/trade/datamanager/utils/logging.py index 0ee405a..346e78a 100644 --- a/trade/datamanager/utils/logging.py +++ b/trade/datamanager/utils/logging.py @@ -66,6 +66,13 @@ def change_all_optionlib_loggers_level(level: str = None): for logger in loggers: change_logger_stream_level(logger, getattr(logging, level.upper(), logging.INFO)) +def change_all_logger(level: str = None): + """Change logging level for all loggers under 'trade'.""" + if level is None: + level = LOGGING_LEVEL + change_all_optionlib_loggers_level(level) + change_logging_in_all_datamanager_loggers(level) + def register_to_factor_list(name:str): FACTOR_DMS.add(name) diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index ae4d3a1..78099cf 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -94,5 +94,3 @@ def get_loaded_names() -> set: ## 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 index 5ad1e0b..726f48d 100644 --- a/trade/datamanager/vol.py +++ b/trade/datamanager/vol.py @@ -56,6 +56,7 @@ SpotResult, DividendsResult, ) +from trade.datamanager.utils.cache import MARKET_OPEN, MARKET_CLOSE # noqa from trade.datamanager.utils.vol_helpers import ( _prepare_time_to_expiration, _handle_cache_for_vol, @@ -74,8 +75,10 @@ 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. @@ -281,9 +284,11 @@ def _get_bsm_implied_volatility_timeseries( rates = r.daily_risk_free_rates option_spot = market_price.price forward, rates, option_spot = sync_date_index(forward, rates, option_spot) + expiration_ts = to_datetime(expiration) + expiration_ts = expiration_ts.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation # Use utility: Prepare T - T = _prepare_time_to_expiration(forward.index, expiration) + T = _prepare_time_to_expiration(forward.index, expiration_ts) # Calculate IV iv_timeseries = vector_bsm_iv_estimation( @@ -434,7 +439,9 @@ def _get_crr_implied_volatility_timeseries( ) # Use utility: Prepare T - T = _prepare_time_to_expiration(option_spot.index, expiration) + expiration_ts = to_datetime(expiration) + expiration_ts = expiration_ts.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation + T = _prepare_time_to_expiration(option_spot.index, expiration_ts) # Calculate IV iv_timeseries = vector_crr_iv_estimation( @@ -597,10 +604,12 @@ def _get_european_equivalent_volatility_timeseries( dividends_ts = [()] * len(spot) # Price with CRR using American IVs in European mode + expiration_ts = to_datetime(expiration) + expiration_ts = expiration_ts.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation european_crr_price = vector_crr_binomial_pricing( S0=spot.values, K=[strike] * len(spot), - T=_prepare_time_to_expiration(spot.index, expiration), + T=_prepare_time_to_expiration(spot.index, expiration_ts), r=rates.values, sigma=crr_american_iv.values, dividend_yield=dividend_yield.values, @@ -615,7 +624,7 @@ def _get_european_equivalent_volatility_timeseries( european_equiv_iv = vector_bsm_iv_estimation( F=forward.values, K=[strike] * len(spot), - T=_prepare_time_to_expiration(spot.index, expiration), + T=_prepare_time_to_expiration(spot.index, expiration_ts), r=rates.values, market_price=european_crr_price, right=[right.lower()] * len(spot), diff --git a/trade/helpers/helper.py b/trade/helpers/helper.py index 6ac0898..1111c8d 100644 --- a/trade/helpers/helper.py +++ b/trade/helpers/helper.py @@ -676,7 +676,7 @@ def retrieve_timeseries( Returns: pd.DataFrame: DataFrame with OHLCV data and additional columns for split adjustments """ - if spot_type == "chain_price": + if spot_type == "chain_price" or spot_type == "chain_spot": df = retrieve_timeseries( tick, end=(change_to_last_busday(datetime.today()) + BDay(1)).strftime("%Y-%m-%d"), @@ -686,7 +686,10 @@ def retrieve_timeseries( ) 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["close"] = df["close"] * df["split_factor"] + df["open"] = df["open"] * df["split_factor"] + df["high"] = df["high"] * df["split_factor"] + df["low"] = df["low"] * df["split_factor"] df["cum_split_from_start"] = df["split_ratio"].cumprod() return df else: @@ -754,6 +757,7 @@ def query_data(start, end, tick, interval): 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", @@ -1026,15 +1030,17 @@ def time_distance_helper( ## 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]") + ## Convert to datetime and preserve intraday precision + start = np.array(to_datetime(start), dtype="datetime64[ns]") + end = np.array(to_datetime(end), dtype="datetime64[ns]") ## 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 @@ -1562,6 +1568,7 @@ def binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, di def generate_option_tick(symbol, right, exp, strike): + exp = to_datetime(exp).strftime("%Y-%m-%d") 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'" diff --git a/trade/optionlib/config/defaults.py b/trade/optionlib/config/defaults.py index 7441430..96c9445 100644 --- a/trade/optionlib/config/defaults.py +++ b/trade/optionlib/config/defaults.py @@ -1,4 +1,5 @@ from trade.helpers.Logging import setup_logger +from dbase.DataAPI.ThetaData.utils import THETA_FIRST_ALLOW_DATE from . import load_config from .ssvi.controller import get_global_config logger = setup_logger('trade.optionlib.config.defaults') @@ -9,7 +10,7 @@ DAILY_BASIS = config['DAILY_BASIS'] DIVIDEND_LOOKBACK_YEARS = config['DIVIDEND_FORECAST_LOOKBACK_YEARS'] DIVIDEND_LOOKFORWARD_YEARS = config['DIVIDEND_FORECAST_LOOKFORWARD_YEARS'] -OPTION_TIMESERIES_START_DATE = config['OPTION_TIMESERIES_START_DATE'] +OPTION_TIMESERIES_START_DATE = THETA_FIRST_ALLOW_DATE DIVIDEND_FORECAST_METHOD = config['DIVIDEND_FORECAST_METHOD'] VOL_EST_UPPER_BOUND= config['VOL_EST_UPPER_BOUND'] VOL_EST_LOWER_BOUND= config['VOL_EST_LOWER_BOUND'] diff --git a/trade/optionlib/pricing/binomial.py b/trade/optionlib/pricing/binomial.py index eb405d9..1c0b5bc 100644 --- a/trade/optionlib/pricing/binomial.py +++ b/trade/optionlib/pricing/binomial.py @@ -7,8 +7,9 @@ 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.helper import Scalar, to_datetime from trade.helpers.threads import runThreads +from trade import MARKET_CLOSE from ..utils.format import assert_equal_length from ..config.defaults import DAILY_BASIS from ..assets.forward import time_distance_helper @@ -433,7 +434,9 @@ def __init__( self.option_type = option_type self.start_date = start_date self.valuation_date = valuation_date - self.T = time_distance_helper(end=self.expiration, start=self.valuation_date or datetime.now()) + expiration_dt = to_datetime(expiration) + expiration_dt = expiration_dt.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation + self.T = time_distance_helper(end=expiration_dt, start=self.valuation_date or datetime.now()) self.american = american self.dt = self.T / self.N self.priced = False diff --git a/trade/optionlib/vol/ssvi/utils.py b/trade/optionlib/vol/ssvi/utils.py index 9834401..2ac88cd 100644 --- a/trade/optionlib/vol/ssvi/utils.py +++ b/trade/optionlib/vol/ssvi/utils.py @@ -7,7 +7,7 @@ ) from trade.helpers.helper import retrieve_timeseries, change_to_last_busday, time_distance_helper from trade.helpers.Logging import setup_logger -from trade.assets.rates import get_risk_free_rate_helper +from trade.assets.rates import get_risk_free_rate_helper_v2 from trade.optionlib.assets.forward import ( vectorized_market_forward_calc, ) @@ -355,4 +355,4 @@ def get_rates(date): float: The risk-free rate for the specified date. """ date = pd.to_datetime(date).strftime("%Y-%m-%d") - return get_risk_free_rate_helper()["annualized"][date] + return get_risk_free_rate_helper_v2()["annualized"][date] From a4be660427edd3ce155bf63a2575abe4be394bf3 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:29:38 -0400 Subject: [PATCH 06/81] Add position attribution workflow for EventDriven trades --- EventDriven/attribution.py | 503 ++++++++++++++++++++ EventDriven/attributor.py | 166 ------- trade/assets/calculate/xmultiply_attr.py | 189 +++----- trade/assets/calculate/xmultiply_attr_v2.py | 282 +++++++++++ 4 files changed, 856 insertions(+), 284 deletions(-) create mode 100644 EventDriven/attribution.py delete mode 100644 EventDriven/attributor.py create mode 100644 trade/assets/calculate/xmultiply_attr_v2.py diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py new file mode 100644 index 0000000..e24a8f0 --- /dev/null +++ b/EventDriven/attribution.py @@ -0,0 +1,503 @@ +"""Position attribution workflows for EventDriven backtests. + +Provides quantity normalization, option attribution loading, and position-level +PnL decomposition with trade-aware adjustments for fills, commissions, and +slippage across a trade lifecycle. + +Core Dataclasses: + QuantityTimeSeries: Daily quantity state and execution metadata. + BacktestPositionAttribution: Attribution output for a single trade. + +Core Functions: + _get_trade_quantity_time_series: Builds daily quantity and cost series. + create_position_attribution: Loads and combines leg-level attribution. + compute_position_attribution: Applies quantity and trade adjustments. + compute_backtest_position_attribution: End-to-end portfolio integration. + +Processing Flow: + 1. Build trade quantity time series from buy/sell ledgers. + 2. Load and aggregate leg-level option attribution by date. + 3. Scale greek attribution by position quantity. + 4. Apply open/close trade PnL adjustments and transaction costs. + 5. Return normalized daily attribution components. + +Usage: + >>> analyzer = PositionAttributionAnalyzer(portfolio) + >>> result = analyzer.analyze_trade(trade_id) + >>> daily_attr = result.attribution +""" + +from trade.helpers.helper import change_to_last_busday +from pandas.tseries.offsets import BDay +from typing import Callable +import pandas as pd +from dataclasses import dataclass +from functools import partial +from trade.datamanager.utils.date import DATE_HINT +from EventDriven.types import TradeID, SignalID +from trade.helpers.helper_types import FrozenValidated +from EventDriven.trade import Trade +from trade.assets.calculate.xmultiply_attr_v2 import load_option_pnl_data +from trade.assets.calculate.xmultiply_attr import load_option_pnl_data as load_option_pnl_data_v1 +from trade.helpers.Logging import setup_logger +from EventDriven.riskmanager.market_timeseries import BacktestTimeseries +from EventDriven.new_portfolio import OptionSignalPortfolio +from typing import Tuple, Dict +from tqdm import tqdm + +logger = setup_logger("EventDriven.attribution") + + +@dataclass(frozen=True) +class QuantityTimeSeries(FrozenValidated): + """Immutable time series of position quantity and associated trade metadata. + + Stores daily cumulative quantity, quantity changes, execution prices, commissions, + slippage, and the trade entry/exit dates for a single trade leg. + + Attributes: + tick: The ticker symbol of the asset. + trade_id: Unique identifier for the trade. + signal_id: Identifier of the signal that generated the trade. + daily_qty: Cumulative daily quantity indexed by business day. + quantity_change: Daily quantity change indexed by business day. + exec_price: Execution price per unit indexed by business day. + commission: Per-unit commission cost; defaults to a zero series. + slippage: Per-unit slippage cost; defaults to a zero series. + trade_entry: First fill date; defaults to the earliest date in daily_qty. + trade_exit: Last fill date; defaults to the latest date in daily_qty. + """ + + tick: str + trade_id: TradeID + signal_id: SignalID + daily_qty: pd.Series + quantity_change: pd.Series + exec_price: pd.Series + commission: pd.Series = None + slippage: pd.Series = None + trade_entry: DATE_HINT = None + trade_exit: DATE_HINT = None + + def __post_init__(self): + # Ensure that daily_qty, quantity_change, and exec_price have the same index + if not ( + self.daily_qty.index.equals(self.quantity_change.index) + and self.daily_qty.index.equals(self.exec_price.index) + ): + raise ValueError("daily_qty, quantity_change, and exec_price must have the same index") + + if self.commission is None: + self.commission = pd.Series(0, index=self.daily_qty.index) + if self.slippage is None: + self.slippage = pd.Series(0, index=self.daily_qty.index) + if self.trade_entry is None: + self.trade_entry = self.daily_qty.index.min() + if self.trade_exit is None: + self.trade_exit = self.daily_qty.index.max() + + def __repr__(self) -> str: + return f"QuantityTimeSeries(tick={self.tick}, trade_id={self.trade_id})" + + +@dataclass(frozen=True) +class BacktestPositionAttribution(FrozenValidated): + """Container for the computed attribution of a single backtest position. + + Attributes: + trade_id: Unique identifier for the trade. + signal_id: Identifier of the signal that generated the trade. + qty: The QuantityTimeSeries used to compute the attribution. + attribution: DataFrame of daily attribution components (see + :func:`compute_position_attribution` for column definitions). + """ + + trade_id: TradeID + signal_id: SignalID + qty: QuantityTimeSeries + attribution: pd.DataFrame + + def __repr__(self) -> str: + return f"BacktestPositionAttribution(trade_id={self.trade_id}, signal_id={self.signal_id})" + + +def _get_trade_quantity_time_series( + trade_id: str, + trade_obj: Trade, +) -> QuantityTimeSeries: + """Extract daily quantity and quantity change time series for a given trade. + + Args: + trade_id: The unique identifier for the trade. + trade_obj: The Trade object containing the buy and sell ledgers. + + Returns: + A QuantityTimeSeries containing the daily quantity, quantity change, + execution price, commission, and slippage time series. + """ + + ## Sample trade + sym = trade_obj.symbol + individual_trades = trade_obj.buy_ledger.ledger + trade_obj.sell_ledger.ledger + individual_trades_df = pd.DataFrame(individual_trades) + + ## Format the individual trades DataFrame for analysis + cols = [ + "datetime", + "quantity", + "price", + "per_unit_slippage", + "per_unit_commission", + "per_unit_market_value", + "direction", + ] + new_col = [ + "fill_ts", + "qty_change", + "fill_price", + "per_unit_slippage", + "per_unit_commission", + "per_unit_market_value", + "direction", + ] + individual_trades_df = individual_trades_df[cols] + individual_trades_df.columns = new_col + individual_trades_df["qty_change"] = individual_trades_df.apply( + lambda row: row["qty_change"] if row["direction"] == "BUY" else -abs(row["qty_change"]), axis=1 + ) + trade_entry = individual_trades_df["fill_ts"].min() + trade_exit = individual_trades_df["fill_ts"].max() + + ## Between entry and exit, extract daily quantity and quantiy change + date_range = pd.date_range(start=trade_entry, end=trade_exit, freq="B") + qty_frame = individual_trades_df.set_index("fill_ts").reindex(date_range).fillna(0) + qty_frame["qty_change"] = qty_frame["qty_change"].fillna(0) + qty_frame["cumulative_qty"] = qty_frame["qty_change"].cumsum() + + return QuantityTimeSeries( + tick=sym, + trade_id=trade_id, + signal_id=trade_obj.signal_id, + daily_qty=qty_frame["cumulative_qty"], + quantity_change=qty_frame["qty_change"], + + ## Scale everything to per-unit + exec_price=qty_frame["per_unit_market_value"] / 100, + commission=abs(qty_frame["per_unit_commission"].fillna(0)/100), + slippage=abs(qty_frame["per_unit_slippage"].fillna(0) / 100), + trade_entry=trade_entry, + trade_exit=trade_exit, + ) + + +def create_position_attribution( + trade_id: TradeID, entry_date: DATE_HINT, exit_date: DATE_HINT, v1: bool = False +) -> pd.DataFrame: + """Create a position attribution DataFrame for a given trade ID. + + Extracts the relevant option legs, loads market data, and calculates the + attribution for the position over the specified date range. + + Args: + trade_id: The TradeID for which to create the position attribution. + entry_date: The entry date of the trade (padded back 3 days for data loading). + exit_date: The exit date of the trade (padded forward 3 days for data loading). + v1: If True, uses the v1 attribution loader. Defaults to False. + + Returns: + A DataFrame containing the position attribution for the given trade ID. + """ + legs = trade_id.legs + attribution_frames = [] + entry_padding = pd.to_datetime(entry_date) - pd.Timedelta(days=3) + exit_padding = pd.to_datetime(exit_date) + pd.Timedelta(days=3) + for direction, opttick in legs: + if v1: + attribution = load_option_pnl_data_v1(yesterday=entry_padding, today=exit_padding, opttick=opttick) + else: + attribution = load_option_pnl_data(yesterday=entry_padding, today=exit_padding, opttick=opttick) + if direction == "S": + attribution.attribution *= -1 + attribution_frames.append(attribution.attribution) + combined_attribution = sum(attribution_frames) + return combined_attribution + + +def _get_position_price(market_data: BacktestTimeseries, _id: TradeID, date: DATE_HINT, force: bool = False) -> float: + """Get the position price for a given TradeID and date from the market data. + + Args: + market_data: The BacktestTimeseries containing the market data for the backtest. + _id: The TradeID for which to get the position price. + date: The date for which to get the position price. + force: If True, forces recalculation of the position price even if cached. + + Returns: + The position price for the given TradeID and date. + """ + return market_data.get_at_time_position_data(position_id=_id, date=date).get_price() + + +def compute_position_attribution( + trade_id: TradeID, + attribution: pd.DataFrame, + qty_ts: QuantityTimeSeries, + get_position_price_func: Callable[[TradeID, DATE_HINT, bool], float], +) -> pd.DataFrame: + """Compute position attribution adjusted for quantity changes and execution prices. + + Iterates over attribution dates, checks for quantity changes, and adjusts + attribution components accordingly. + + Args: + trade_id: The TradeID for which to compute the position attribution. + attribution: The DataFrame containing the initial attribution for the position. + qty_ts: The QuantityTimeSeries containing the daily quantity, quantity changes, + execution prices, and costs for the position. + get_position_price_func: Callable with signature + ``(trade_id: TradeID, date: DATE_HINT, force: bool) -> float`` used to + fetch the mark price for the position on a given date. + + Returns: + DataFrame with the following columns: + + - ``opt_dod_change``: Day-over-day change in option value from the attribution data. + - ``opt_plus_adj``: Sum of ``opt_dod_change`` and ``trade_pnl_adjustment``. + - ``total_pnl``: Total PnL after all adjustments. + - ``unexplained_pnl``: Residual PnL not explained by greeks or trade adjustments. + - ``trade_pnl_adjustment``: PnL adjustment for quantity-change days; zeroed on + full open/close to avoid double counting. + - ``commission_cost``: Commission cost for the quantity change on that day. + - ``slippage_cost``: Slippage cost for the quantity change on that day. + - ``delta_pnl``, ``gamma_pnl``, ``vega_pnl``, ``theta_pnl``, ``volga_pnl``, + ``vanna_pnl``: Greek PnL components scaled by daily quantity. + """ + + ## Extract series from qty_ts for easier access + daily_qty = qty_ts.daily_qty + quantity_change = qty_ts.quantity_change + exec_price = qty_ts.exec_price + attribution = attribution.copy() + commission = qty_ts.commission + slippage = qty_ts.slippage + + ## Ensure attribution has necessary columns, if not create them with default values + if "commission_cost" not in attribution.columns: + attribution["commission_cost"] = commission.fillna(0) + if "slippage_cost" not in attribution.columns: + attribution["slippage_cost"] = slippage.fillna(0) + if "trade_pnl_adjustment" not in attribution.columns: + attribution["trade_pnl_adjustment"] = 0.0 + if "total_pnl" not in attribution.columns: + attribution["total_pnl"] = attribution["opt_dod_change"] * daily_qty + + def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: + """Compute trade PnL for an open or close event on the given date. + + Args: + date: The date of the quantity change. + qty: The signed quantity change (positive for open, negative for close). + + Returns: + Tuple of ``(pnl, entry_price, close_price)``. + """ + if qty > 0: + # OPEN: entry is execution price on this date, close is current position price + entry_p = abs(exec_price.loc[date]) + close_p = get_position_price_func(_id=trade_id, date=date, force=True) + else: + # CLOSE: entry is previous day's position price, close is execution price on this date + prev_date = change_to_last_busday(date - BDay(1)) + entry_p = get_position_price_func(_id=trade_id, date=prev_date, force=True) + close_p = abs(exec_price.loc[date]) + pnl = (close_p - entry_p) * abs(qty) + return pnl, entry_p, close_p + + # iterate over attribution dates (stable, less overhead than iterrows) + for date in attribution.index: + # get quantities (use .get so missing dates default to 0) + qty_change = quantity_change.get(date, 0) + today_qty = daily_qty.get(date, 0) + + # scale attribution to today's quantity + attribution.loc[date, :] = attribution.loc[date, :] * today_qty + + # if no position at all today, zero all components and continue + if today_qty == 0 and qty_change == 0: + attribution.loc[date, :] = 0 + continue + + # if no change in quantity on this date, nothing else to do + if qty_change == 0: + continue + + # there is a quantity change: compute prev qty and flags + prev_qty = today_qty - qty_change + fully_closed = today_qty == 0 + just_opened = prev_qty == 0 + + # compute pnl for the open/close event + trade_pnl, entry_p, close_p = _compute_pnl_for_change(date, qty_change) + commission_cost = commission.get(date, 0) * abs(qty_change) + slippage_cost = slippage.get(date, 0) * abs(qty_change) + trade_pnl -= commission_cost + slippage_cost + + # if fully closed or just opened, zero other components on that date + if fully_closed or just_opened: + attribution.loc[date, :] = 0 + + # apply adjustments + attribution.loc[date, "trade_pnl_adjustment"] += trade_pnl + attribution.loc[date, "commission_cost"] += commission_cost + attribution.loc[date, "slippage_cost"] += slippage_cost + attribution.loc[date, "total_pnl"] += trade_pnl + logger.info( + f"Date: {date.date()}, Qty: {qty_change}, Entry: {entry_p}, Close: {close_p}, PnL: {trade_pnl}, PrevQty: {prev_qty}, Commission: {commission_cost}, Slippage: {slippage_cost}" + ) + + attribution["opt_plus_adj"] = attribution["opt_dod_change"] + attribution["trade_pnl_adjustment"] + attribution = attribution[ + [ + "opt_dod_change", + "opt_plus_adj", + "total_pnl", + "unexplained_pnl", + "trade_pnl_adjustment", + "commission_cost", + "slippage_cost", + "delta_pnl", + "gamma_pnl", + "vega_pnl", + "theta_pnl", + "volga_pnl", + "vanna_pnl", + ] + ] + + return attribution + + +def compute_backtest_position_attribution( + portfolio: OptionSignalPortfolio, + trade_id: TradeID, +) -> BacktestPositionAttribution: + """Compute position attribution for a given TradeID within a backtest portfolio. + + Retrieves the necessary trade and market data, creates the initial attribution, + and computes the adjusted position attribution. + + Args: + portfolio: The OptionSignalPortfolio containing the trades and market data. + trade_id: The TradeID for which to compute the position attribution. + + Returns: + A BacktestPositionAttribution containing the adjusted position attribution + for the given TradeID. + + Raises: + ValueError: If trade_id is not found in portfolio.trades_map. + """ + # Retrieve the trade object from the portfolio using the trade_id + trade_obj: Trade = portfolio.trades_map.get(trade_id) + if not trade_obj: + raise ValueError(f"TradeID {trade_id} not found in portfolio trades_map") + + # Extract quantity time series for the trade + qty_ts = _get_trade_quantity_time_series(trade_id, trade_obj) + + # Create initial attribution for the position) + trade_entry = qty_ts.trade_entry + trade_exit = qty_ts.trade_exit + attr = create_position_attribution(trade_id=trade_id, entry_date=trade_entry, exit_date=trade_exit, v1=False) + attr = attr.loc[trade_entry:trade_exit] + + # Make partial function for getting position price with market data from the portfolio's risk manager + get_price_func = partial(_get_position_price, market_data=portfolio.risk_manager.market_data, force=True) + + # Compute the adjusted position attribution based on the quantity time series and execution prices + computed_attr = compute_position_attribution( + trade_id=trade_id, attribution=attr, qty_ts=qty_ts, get_position_price_func=get_price_func + ) + return BacktestPositionAttribution( + trade_id=trade_id, signal_id=trade_obj.signal_id, qty=qty_ts, attribution=computed_attr + ) + + +class PositionAttributionAnalyzer: + """Analyzes position-level attribution for all trades in a backtest portfolio. + + Computes and caches BacktestPositionAttribution for each trade, and provides + utilities to aggregate results into DataFrames grouped by signal or trade. + """ + + def __init__(self, portfolio: OptionSignalPortfolio): + self.portfolio = portfolio + self.attribution_cache: Dict[TradeID, BacktestPositionAttribution] = {} + + def analyze_trade(self, trade_id: TradeID, force: bool = False) -> BacktestPositionAttribution: + """Analyze a specific trade by computing its position attribution. + + Args: + trade_id: The TradeID of the trade to analyze. + force: If True, forces re-computation even if the result is cached. + + Returns: + A BacktestPositionAttribution containing the attribution analysis + for the specified trade. + """ + if trade_id not in self.attribution_cache or force: + self.attribution_cache[trade_id] = compute_backtest_position_attribution(self.portfolio, trade_id) + return self.attribution_cache[trade_id] + + def analyze_all_trades(self, force: bool = False) -> Dict[TradeID, BacktestPositionAttribution]: + """Analyze all trades in the portfolio by computing their position attributions. + + Args: + force: If True, forces re-computation even if results are cached. + + Returns: + A dictionary mapping each TradeID to its BacktestPositionAttribution. + """ + for trade_id in tqdm(self.portfolio.trades_map.keys(), desc="Analyzing trades"): + if trade_id not in self.attribution_cache or force: + self.attribution_cache[trade_id] = compute_backtest_position_attribution(self.portfolio, trade_id) + return self.attribution_cache + + def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: bool = False) -> pd.DataFrame: + """Convert cached attributions to a grouped summary DataFrame. + + Args: + groupby: Column by which to group results. Must be ``"signal_id"`` or + ``"trade_id"``. + ignore_missing: If True, skips trades without computed attributions. + If False, raises an error for any missing trades. + + Returns: + A DataFrame with attribution columns summed and scaled by 100, grouped + by the specified column. + + Raises: + ValueError: If no attributions have been computed yet. + ValueError: If ``ignore_missing=False`` and any trades are missing attributions. + AssertionError: If ``groupby`` is not ``"signal_id"`` or ``"trade_id"``. + """ + assert groupby in ["signal_id", "trade_id"], "groupby must be either 'signal_id' or 'trade_id'" + if not self.attribution_cache: + raise ValueError("No attributions computed yet. Please run analyze_all_trades first.") + if not ignore_missing: + missing_trades = [ + trade_id for trade_id in self.portfolio.trades_map.keys() if trade_id not in self.attribution_cache + ] + if missing_trades: + raise ValueError(f"Missing attributions for TradeIDs: {missing_trades}") + records = [] + for attr in self.attribution_cache.values(): + df = attr.attribution.copy() + df["trade_id"] = attr.trade_id + df["signal_id"] = attr.signal_id + records.append(df) + combined_df = pd.concat(records) + if groupby == "signal_id": + return combined_df.drop(columns=["trade_id"]).groupby("signal_id").sum() * 100 + else: + return combined_df.drop(columns=["signal_id"]).groupby("trade_id").sum() * 100 diff --git a/EventDriven/attributor.py b/EventDriven/attributor.py deleted file mode 100644 index fbbe602..0000000 --- a/EventDriven/attributor.py +++ /dev/null @@ -1,166 +0,0 @@ -from trade.assets.OptionStructure import OptionStructure -from trade.assets.helpers.loaders import create_object_from_id -from trade.helpers.Context import Context -from trade.assets.Calculate import Calculate -import yfinance as yf -import pandas as pd -import numpy as np -from trade.assets.Stock import Stock -from pandas.tseries.offsets import BDay -from trade.assets.Option import Option -import threading -import time -from trade.helpers.Logging import setup_logger -from IPython.display import clear_output -logger = setup_logger('EventDriven.attributor') - - -class EVBAttributor: - """ - Class to load data from Event Driven Backtester and calculate PnL and Greeks - """ - def __init__(self, - trades: pd.DataFrame, - attribution_fill: str = 'default_fill', - option_fill: str = 'midpoint', - retries: int = 3): - """ - trades: DataFrame containing trades data from Event Driven Backtester - Eexpected columns: EntryTime, ExitTime, Quantity, Positions - retries: Number of retries for each trade - attribution_fill: Method to fill missing data in attribution calculation - option_fill: Method to fill missing data in option class. Default is midpoint - """ - assert 'EntryTime' in trades.columns, 'EntryTime column not found' - assert 'ExitTime' in trades.columns, 'ExitTime column not found' - assert 'Quantity' in trades.columns, 'Quantity column not found' - assert 'Positions' in trades.columns, 'Positions column not found' - - self.trades = trades - self.trades['Structure'] = None - self.retries = retries - self.stored_data = { - 'attribution': {}, - 'greeks': {}, - 'vol_slides': {}, - 'pct_spot_slides': {} - } - self.attribution_fill = attribution_fill - self.option_fill = option_fill - - def _create_object_from_id(self, *args ,**kwargs): - try: - return create_object_from_id(*args, **kwargs) - except Exception as e: - print(e) - print(kwargs, args) - return None - - def load_data(self, - attribution: bool = True, - greeks: bool = True, - attribution_method: str = 'RV', - print_output: bool = True) -> None: - - """ - Load data from trades DataFrame and calculate PnL and Greeks - - params: - attribution: bool - Calculate attribution data if True - greeks: bool - Calculate greeks data if True - attribution_method: str - Method to calculate attribution data. available methods are 'RV', 'GB' - print_output: bool - Print output if True - - return: None - use self.attribution and self.greeks to access the calculated data - """ - trades = self.trades - - date_range = pd.date_range(trades['EntryTime'].min(), trades['ExitTime'].max(), freq = 'B') - tries = 0 - failed = ['start'] ## Making sure the loop runs at least once - ## Load Data - while len(failed) > 0 : - if tries >= self.retries: - print(f"Retries exceeded for {failed}") - break - tries += 1 - for index in trades[trades.Structure.isna()].index: - clear_output(wait=True) - print(f"Starting {index}") if print_output else None - start = (pd.to_datetime(trades.loc[index]['EntryTime']) + BDay(1)).strftime('%Y-%m-%d') ## Calculate PnL from the day after trade entry. This replicates buy at close and sell at open - end = (pd.to_datetime(trades.loc[index]['ExitTime']) - BDay(1)).strftime('%Y-%m-%d') ## Temp fix for trades enddate issue - quantity = trades.loc[index]['Quantity'] - id = trades.loc[index]['Positions'] - structure = self._create_object_from_id(id, start, default_fill = self.option_fill) - - ## Create PnL and Greeks Data from structure object - if structure is not None: - if attribution: - pnl = Calculate.attribution(structure, start, end, method = attribution_method, replace = self.attribution_fill) * quantity - self.stored_data['attribution'][index] = pnl - if greeks: - greeks_data = structure.greeks('greek', ts_start = start, ts_end = end) * quantity * 100 - self.stored_data['greeks'][index] = greeks_data - else: - trades.loc[index, 'Structure'] = None - print(f"Failed to load {index}") if print_output else None - self.stored_data['attribution'][index] = 0 - self.stored_data['greeks'][index] = 0 - continue - - - - trades.loc[index, 'Structure'] = structure - print(f"Completed {index}") if print_output else None - - - ## Produce Empty DataFrames based on Date Range from Trades Entry to Exit - ## Columns off the DataFrames are based on the first successful attribution and greeks data - for ind in self.stored_data['attribution'].keys(): - if isinstance(self.stored_data['attribution'][ind], pd.DataFrame): - pnl_sample = self.stored_data['attribution'][ind].copy() - greeks_sample = self.stored_data['greeks'][ind].copy() - - attribution = pd.DataFrame(index = date_range, - - data = {x: [0] * len(date_range) for x in pnl_sample.columns}) - - pt_greeks = pd.DataFrame(index = date_range, - data = {x: [0] * len(date_range) for x in greeks_sample.columns}) - - pct_spot_slides = pd.DataFrame(index = date_range, - data = {x: [0] * len(date_range) for x in greeks_sample.columns}) - - vol_slides = pd.DataFrame(index = date_range, - data = {x: [0] * len(date_range) for x in greeks_sample.columns}) - - - ## Fill DataFrames with PnL and Greeks Data - ## This is done by adding the pnl and greeks data to the corresponding columns in the dataframes - failed = [] - for index, pnl in self.stored_data['attribution'].items(): - if not isinstance(pnl, pd.DataFrame): - print(f"{index} failed") if print_output else None - failed.append(index) - continue - days_mask = attribution.index.isin(pnl.index) - attribution.loc[days_mask, :] += pnl - attribution.fillna(0, inplace=True) - - for index, greeks in self.stored_data['greeks'].items(): - if not isinstance(greeks, pd.DataFrame): - print(f"{index} failed") if print_output else None - failed.append(index) - continue - days_mask = pt_greeks.index.isin(greeks.index) - pt_greeks.loc[days_mask, :] += greeks - pt_greeks.fillna(0, inplace=True) - - self.attribution = attribution - self.greeks = pt_greeks - self.pct_spot_slides = pct_spot_slides - self.vol_slides = vol_slides - - - diff --git a/trade/assets/calculate/xmultiply_attr.py b/trade/assets/calculate/xmultiply_attr.py index dbb84a5..ecd8f99 100644 --- a/trade/assets/calculate/xmultiply_attr.py +++ b/trade/assets/calculate/xmultiply_attr.py @@ -3,33 +3,24 @@ from pandas.tseries.offsets import BDay import pandas as pd from pydantic import validate_call, ConfigDict -from trade.assets.calculate.data_classes import ( - SymbolPayload, - OptionPnlPayload, - TradePnlInfo, - SYMBOL_PAYLOADS -) +from trade.assets.calculate.data_classes import SymbolPayload, OptionPnlPayload, TradePnlInfo, SYMBOL_PAYLOADS from trade.assets.calculate.enums import AttributionModel from trade.assets.calculate.adjustments import trade_pnl_adjustment -from trade.assets.rates import get_risk_free_rate_helper -from trade.helpers.helper import ( - retrieve_timeseries, - change_to_last_busday -) +from trade.assets.rates import get_risk_free_rate_helper_v2 +from trade.helpers.helper import retrieve_timeseries, change_to_last_busday from trade.helpers.Logging import setup_logger from trade.helpers.decorators import log_time from module_test.raw_code.DataManagers.DataManagers import OptionDataManager, set_skip_mysql_query -from trade.datamanager.timeseries import TimeseriesDataManager # noqa +from trade.datamanager.timeseries import TimeseriesDataManager # noqa ##TODO: Take this out once DataManagers has been optimized set_skip_mysql_query(True) -logger = setup_logger('trade.assets.calculate.xmultiply_attr') +logger = setup_logger("trade.assets.calculate.xmultiply_attr") + @log_time() @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) -def load_symbol_payload(symbol: str, - today: datetime, - yesterday: datetime) -> SymbolPayload: +def load_symbol_payload(symbol: str, today: datetime, yesterday: datetime) -> SymbolPayload: """ Load symbol payload data for a given symbol between yesterday and today. @@ -40,23 +31,16 @@ def load_symbol_payload(symbol: str, Returns: SymbolPayload: The loaded symbol data. """ - - spot_series = retrieve_timeseries( - tick=symbol, - start=yesterday - BDay(1), - end=today)['close'] - spot_series.name = 'spot' - - return SymbolPayload( - symbol=symbol, - datetime=today, - spot=spot_series - ) + + spot_series = retrieve_timeseries(tick=symbol, start=yesterday - BDay(1), end=today)["close"] + spot_series.name = "spot" + + return SymbolPayload(symbol=symbol, datetime=today, spot=spot_series) + @log_time() @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) -def load_rate_payload(today: datetime, - yesterday: datetime) -> SymbolPayload: +def load_rate_payload(today: datetime, yesterday: datetime) -> SymbolPayload: """ Load rate payload data for USD rates between yesterday and today. @@ -66,22 +50,16 @@ def load_rate_payload(today: datetime, Returns: SymbolPayload: The loaded rate data. """ - rates_series: pd.Series = get_risk_free_rate_helper()['annualized'] + rates_series: pd.Series = get_risk_free_rate_helper_v2()["annualized"] rates_series = rates_series[rates_series.index.date >= yesterday.date()] - rates_series.name = 'rates' + rates_series.name = "rates" - return SymbolPayload( - symbol='RATES_USD', - datetime=today, - spot=rates_series - ) + return SymbolPayload(symbol="RATES_USD", datetime=today, spot=rates_series) @log_time() @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) -def add_dod_change(payload: OptionPnlPayload, - yesterday: datetime, - today: datetime) -> OptionPnlPayload: +def add_dod_change(payload: OptionPnlPayload, yesterday: datetime, today: datetime) -> OptionPnlPayload: """ Add day-over-day change data to the option payload. Args: @@ -91,22 +69,22 @@ def add_dod_change(payload: OptionPnlPayload, Returns: OptionPnlPayload: The updated option payload with day-over-day change data. """ - ## Create DoD DataFrame - raw = pd.DataFrame(index = payload.spot.index) + ## Create DoD DataFrame + raw = pd.DataFrame(index=payload.spot.index) opt_change = payload.spot.diff() - opt_change.name = 'opt_change' + opt_change.name = "opt_change" raw = pd.concat([raw, opt_change], axis=1) vol_change = payload.vol.diff() - vol_change.name = 'vol_change' + vol_change.name = "vol_change" raw = pd.concat([raw, vol_change], axis=1) rates_change = payload.rates_payload.spot.diff() - rates_change.name = 'rates_change' + rates_change.name = "rates_change" raw = pd.concat([raw, rates_change], axis=1) asset_change = payload.asset_payload.spot.diff() - asset_change.name = 'asset_spot_change' + asset_change.name = "asset_spot_change" raw = pd.concat([raw, asset_change], axis=1) date_range_bool = (raw.index.date >= yesterday.date()) & (raw.index.date <= today.date()) @@ -119,12 +97,9 @@ def add_dod_change(payload: OptionPnlPayload, @log_time() @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) -def load_option_pnl_data(yesterday: datetime, - today: datetime, - *, - dm: OptionDataManager=None, - opttick: str=None) -> OptionPnlPayload: - +def load_option_pnl_data( + yesterday: datetime, today: datetime, *, dm: OptionDataManager = None, opttick: str = None +) -> OptionPnlPayload: """ Load option data for a given option data manager between yesterday and today. Args: @@ -141,55 +116,37 @@ def load_option_pnl_data(yesterday: datetime, ## Back up yesterday by 1BDAY to ensure inclusive data retrieval yesterday = change_to_last_busday(yesterday - BDay(1)) - + ## Query Vol Data - vol_req = dm.get_timeseries( - start=yesterday, - end=today, - type_='vol', - model='bs' - ) - vol_data = vol_req.post_processed_data['Midpoint_bs_iv'] - vol_data.name='vol' - + vol_req = dm.get_timeseries(start=yesterday, end=today, type_="vol", model="bs") + vol_data = vol_req.post_processed_data["Midpoint_bs_iv"] + vol_data.name = "vol" + ## Query Option Spot Data - spot_req = dm.get_timeseries( - start=yesterday, - end=today, - type_='spot' - ) - spot_data = spot_req.post_processed_data['Midpoint'] - spot_data.name='spot' - + spot_req = dm.get_timeseries(start=yesterday, end=today, type_="spot") + spot_data = spot_req.post_processed_data["Midpoint"] + spot_data.name = "spot" + ## Query Greeks Data - greeks_req = dm.get_timeseries( - start=yesterday, - end=today, - type_='greeks', - model='bs' - ) - greeks_cols = greeks_req.post_processed_data.columns.str.contains('Midpoint') + greeks_req = dm.get_timeseries(start=yesterday, end=today, type_="greeks", model="bs") + greeks_cols = greeks_req.post_processed_data.columns.str.contains("Midpoint") greeks_data = greeks_req.post_processed_data.loc[:, greeks_cols] - greeks_data.columns = [col.split('_')[-1] for col in greeks_data.columns] + greeks_data.columns = [col.split("_")[-1] for col in greeks_data.columns] ## Load Symbol Payload sym_payload = SYMBOL_PAYLOADS.get((dm.symbol, yesterday, today)) if sym_payload is None: - sym_payload = load_symbol_payload( - symbol=dm.symbol, - today=today, - yesterday=yesterday - ) + sym_payload = load_symbol_payload(symbol=dm.symbol, today=today, yesterday=yesterday) SYMBOL_PAYLOADS[(dm.symbol, yesterday, today)] = sym_payload - + ## Load Rates Payload - rates_payload = SYMBOL_PAYLOADS.get(('RATES_USD', yesterday, today)) + rates_payload = SYMBOL_PAYLOADS.get(("RATES_USD", yesterday, today)) if rates_payload is None: rates_payload = load_rate_payload( today=today, yesterday=yesterday, ) - SYMBOL_PAYLOADS[('RATES_USD', yesterday, today)] = rates_payload + SYMBOL_PAYLOADS[("RATES_USD", yesterday, today)] = rates_payload payload = OptionPnlPayload( opttick=dm.opttick, @@ -198,18 +155,18 @@ def load_option_pnl_data(yesterday: datetime, spot=spot_data, greeks=greeks_data, asset_payload=sym_payload, - rates_payload=rates_payload + rates_payload=rates_payload, ) payload.attribution_model = AttributionModel.xMULTIPLY payload = add_dod_change(payload, yesterday, today) return calculate_pnl_decomposition(payload) + @log_time() @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def calculate_pnl_decomposition( - payload: OptionPnlPayload, - trade_pnl_entries: Optional[List[TradePnlInfo]] = None + payload: OptionPnlPayload, trade_pnl_entries: Optional[List[TradePnlInfo]] = None ) -> OptionPnlPayload: """ Calculate the PnL decomposition for the given option payload. @@ -226,40 +183,39 @@ def calculate_pnl_decomposition( dod_change = payload.dod_change.copy() ## This serves as moving yesterday's greeks to today. In other to align with DoD changes - greeks['shifted_date'] = greeks.index.shift(1) - greeks = greeks.reset_index().set_index('shifted_date') - delta_pnl = (dod_change['asset_spot_change'] * greeks['delta']).dropna() - delta_pnl.name = 'delta_pnl' + greeks["shifted_date"] = greeks.index.shift(1) + greeks = greeks.reset_index().set_index("shifted_date") + delta_pnl = (dod_change["asset_spot_change"] * greeks["delta"]).dropna() + delta_pnl.name = "delta_pnl" - gamma_pnl = ((0.5 * dod_change['asset_spot_change'] **2) * greeks['gamma']).dropna() - gamma_pnl.name = 'gamma_pnl' + gamma_pnl = ((0.5 * dod_change["asset_spot_change"] ** 2) * greeks["gamma"]).dropna() + gamma_pnl.name = "gamma_pnl" ## Vega is per 1% vol change, hence the multiplication by 100 ## Currently using vol_change in decimal form (e.g., 0.01 for 1%) - vega_pnl = (dod_change['vol_change'] * greeks['vega'] * 100).dropna() - vega_pnl.name = 'vega_pnl' + vega_pnl = (dod_change["vol_change"] * greeks["vega"] * 100).dropna() + vega_pnl.name = "vega_pnl" - theta_pnl = (dod_change.index.to_series().diff().dt.days * greeks['theta']).dropna() - theta_pnl.name = 'theta_pnl' + theta_pnl = (dod_change.index.to_series().diff().dt.days * greeks["theta"]).dropna() + theta_pnl.name = "theta_pnl" - vanna_pnl = (dod_change['asset_spot_change'] * dod_change['vol_change'] * greeks['vanna'] * 100).dropna() - vanna_pnl.name = 'vanna_pnl' + vanna_pnl = (dod_change["asset_spot_change"] * dod_change["vol_change"] * greeks["vanna"] * 100).dropna() + vanna_pnl.name = "vanna_pnl" - volga_pnl = (0.5 * ((dod_change['vol_change'] **2) * 100) * greeks['volga']).dropna() - volga_pnl.name = 'volga_pnl' + volga_pnl = (0.5 * ((dod_change["vol_change"] ** 2) * 100) * greeks["volga"]).dropna() + volga_pnl.name = "volga_pnl" - rho_pnl = (dod_change['rates_change'] * greeks['rho'] * 100).dropna() - rho_pnl.name = 'rho_pnl' + rho_pnl = (dod_change["rates_change"] * greeks["rho"] * 100).dropna() + rho_pnl.name = "rho_pnl" total_pnl = delta_pnl + gamma_pnl + vega_pnl + theta_pnl + vanna_pnl + rho_pnl + volga_pnl - total_pnl.name = 'total_pnl_excl_trade_pnl' + total_pnl.name = "total_pnl_excl_trade_pnl" - unexplained_pnl = (dod_change['opt_change'] - total_pnl).dropna() - unexplained_pnl.name = 'unexplained_pnl' - - opt_change = dod_change['opt_change'].dropna() - opt_change.name = 'opt_dod_change' + unexplained_pnl = (dod_change["opt_change"] - total_pnl).dropna() + unexplained_pnl.name = "unexplained_pnl" + opt_change = dod_change["opt_change"].dropna() + opt_change.name = "opt_dod_change" all_pnl: pd.DataFrame = pd.concat( [ @@ -272,14 +228,11 @@ def calculate_pnl_decomposition( volga_pnl, total_pnl, unexplained_pnl, - opt_change + opt_change, ], - axis=1 - ) - all_pnl.index.name = 'date' - all_pnl = trade_pnl_adjustment( - attribution_table=all_pnl, - entry_info=trade_pnl_entries or [] + axis=1, ) + all_pnl.index.name = "date" + all_pnl = trade_pnl_adjustment(attribution_table=all_pnl, entry_info=trade_pnl_entries or []) payload.attribution = all_pnl return payload diff --git a/trade/assets/calculate/xmultiply_attr_v2.py b/trade/assets/calculate/xmultiply_attr_v2.py new file mode 100644 index 0000000..413ff18 --- /dev/null +++ b/trade/assets/calculate/xmultiply_attr_v2.py @@ -0,0 +1,282 @@ +from datetime import datetime +from typing import Optional, List +from pandas.tseries.offsets import BDay +import pandas as pd +from pydantic import validate_call, ConfigDict +from trade.assets.calculate.data_classes import SymbolPayload, OptionPnlPayload, TradePnlInfo, SYMBOL_PAYLOADS +from trade.assets.calculate.enums import AttributionModel +from trade.assets.calculate.adjustments import trade_pnl_adjustment +from trade.helpers.helper import ( + parse_option_tick, + retrieve_timeseries, # noqa + change_to_last_busday, +) +from trade.helpers.Logging import setup_logger +from trade.helpers.decorators import log_time +from module_test.raw_code.DataManagers.DataManagers import OptionDataManager, set_skip_mysql_query +from trade.datamanager.timeseries import TimeseriesDataManager # noqa + +##TODO: Take this out once DataManagers has been optimized +set_skip_mysql_query(True) +logger = setup_logger("trade.assets.calculate.xmultiply_attr") + + +def get_symbol_timeseries(symbol) -> TimeseriesDataManager: + """ + Get a timeseries data manager for a given symbol. + Args: + symbol (str): The asset symbol to retrieve data for. + Returns: + TimeseriesDataManager: The data manager for the symbol's timeseries data. + """ + return TimeseriesDataManager(symbol=symbol) + + +@log_time() +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def load_symbol_payload(symbol: str, today: datetime, yesterday: datetime) -> SymbolPayload: + """ + Load symbol payload data for a given symbol between yesterday + and today. + Args: + symbol (str): The asset symbol to load data for. + today (datetime): The end date for the data. + yesterday (datetime): The start date for the data. + Returns: + SymbolPayload: The loaded symbol data. + """ + + spot_series = get_symbol_timeseries(symbol).spot.get_timeseries(start_date=yesterday, end_date=today).timeseries + spot_series.name = "spot" + + return SymbolPayload(symbol=symbol, datetime=today, spot=spot_series) + + +@log_time() +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def load_rate_payload(today: datetime, yesterday: datetime) -> SymbolPayload: + """ + Load rate payload data for USD rates between yesterday + and today. + Args: + today (datetime): The end date for the data. + yesterday (datetime): The start date for the data. + Returns: + SymbolPayload: The loaded rate data. + """ + rates_series: pd.Series = ( + get_symbol_timeseries("RATES_USD").rates.get_timeseries(start_date=yesterday, end_date=today).timeseries + ) + rates_series = rates_series[rates_series.index.date >= yesterday.date()] + rates_series.name = "rates" + + return SymbolPayload(symbol="RATES_USD", datetime=today, spot=rates_series) + + +@log_time() +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def add_dod_change(payload: OptionPnlPayload, yesterday: datetime, today: datetime) -> OptionPnlPayload: + """ + Add day-over-day change data to the option payload. + Args: + payload (OptionPnlPayload): The option payload to add data to. + yesterday (datetime): The start date for the data. + today (datetime): The end date for the data. + Returns: + OptionPnlPayload: The updated option payload with day-over-day change data. + """ + ## Create DoD DataFrame + raw = pd.DataFrame(index=payload.spot.index) + opt_change = payload.spot.diff() + opt_change.name = "opt_change" + raw = pd.concat([raw, opt_change], axis=1) + + opt_spot = payload.spot + opt_spot.name = "opt_spot" + raw = pd.concat([raw, opt_spot], axis=1) + + vol_change = payload.vol.diff() + vol_change.name = "vol_change" + raw = pd.concat([raw, vol_change], axis=1) + + rates_change = payload.rates_payload.spot.diff() + rates_change.name = "rates_change" + raw = pd.concat([raw, rates_change], axis=1) + + asset_change = payload.asset_payload.spot.diff() + asset_change.name = "asset_spot_change" + raw = pd.concat([raw, asset_change], axis=1) + + date_range_bool = (raw.index.date >= yesterday.date()) & (raw.index.date <= today.date()) + raw = raw[date_range_bool] + + payload.dod_change = raw + + return payload + + +@log_time() +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def load_option_pnl_data( + yesterday: datetime, today: datetime, *, dm: OptionDataManager = None, opttick: str = None +) -> OptionPnlPayload: + """ + Load option data for a given option data manager between yesterday and today. + Args: + dm (OptionDataManager): The option data manager to load data from. + yesterday (datetime): The start date for the data. + today (datetime): The end date for the data. + Returns: + OptionPnlPayload: The loaded option data. + """ + if dm is None and opttick is None: + raise ValueError("Either 'dm' or 'opttick' must be provided.") + if dm is not None: + raise ValueError("Data manager input is currently not supported. Please provide 'opttick' directly.") + option_meta = parse_option_tick(opttick) + + ## Back up yesterday by 1BDAY to ensure inclusive data retrieval + yesterday = change_to_last_busday(yesterday - BDay(1)) + ts = get_symbol_timeseries(option_meta["ticker"]) + + ## Query Vol Data + vol_req = ts.vol.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + ) + vol_data = vol_req.timeseries + vol_data.name = "vol" + + ## Query Option Spot Data + spot_req = ts.option_spot.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + ) + spot_data = spot_req.price + spot_data.name = "spot" + + ## Query Greeks Data + greeks_req = ts.greeks.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + ) + greeks_data = greeks_req.timeseries + + ## Load Symbol Payload + sym_payload = SYMBOL_PAYLOADS.get((option_meta["ticker"], yesterday, today)) + if sym_payload is None: + sym_payload = load_symbol_payload(symbol=option_meta["ticker"], today=today, yesterday=yesterday) + SYMBOL_PAYLOADS[(option_meta["ticker"], yesterday, today)] = sym_payload + + ## Load Rates Payload + rates_payload = SYMBOL_PAYLOADS.get(("RATES_USD", yesterday, today)) + if rates_payload is None: + rates_payload = load_rate_payload( + today=today, + yesterday=yesterday, + ) + SYMBOL_PAYLOADS[("RATES_USD", yesterday, today)] = rates_payload + + payload = OptionPnlPayload( + opttick=opttick, + date=today, + vol=vol_data, + spot=spot_data, + greeks=greeks_data, + asset_payload=sym_payload, + rates_payload=rates_payload, + ) + payload.attribution_model = AttributionModel.xMULTIPLY + payload = add_dod_change(payload, yesterday, today) + + return calculate_pnl_decomposition(payload) + + +@log_time() +@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +def calculate_pnl_decomposition( + payload: OptionPnlPayload, trade_pnl_entries: Optional[List[TradePnlInfo]] = None +) -> OptionPnlPayload: + """ + Calculate the PnL decomposition for the given option payload. + Args: + payload (OptionPnlPayload): The option payload to calculate PnL decomposition for. + trade_pnl_adjustment (Optional[pd.Series]): Optional series to account for trade PnL adjustments. + This pnl is considering an entry into the position. It therefore adds an offset so the total pnl matches position pnl + while excluding trade pnl from the decomposition & totaling matches DoD option pnl change. + - Expecting index to be datetime, values to be spot. + Returns: + OptionPnlPayload: The updated option payload with PnL decomposition data. + """ + greeks = payload.greeks.copy() + dod_change = payload.dod_change.copy() + + ## This serves as moving yesterday's greeks to today. In other to align with DoD changes + greeks["shifted_date"] = greeks.index.to_series().shift(-1) + greeks = greeks.reset_index().set_index("shifted_date") + delta_pnl = (dod_change["asset_spot_change"] * greeks["delta"]).dropna() + delta_pnl.name = "delta_pnl" + + gamma_pnl = ((0.5 * dod_change["asset_spot_change"] ** 2) * greeks["gamma"]).dropna() + gamma_pnl.name = "gamma_pnl" + + ## Vega is per 1% vol change, hence the multiplication by 100 + ## Currently using vol_change in decimal form (e.g., 0.01 for 1%) + vega_pnl = (dod_change["vol_change"] * greeks["vega"] * 100).dropna() + vega_pnl.name = "vega_pnl" + + theta_pnl = (dod_change.index.to_series().diff().dt.days * greeks["theta"]).dropna() + theta_pnl.name = "theta_pnl" + + if "vanna" not in greeks.columns: + vanna_pnl = pd.Series( + dtype=float, data=0.0, index=vega_pnl.index + ) # Placeholder for vanna PnL, currently set to empty series + else: + vanna_pnl = (dod_change["asset_spot_change"] * dod_change["vol_change"] * greeks["vanna"] * 100).dropna() + vanna_pnl.name = "vanna_pnl" + + volga_pnl = (0.5 * ((dod_change["vol_change"] ** 2) * 100) * greeks["volga"]).dropna() + volga_pnl.name = "volga_pnl" + + rho_pnl = (dod_change["rates_change"] * greeks["rho"] * 100).dropna() + rho_pnl.name = "rho_pnl" + + + + opt_change = dod_change["opt_change"].dropna() + opt_change.name = "opt_dod_change" + + opt_spot = dod_change["opt_spot"].dropna() + opt_spot.name = "opt_spot" + + all_pnl: pd.DataFrame = pd.concat( + [ + delta_pnl, + gamma_pnl, + vega_pnl, + theta_pnl, + vanna_pnl, + rho_pnl, + volga_pnl, + opt_change, + opt_spot, + ], + axis=1, + ) + all_pnl.index.name = "date" + all_pnl.fillna(0, inplace=True) + all_pnl["total_pnl_excl_trade_pnl"] = all_pnl.drop(columns=["opt_dod_change", "opt_spot"]).sum(axis=1) + all_pnl["unexplained_pnl"] = all_pnl["opt_dod_change"] - all_pnl["total_pnl_excl_trade_pnl"] + all_pnl = trade_pnl_adjustment(attribution_table=all_pnl, entry_info=trade_pnl_entries or []) + payload.attribution = all_pnl + return payload From 3468ddeff36191692786e735b04d64bd77fdd313 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:30:01 -0400 Subject: [PATCH 07/81] Normalize EventDriven logging and supporting utilities --- EventDriven/configs/vars.py | 15 ++-- EventDriven/helpers.py | 30 ++++---- EventDriven/logging.py | 68 +++++++++++++++++++ EventDriven/performance.py | 23 ++++--- EventDriven/portfolio_utils.py | 4 ++ EventDriven/riskmanager/actions.py | 5 +- EventDriven/riskmanager/config.py | 3 + .../position/cogs/analyze_utils.py | 6 +- EventDriven/riskmanager/position/cogs/vars.py | 2 +- .../riskmanager/position/live_cogs/limits.py | 12 +++- .../position/live_cogs/save_utils.py | 2 +- EventDriven/riskmanager/price_execution.py | 2 +- EventDriven/riskmanager/sizer/_sizer.py | 24 +++---- EventDriven/riskmanager/sizer/_utils.py | 20 +++--- EventDriven/strategy.py | 60 ++++++++-------- ruff.toml | 2 +- 16 files changed, 188 insertions(+), 90 deletions(-) create mode 100644 EventDriven/logging.py diff --git a/EventDriven/configs/vars.py b/EventDriven/configs/vars.py index af9b6fd..d9589ce 100644 --- a/EventDriven/configs/vars.py +++ b/EventDriven/configs/vars.py @@ -1,4 +1,3 @@ - # Class-level descriptions for each config class CONFIG_CLASS_DESCRIPTIONS = { "BaseConfigs": "Base configuration class providing common functionality and run tracking for all configuration types.", @@ -19,7 +18,7 @@ "PositionAnalyzerConfig": "Configuration for the position analyzer orchestrating multiple cogs for comprehensive position analysis.", "PortfolioManagerConfig": "Configuration for portfolio management including weights haircut adjustments.", "BacktesterConfig": "Configuration for backtest execution including settlement delays and trade finalization.", - "RiskManagerConfig": "Configuration for the risk manager controlling slippage limits and order caching behavior.", + "RiskManagerConfig": "Configuration for the risk manager controlling order and analysis caching behavior.", "CashAllocatorConfig": "Threshold-based cash bucket allocator for symbols.", } @@ -134,20 +133,21 @@ "PortfolioManagerConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", "weights_haircut": "Haircut applied to position weights for conservative allocation (default 0.0).", - "t_plus_n": "Settlement delay for orders in business days (T+N, default 1).", + "roll_failed_orders": "Whether signals that fail to be processed should be rolled forward to the next available date (default True).", }, "BacktesterConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", "t_plus_n": "Settlement delay for orders in business days (T+N, default 1).", "finalize_trades": "Flag to enable finalization of trades at end of backtest (default False).", "raise_errors": "Flag to raise errors during backtest execution instead of logging them (default False).", + "min_slippage_pct": "Minimum slippage percentage applied to trade execution (default 0.075).", + "max_slippage_pct": "Maximum slippage percentage applied to trade execution (default 0.15).", }, "RiskManagerConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", - "max_slippage": "Maximum allowable slippage percentage for trade execution (default 0.25).", - "min_slippage": "Minimum allowable slippage percentage for trade execution (default 0.16).", "cache_orders": "Flag to enable caching of generated orders for reuse (default False).", "cache_position_analysis": "Flag to enable caching of position analysis results for performance optimization (default False).", + "cache_order_requests": "Flag to enable caching of order requests to avoid redundant order generation (default False).", }, "CashAllocatorConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -155,6 +155,7 @@ }, } + def get_config_class_description(class_name: str) -> str: """ Retrieve the class-level description for a given configuration class. @@ -167,6 +168,7 @@ def get_config_class_description(class_name: str) -> str: """ return CONFIG_CLASS_DESCRIPTIONS.get(class_name, "") + def get_class_config_descriptions(class_name: str) -> dict: """ Retrieve the configuration descriptions for a given class name. @@ -179,6 +181,7 @@ def get_class_config_descriptions(class_name: str) -> dict: """ return CONFIG_DEFINITIONS.get(class_name, {}) + def get_variable_in_class_config_description(class_name: str, config_name: str) -> str: """ Retrieve the description for a specific configuration of a given class. @@ -190,5 +193,5 @@ def get_variable_in_class_config_description(class_name: str, config_name: str) returns: str: The description of the configuration. """ - + return get_class_config_descriptions(class_name).get(config_name, None) diff --git a/EventDriven/helpers.py b/EventDriven/helpers.py index cb5e139..217d082 100644 --- a/EventDriven/helpers.py +++ b/EventDriven/helpers.py @@ -1,35 +1,39 @@ import pandas as pd from typing import Tuple from trade.helpers.helper import parse_option_tick +from trade.helpers.Logging import setup_logger -def generate_signal_id(underlier, - date, - signal_type): - signal_date = pd.to_datetime(date).strftime('%Y%m%d') +logger = setup_logger("EventDriven.helpers") + + +def generate_signal_id(underlier, date, signal_type): + signal_date = pd.to_datetime(date).strftime("%Y%m%d") key = underlier.upper() + signal_date + signal_type return key -def normalize_dollar_amount_to_decimal(price: float | int ) -> float | int: +def normalize_dollar_amount_to_decimal(price: float | int) -> float | int: """ divide by 100 """ return price / 100 -def normalize_dollar_amount( price: float | int) -> float | int : + +def normalize_dollar_amount(price: float | int) -> float | int: """ multiply by 100 """ return price * 100 + def parse_signal_id(id): - if 'SHORT' in id: - return dict(direction = id[-5:], date = pd.to_datetime(id[-13:-5]), ticker = id[:-13]) - elif 'LONG' in id: - return dict(direction = id[-4:], date = pd.to_datetime(id[-12:-4]), ticker = id[:-12]) + if "SHORT" in id: + return dict(direction=id[-5:], date=pd.to_datetime(id[-13:-5]), ticker=id[:-13]) + elif "LONG" in id: + return dict(direction=id[-4:], date=pd.to_datetime(id[-12:-4]), ticker=id[:-12]) else: - raise ValueError(f'Invalid signal id `{id}`, neither LONG nor SHORT was found in the id') - + 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 @@ -39,4 +43,4 @@ def parse_position_id(positionID: str) -> Tuple[dict, 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 + return position_dict, position_list diff --git a/EventDriven/logging.py b/EventDriven/logging.py new file mode 100644 index 0000000..c65f0ae --- /dev/null +++ b/EventDriven/logging.py @@ -0,0 +1,68 @@ +import logging + +from trade.helpers.Logging import setup_logger, find_loggers_by_pattern, change_logger_stream_level + +_BASE_LOGGING_LEVEL = "WARNING" +LOGGING_LEVEL = "WARNING" + +logger = setup_logger("EventDriven.logging", stream_log_level=LOGGING_LEVEL) + + +def get_logging_level() -> str: + """Return the current module-level logging level.""" + return LOGGING_LEVEL + + +def set_logging_level(level: str) -> None: + """Set the module-level logging level (does not propagate to loggers).""" + global LOGGING_LEVEL + LOGGING_LEVEL = level + + +def get_eventdriven_loggers() -> list[logging.Logger]: + """Retrieve all loggers under 'EventDriven'.""" + return find_loggers_by_pattern("EventDriven") + + +def change_all_eventdriven_loggers_level(level: str) -> None: + """Change the stream log level for every logger under 'EventDriven'.""" + int_level = getattr(logging, level.upper(), logging.WARNING) + for _logger in find_loggers_by_pattern("EventDriven"): + change_logger_stream_level(_logger, int_level) + + +def reset_base_logging_level() -> None: + """Reset LOGGING_LEVEL back to the package default ('WARNING').""" + global LOGGING_LEVEL + LOGGING_LEVEL = _BASE_LOGGING_LEVEL + + +def change_all_to_base_logging_level() -> None: + """Apply the current base LOGGING_LEVEL to all EventDriven loggers.""" + change_all_eventdriven_loggers_level(_BASE_LOGGING_LEVEL) + + +def change_specific_logger(name: str, level: str) -> None: + """Change the stream log level for a single EventDriven logger. + + Args: + name: Logger name, either fully-qualified (e.g. ``"EventDriven.types"``) + or short form (e.g. ``"types"``). If the name does not start with + ``"EventDriven."`` it is automatically prefixed. + level: Target log level string, e.g. ``"DEBUG"``, ``"WARNING"``. + + Raises: + ValueError: If no logger matching *name* has been registered yet. + """ + if not name.startswith("EventDriven."): + name = f"EventDriven.{name}" + + existing = find_loggers_by_pattern(name) + if not existing: + raise ValueError( + f"No logger named '{name}' has been registered. Ensure the corresponding module has been imported." + ) + + int_level = getattr(logging, level.upper(), logging.WARNING) + for _logger in existing: + change_logger_stream_level(_logger, int_level) diff --git a/EventDriven/performance.py b/EventDriven/performance.py index 5c3ad13..06ff15c 100644 --- a/EventDriven/performance.py +++ b/EventDriven/performance.py @@ -1,9 +1,13 @@ import numpy as np import pandas as pd +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.performance") + def create_sharpe_ratio(returns, periods=252): """ - Create the Sharpe ratio for the strategy, based on a + Create the Sharpe ratio for the strategy, based on a benchmark of zero (i.e. no risk-free rate information). Parameters: @@ -13,11 +17,10 @@ def create_sharpe_ratio(returns, periods=252): return np.sqrt(periods) * (np.mean(returns)) / np.std(returns) - def create_drawdowns(equity_curve): """ Calculate the largest peak-to-trough drawdown of the PnL curve - as well as the duration of the drawdown. Requires that the + as well as the duration of the drawdown. Requires that the pnl_returns is a pandas Series. Parameters: @@ -27,18 +30,18 @@ def create_drawdowns(equity_curve): drawdown, duration - Highest peak-to-trough drawdown and duration. """ - # Calculate the cumulative returns curve + # Calculate the cumulative returns curve # and set up the High Water Mark # Then create the drawdown and duration series hwm = [0] eq_idx = equity_curve.index - drawdown = pd.Series(index = eq_idx, dtype=float) - duration = pd.Series(index = eq_idx, dtype=float) + drawdown = pd.Series(index=eq_idx, dtype=float) + duration = pd.Series(index=eq_idx, dtype=float) # Loop over the index range for t in range(1, len(eq_idx)): - cur_hwm = max(hwm[t-1], equity_curve[t]) + cur_hwm = max(hwm[t - 1], equity_curve[t]) hwm.append(cur_hwm) - drawdown[t]= hwm[t] - equity_curve[t] - duration[t]= 0 if drawdown[t] == 0 else duration[t-1] + 1 - return drawdown.max(), duration.max() \ No newline at end of file + drawdown[t] = hwm[t] - equity_curve[t] + duration[t] = 0 if drawdown[t] == 0 else duration[t - 1] + 1 + return drawdown.max(), duration.max() diff --git a/EventDriven/portfolio_utils.py b/EventDriven/portfolio_utils.py index 766fffb..6d2a17a 100644 --- a/EventDriven/portfolio_utils.py +++ b/EventDriven/portfolio_utils.py @@ -4,6 +4,9 @@ from EventDriven.types import PositionsDict, EventTypes from EventDriven.dataclasses.states import PositionState from EventDriven.exceptions import BacktestNotImplementedError +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.portfolio_utils") ORDER_TYPES = [ EventTypes.OPEN, @@ -23,6 +26,7 @@ EventTypes.EXERCISE, ] + def extract_events( actionables: List[PositionState], current_positions: Dict[str, Dict[str, PositionsDict]] ) -> List[Union[OrderEvent, RollEvent]]: diff --git a/EventDriven/riskmanager/actions.py b/EventDriven/riskmanager/actions.py index dcfedf8..62b3d4e 100644 --- a/EventDriven/riskmanager/actions.py +++ b/EventDriven/riskmanager/actions.py @@ -76,6 +76,9 @@ from datetime import datetime from EventDriven.types import EventTypes from typing_extensions import TypedDict, Union +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.actions") class Changes(TypedDict): @@ -142,7 +145,7 @@ def __init__(self, trade_id: str, action: action_hint = None): self.reason = None def __repr__(self): - return f'ADJUST({self.trade_id}, Quantity Change: {self.action["quantity_diff"]}), Reason: {self.reason})' + return f"ADJUST({self.trade_id}, Quantity Change: {self.action['quantity_diff']}), Reason: {self.reason})" class EXERCISE(RMAction): diff --git a/EventDriven/riskmanager/config.py b/EventDriven/riskmanager/config.py index 660b873..948b172 100644 --- a/EventDriven/riskmanager/config.py +++ b/EventDriven/riskmanager/config.py @@ -105,6 +105,9 @@ import numbers import pandas as pd import numpy as np +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.config") def assert_missing_keys(config: dict) -> None: diff --git a/EventDriven/riskmanager/position/cogs/analyze_utils.py b/EventDriven/riskmanager/position/cogs/analyze_utils.py index 1e9250f..18fcbe4 100644 --- a/EventDriven/riskmanager/position/cogs/analyze_utils.py +++ b/EventDriven/riskmanager/position/cogs/analyze_utils.py @@ -342,7 +342,7 @@ def analyze_position( if dte - t_plus_n <= 0: # Inline the check for performance action = EXERCISE(trade_id=trade_id, action=Changes(quantity_diff=qty, new_quantity=0)) action.reason = f"position is expiring (DTE: {dte} <= {t_plus_n})" - logger.debug(f"EXERCISE action for {trade_id} due to expiration.") + logger.debug(f"EXERCISE action for {trade_id} due to expiration. DTE: {dte}, threshold: {t_plus_n}") return action ## P2: ROLL Checks (second priority - return immediately if triggered) @@ -351,7 +351,7 @@ def analyze_position( if dte < dte_limit: # Inline the check for performance action = ROLL(trade_id=trade_id, action=Changes(quantity_diff=0, new_quantity=qty)) action.reason = f"not enough DTE ({dte} < {dte_limit})" - logger.debug(f"ROLL action for {trade_id} due to DTE check.") + logger.debug(f"ROLL action for {trade_id} due to DTE check. DTE: {dte}, threshold: {dte_limit}") return action # Moneyness Check @@ -360,7 +360,7 @@ def analyze_position( m = min(moneyness_list, key=abs) action = ROLL(trade_id=trade_id, action=Changes(quantity_diff=0, new_quantity=qty)) action.reason = f"position is too ITM ({m} exceeds {moneyness_limit})" - logger.debug(f"ROLL action for {trade_id} due to moneyness check.") + logger.debug(f"ROLL action for {trade_id} due to moneyness check. Moneyness values: {moneyness_list}, threshold: {moneyness_limit}") return action ## P3: ADJUST Checks (lowest actionable priority) diff --git a/EventDriven/riskmanager/position/cogs/vars.py b/EventDriven/riskmanager/position/cogs/vars.py index 3860418..635489a 100644 --- a/EventDriven/riskmanager/position/cogs/vars.py +++ b/EventDriven/riskmanager/position/cogs/vars.py @@ -123,7 +123,7 @@ from dbase.database.SQLHelpers import DatabaseAdapter -logger = setup_logger("algo.positions.analyze") +logger = setup_logger("EventDriven.riskmanager.position.cogs.vars") db = DatabaseAdapter() MEASURES = ("delta", "gamma", "vega", "theta") MEASURES_SET = frozenset(MEASURES) # O(1) lookup for filtering performance diff --git a/EventDriven/riskmanager/position/live_cogs/limits.py b/EventDriven/riskmanager/position/live_cogs/limits.py index 58e7266..7276d3b 100644 --- a/EventDriven/riskmanager/position/live_cogs/limits.py +++ b/EventDriven/riskmanager/position/live_cogs/limits.py @@ -217,14 +217,21 @@ from EventDriven.configs.core import LimitsEnabledConfig, BaseSizerConfigs from .save_utils import get_position_limit, store_position_limits from ..cogs.vars import MEASURES_SET +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.position.live_cogs.limits") + def enable_storing_to_db(enable: bool = True): """Utility to enable or disable database storage of limits globally for testing.""" LiveCOGLimitsAndSizingCog.SAVE_LIMITS_TO_DB = enable + def reset_storing_to_db(): """Utility to reset database storage of limits to default (enabled).""" LiveCOGLimitsAndSizingCog.SAVE_LIMITS_TO_DB = True + + class LiveCOGLimitsAndSizingCog(LimitsAndSizingCog): """ Live trading specialized limits cog with database persistence. @@ -244,6 +251,7 @@ class LiveCOGLimitsAndSizingCog(LimitsAndSizingCog): state must persist across restarts and limit history must be maintained for compliance and analysis. """ + SAVE_LIMITS_TO_DB: bool = True def __init__( @@ -262,7 +270,7 @@ def _save_position_limits(self, trade_id: str, signal_id: str, limits: PositionL if not self.SAVE_LIMITS_TO_DB: self.position_limits[trade_id] = limits return - + store_position_limits( delta_limit=limits.delta, gamma_limit=limits.gamma, @@ -280,7 +288,7 @@ def _get_position_limits(self, trade_id: str, signal_id: str) -> PositionLimits: """ if trade_id in self.position_limits: return self.position_limits[trade_id] - + lm = PositionLimits() for risk_measure in MEASURES_SET: date, limit_value = get_position_limit( diff --git a/EventDriven/riskmanager/position/live_cogs/save_utils.py b/EventDriven/riskmanager/position/live_cogs/save_utils.py index e19091d..4b15daf 100644 --- a/EventDriven/riskmanager/position/live_cogs/save_utils.py +++ b/EventDriven/riskmanager/position/live_cogs/save_utils.py @@ -149,7 +149,7 @@ from dbase.database.SQLHelpers import DatabaseAdapter, dynamic_batch_update, get_engine from EventDriven.riskmanager.position.cogs.vars import MEASURES_SET -logger = setup_logger("algo.positions.limits.save_utils") +logger = setup_logger("EventDriven.riskmanager.position.live_cogs.save_utils") LIMITS_DF = None _ACCESS_COUNTER = 0 db = DatabaseAdapter() diff --git a/EventDriven/riskmanager/price_execution.py b/EventDriven/riskmanager/price_execution.py index cb88d3a..b7543c0 100644 --- a/EventDriven/riskmanager/price_execution.py +++ b/EventDriven/riskmanager/price_execution.py @@ -147,7 +147,7 @@ from trade.helpers.Logging import setup_logger from trade.backtester_._types import Side, SideInt # noqa -logger = setup_logger("algo.strategies.fill_optimizer") +logger = setup_logger("EventDriven.riskmanager.price_execution") ALPHA_LEVELS = { 1: 0.25, # very passive diff --git a/EventDriven/riskmanager/sizer/_sizer.py b/EventDriven/riskmanager/sizer/_sizer.py index 49c2857..dbd7050 100644 --- a/EventDriven/riskmanager/sizer/_sizer.py +++ b/EventDriven/riskmanager/sizer/_sizer.py @@ -230,7 +230,7 @@ from EventDriven.new_portfolio import OptionSignalPortfolio -logger = setup_logger("QuantTools.EventDriven.riskmanager.sizer") +logger = setup_logger("EventDriven.riskmanager.sizer._sizer") BASE = Path(os.environ["WORK_DIR"]) / ".riskmanager_cache" @@ -564,7 +564,7 @@ def calculate_position_size( ] ## 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))}" + 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)) @@ -643,12 +643,12 @@ def __init__( ## Ensure underlier_list is provided if pm and rm are not provided if self._unavailable: - assert ( - "underlier_list" in kwargs - ), "underlier_list must be provided in kwargs when pm and rm are not provided." - assert ( - isinstance(kwargs["underlier_list"], list) and len(kwargs["underlier_list"]) > 0 - ), "underlier_list must be a non-empty list." + assert "underlier_list" in kwargs, ( + "underlier_list must be provided in kwargs when pm and rm are not provided." + ) + assert isinstance(kwargs["underlier_list"], list) and len(kwargs["underlier_list"]) > 0, ( + "underlier_list must be a non-empty list." + ) self.underlier_list = kwargs["underlier_list"] else: logger.critical( @@ -700,9 +700,9 @@ def __rvol_window_assert(self, vol_type: str, rvol_window: int | tuple = None) - elif isinstance( rvol_window, tuple ): ## If rvol_window is a tuple, it must be of length 3 and vol_type must be 'weighted_mean' or 'mean' - assert ( - vol_type == "weighted_mean" or vol_type == "mean" - ), "rvol_window must be a tuple only when vol_type is 'weighted_mean' or 'mean'." + assert vol_type == "weighted_mean" or vol_type == "mean", ( + "rvol_window must be a tuple only when vol_type is 'weighted_mean' or 'mean'." + ) assert len(rvol_window) == 3, "rvol_window must be a tuple of length 3 for weighted mean calculation." return rvol_window @@ -882,7 +882,7 @@ def calculate_position_size( ] ## 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))}" + 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)) diff --git a/EventDriven/riskmanager/sizer/_utils.py b/EventDriven/riskmanager/sizer/_utils.py index 0a2e620..2149ecc 100644 --- a/EventDriven/riskmanager/sizer/_utils.py +++ b/EventDriven/riskmanager/sizer/_utils.py @@ -167,9 +167,12 @@ from dataclasses import dataclass, field from typing import Literal, ClassVar import pandas as pd -from EventDriven.riskmanager.utils import logger +from trade.helpers.Logging import setup_logger from ..._vars import Y2_LAGGED_START_DATE from trade.datamanager.market_data import get_timeseries_obj +import numpy as np + +logger = setup_logger("EventDriven.riskmanager.sizer._utils") def default_delta_limit( @@ -283,7 +286,10 @@ def scaler(realized_vol_series: pd.Series, factor: float, window: int) -> pd.Ser rolling_mean = realized_vol_series.rolling(window=window).mean() rolling_std = realized_vol_series.rolling(window=window).std() z = (realized_vol_series - rolling_mean) / rolling_std - return factor / (1 + z.abs()) + # return factor / (1 + z.abs()) + # return factor / (1 + np.exp(z)) + scaler = np.clip(factor / (1 + np.exp(z)), 0.5, factor) + return scaler @dataclass @@ -340,9 +346,9 @@ def __rvol_window_assert(self, vol_type: str, rvol_window: int | tuple = None) - elif isinstance( rvol_window, tuple ): ## If rvol_window is a tuple, it must be of length 3 and vol_type must be 'weighted_mean' or 'mean' - assert ( - vol_type == "weighted_mean" or vol_type == "mean" - ), "rvol_window must be a tuple only when vol_type is 'weighted_mean' or 'mean'." + assert vol_type == "weighted_mean" or vol_type == "mean", ( + "rvol_window must be a tuple only when vol_type is 'weighted_mean' or 'mean'." + ) assert len(rvol_window) == 3, "rvol_window must be a tuple of length 3 for weighted mean calculation." return rvol_window @@ -397,9 +403,7 @@ 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() - ) + timeseries.load_timeseries(sym=sym, start_date=Y2_LAGGED_START_DATE, end_date=datetime.now()) ts = timeseries.get_timeseries(sym=sym).spot["close"] if self.vol_type == "window": diff --git a/EventDriven/strategy.py b/EventDriven/strategy.py index 42af302..86a6b5a 100644 --- a/EventDriven/strategy.py +++ b/EventDriven/strategy.py @@ -5,13 +5,14 @@ from EventDriven.event import SignalEvent from trade.helpers.Logging import setup_logger + class Strategy(object): """ Strategy is an abstract base class providing an interface for all subsequent (inherited) strategy handling objects. The goal of a (derived) Strategy object is to generate Signal - objects for particular symbols based on the inputs of Bars + objects for particular symbols based on the inputs of Bars (OLHCVI) generated by a DataHandler object. This is designed to work both with historic and live data as @@ -27,13 +28,11 @@ def calculate_signals(self): Provides the mechanisms to calculate the list of signals. """ raise NotImplementedError("Should implement calculate_signals()") - - - + class BuyAndHoldStrategy(Strategy): """ - This is an extremely simple strategy that goes LONG all of the + This is an extremely simple strategy that goes LONG all of the symbols as soon as a bar is received. It will never exit a position. It is primarily used as a testing mechanism for the Strategy class @@ -54,7 +53,7 @@ def __init__(self, bars, events): # Once buy & hold signal is given, these are set to True self.bought = self._calculate_initial_bought() - + def _calculate_initial_bought(self): """ Adds keys to the bought dictionary for all symbols @@ -68,62 +67,61 @@ def _calculate_initial_bought(self): def calculate_signals(self, event): """ For "Buy and Hold" we generate a single signal per symbol - and then no additional signals. This means we are + and then no additional signals. This means we are constantly long the market from the date of strategy initialisation. Parameters - event - A MarketEvent object. + event - A MarketEvent object. """ - if event.type == 'MARKET': + if event.type == "MARKET": for s in self.symbol_list: bars = self.bars.get_latest_bars(s, N=1) if bars is not None and bars != []: - if self.bought[s] == False: + if self.bought[s] == False: # noqa # (Symbol, Datetime, Type = LONG, SHORT or EXIT) - signal = SignalEvent(bars[0][0], bars[0][1], 'LONG') + signal = SignalEvent(bars[0][0], bars[0][1], "LONG") self.events.put(signal) self.bought[s] = True - - - -class OptionSignalStrategy(Strategy): + + +class OptionSignalStrategy(Strategy): """ Ths is a simple strategy that generates a signal to buy or sell options based on data from the data handler. """ - + def __init__(self, bars, events): self.bars = bars self.events = events self.symbol_list = self.bars.symbol_list - self.logger = setup_logger('OptionSignalStrategy') + self.logger = setup_logger("EventDriven.strategy") - def calculate_signals(self): + def calculate_signals(self): """ - For signal reading, we read a dataframe row for 1, -1 or 0 to signify the following: - 1: Buy option - -1: Sell option - 0: Do nothing + For signal reading, we read a dataframe row for 1, -1 or 0 to signify the following: + 1: Buy option + -1: Sell option + 0: Do nothing """ - self.__latest_signals = self.bars.get_latest_bars('') #this returns a dataframe with the latest signals for each underlier + self.__latest_signals = self.bars.get_latest_bars( + "" + ) # this returns a dataframe with the latest signals for each underlier latest_signals_row = self.__latest_signals.iloc[0] - date = latest_signals_row['Date'] - + date = latest_signals_row["Date"] + for underlier in self.symbol_list: signal_value = latest_signals_row[underlier] if signal_value == 1: - signal = SignalEvent(underlier, date, 'LONG', generate_signal_id(underlier, date, 'LONG')) + signal = SignalEvent(underlier, date, "LONG", generate_signal_id(underlier, date, "LONG")) self.logger.info(f"LONG Signal for {underlier} at {date}") self.events.put(signal) elif signal_value == -1: - signal = SignalEvent(underlier, date, 'CLOSE') + signal = SignalEvent(underlier, date, "CLOSE") self.logger.info(f"CLOSE Signal for {underlier} at {date}") self.events.put(signal) elif signal_value == 2: - signal = SignalEvent(underlier, date, 'SHORT', generate_signal_id(underlier, date, 'SHORT')) + signal = SignalEvent(underlier, date, "SHORT", generate_signal_id(underlier, date, "SHORT")) self.logger.info(f"SHORT Signal for {underlier} at {date}") self.events.put(signal) - else: + else: self.logger.info(f"No signal for {underlier} at {date}") - - \ No newline at end of file diff --git a/ruff.toml b/ruff.toml index 307ac75..89b2762 100644 --- a/ruff.toml +++ b/ruff.toml @@ -2,4 +2,4 @@ line-length = 120 [lint] extend-select = ["B", "I"] -ignore = ["E501", "I001", "E731", "B009", "E722"] \ No newline at end of file +ignore = ["E501", "I001", "E731", "B009", "E722", "F541", "B905"] \ No newline at end of file From 2b9f8d226ff6e4fcc7406538707a5e9834ea920e Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:30:08 -0400 Subject: [PATCH 08/81] Improve backtester strategy initialization and aggregation --- trade/backtester_/_multi_asset_strategy.py | 55 ++++++++++++++++++++-- trade/backtester_/_strategy.py | 11 +++-- trade/backtester_/backtester_.py | 33 ------------- trade/backtester_/utils/aggregators.py | 13 ++--- 4 files changed, 64 insertions(+), 48 deletions(-) diff --git a/trade/backtester_/_multi_asset_strategy.py b/trade/backtester_/_multi_asset_strategy.py index 05eecb1..1833a27 100644 --- a/trade/backtester_/_multi_asset_strategy.py +++ b/trade/backtester_/_multi_asset_strategy.py @@ -5,6 +5,36 @@ from trade.backtester_._strategy import StrategyBase, TradeDecision from trade.backtester_.backtester_ import PTDataset +def _setup_strategy( + strategy_class: Type[StrategyBase], + data: PTDataset, + start_date: str, + ticker: str, + params: Dict[str, Any] +) -> StrategyBase: + """ + Utility function to initialize a strategy instance with the given parameters. + + Args: + strategy_class (Type[StrategyBase]): The StrategyBase subclass to instantiate + data (PTDataset): The dataset for the strategy + start_date (str): The starting date for the strategy in 'YYYY-MM-DD' format + ticker (str): The ticker symbol for the strategy + params (Dict[str, Any]): Additional parameters to pass to the strategy's __init__ + + Returns: + StrategyBase: An initialized instance of the specified strategy class + """ + default_params = strategy_class.bt_params + + ## Add default params to params if key is missing + for key, value in default_params.items(): + if key not in params: + params[key] = value + + return strategy_class( + data=data, start_trading_date=start_date, ticker=ticker, **params +) @dataclass class SimulationResults: @@ -117,14 +147,31 @@ def __post_init__(self): for ticker, ticker_params in self.params.items(): if ticker not in self.data: raise ValueError(f"Data not provided for ticker: {ticker}") - - # Initialize strategy with data, start_date, and ticker-specific params - self.asset_strategies[ticker] = self.strategy_class( - data=self.data[ticker], start_trading_date=self.start_date, ticker=ticker, **ticker_params + + self.asset_strategies[ticker] = _setup_strategy( + strategy_class=self.strategy_class, + data=self.data[ticker], + start_date=self.start_date, + ticker=ticker, + params=ticker_params ) self.current_open_positions[ticker] = False self.strategy_settings[ticker] = ticker_params + # default_params = self.strategy_class.bt_params + + # ## Add default params to ticker_params if key is missing + # for key, value in default_params.items(): + # if key not in ticker_params: + # ticker_params[key] = value + + # # Initialize strategy with data, start_date, and ticker-specific params + # self.asset_strategies[ticker] = self.strategy_class( + # data=self.data[ticker], start_trading_date=self.start_date, ticker=ticker, **ticker_params + # ) + # self.current_open_positions[ticker] = False + # self.strategy_settings[ticker] = ticker_params + def reset_strategies(self): """ Reset all strategy instances to their initial state. diff --git a/trade/backtester_/_strategy.py b/trade/backtester_/_strategy.py index a3b4bdf..2c43907 100644 --- a/trade/backtester_/_strategy.py +++ b/trade/backtester_/_strategy.py @@ -61,7 +61,7 @@ def __post_init__(self): if self.side not in (1, -1, 0): raise ValueError("TradeDecision.side must be 1 (long), -1 (short), or 0 (no position).") if (self.signal_id is not None and not isinstance(self.signal_id, SignalID)) and self.signal_id != "N/A": - raise TypeError("TradeDecision.signal_id must be of type SignalID, 'N/A', or None.") + raise TypeError("TradeDecision.signal_id must be of type SignalID, 'N/A', or None. Received type: {}".format(type(self.signal_id))) if self.pos_effect is not None and not isinstance(self.pos_effect, PositionEffect): raise TypeError("TradeDecision.pos_effect must be of type PositionEffect or None.") @@ -210,6 +210,7 @@ def __init_subclass__(cls, **kwargs): "data", "start_trading_date", "ticker", + "tplusn", ] is_abstract = inspect.isabstract(cls) @@ -234,12 +235,12 @@ def __init_subclass__(cls, **kwargs): raise TypeError(f"{cls.__name__}.__init__ must accept parameter '{req}'.") # If __init__ has **kwargs, accept anything; still enforce bt_params existence/type. - has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()) - if has_kwargs: - return + # has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()) + # if has_kwargs: + # return for k in cls.bt_params.keys(): - if k not in params: + if k not in params and k not in must_have_in_init: raise TypeError( f"{cls.__name__}.bt_params includes '{k}', but {cls.__name__}.__init__ " f"does not accept '{k}' (and has no **kwargs)." diff --git a/trade/backtester_/backtester_.py b/trade/backtester_/backtester_.py index 09796dd..3dc9b48 100644 --- a/trade/backtester_/backtester_.py +++ b/trade/backtester_/backtester_.py @@ -258,39 +258,6 @@ def pf_value_ts(self) -> pd.DataFrame: eq = eq[eq.index.date >= pd.to_datetime(self.start_overwrite).date()] return eq - - - PortStats = self.__port_stats - if self.start_overwrite: - start = pd.to_datetime(self.start_overwrite).date() - date_range = pd.date_range(start= self.dates_(True), end = self.dates_(False), freq = 'B') - start = self.dates_(True) - _ = self.dates_(False) - port_equity_data = pd.DataFrame(index = date_range) - for tick, data in PortStats.items(): - equity_curve = data['_equity_curve']['Equity'] - if isinstance(self.cash, dict): - cash = self.cash[tick] - elif isinstance(self.cash, int) or isinstance(self.cash, float): - cash = self.cash - - equity_curve.name = tick - tick_start = min(equity_curve.index) - if tick_start > pd.Timestamp(start): - temp = pd.DataFrame(index = pd.date_range(start = start, end =equity_curve.index.min(), freq = 'B' )) - temp[tick] = cash - equity_curve = pd.concat([equity_curve, temp], axis = 0) - - port_equity_data = port_equity_data.join(equity_curve) - - port_equity_data = port_equity_data.dropna(how = 'all') - port_equity_data = port_equity_data.fillna(method = 'ffill') - port_equity_data['Total'] = port_equity_data.sum(axis = 1) - port_equity_data.index = pd.DatetimeIndex(port_equity_data.index) - if self.start_overwrite: - port_equity_data = port_equity_data[port_equity_data.index.date >= pd.to_datetime(self.start_overwrite).date()] - port_equity_data = port_equity_data[~port_equity_data.index.duplicated(keep = 'first')] - return port_equity_data def __trades(self): return self.trades() diff --git a/trade/backtester_/utils/aggregators.py b/trade/backtester_/utils/aggregators.py index a700ba8..8b683b7 100644 --- a/trade/backtester_/utils/aggregators.py +++ b/trade/backtester_/utils/aggregators.py @@ -470,11 +470,12 @@ def trade_percentile(trades_df: pd.DataFrame, percentile: float) -> float: assert 0 <= percentile <= 100, ( f"Percentile must be between 0 and 100. Current Value: {percentile}" ) - return ( - np.percentile(trades_df.ReturnPct, percentile) * 100 - if "ReturnPct" in trades_df.columns - else np.percentile(trades_df.PnL, percentile) - ) + return trades_df.ReturnPct.quantile(percentile / 100) * 100 if "ReturnPct" in trades_df.columns else np.nan + # return ( + # np.percentile(trades_df.ReturnPct, percentile) * 100 + # if "ReturnPct" in trades_df.columns + # else np.percentile(trades_df.PnL, percentile) + # ) def profitFactor(trades_df: pd.DataFrame) -> float: @@ -556,7 +557,7 @@ def yearly_retrns(equity_timeseries: pd.DataFrame) -> dict: ts = equity_timeseries.copy(deep=True) ts["Year"] = ts.index.year ## dropping duplicate years if there are multiple - # ts = ts[~ts.index.duplicated(keep="first")] + ts = ts[~ts.index.duplicated(keep="first")] unq_year = ts.Year.unique() rtrn_d = {} for year in unq_year: From 1d3b548b5e57c5e306173028a5238d8e9719f924 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:30:17 -0400 Subject: [PATCH 09/81] Adjust pricing utilities and greek calculation helpers --- trade/assets/Calculate.py | 1454 ++++++++++------- trade/helpers/helper_types.py | 9 +- trade/helpers/pricing.py | 60 +- .../greeks/analytical/black_scholes.py | 5 +- .../optionlib/greeks/numerical/finite_diff.py | 2 +- trade/optionlib/vol/implied_vol.py | 7 +- 6 files changed, 878 insertions(+), 659 deletions(-) diff --git a/trade/assets/Calculate.py b/trade/assets/Calculate.py index 5fad186..ebc4d72 100644 --- a/trade/assets/Calculate.py +++ b/trade/assets/Calculate.py @@ -1,4 +1,3 @@ - from threading import Thread from datetime import datetime import datetime as dt @@ -7,16 +6,16 @@ from scipy.stats import norm from trade.assets.Stock import Stock from trade.helpers.helper import ( - time_distance_helper, - vanna, - volga, - identify_interval, - optionPV_helper, - vanna_decimal, - volga_decimal + time_distance_helper, + vanna, + volga, + identify_interval, + optionPV_helper, + vanna_decimal, + volga_decimal, ) from trade.helpers.Context import Context -from trade.assets.rates import get_risk_free_rate_helper +from trade.assets.rates import get_risk_free_rate_helper_v2 from typing import Union import pandas as pd from py_vollib.black_scholes_merton.greeks.numerical import delta, vega, theta, rho, gamma @@ -28,7 +27,8 @@ from pandas.tseries.offsets import BDay from trade.helpers.helper_types import OptionModelAttributes from trade.helpers.helper import get_parrallel_apply -logger = setup_logger('trade.assets.Calculate', stream_log_level=logging.CRITICAL) + +logger = setup_logger("trade.assets.Calculate", stream_log_level=logging.CRITICAL) ## TODO, recalculate the pv with the new vol values @@ -41,8 +41,7 @@ ## TODO: No need to calculate greeks if using RV for attribution - -def calculate_volga(S, K, T, r, sigma, option_type, q=0, delta_sigma=0.0001): +def calculate_volga(S, K, T, r, sigma, option_type, q=0, delta_sigma=0.0001): """ Calculates Volga (Vomma) numerically. @@ -71,18 +70,21 @@ def black_scholes_call_price(S, K, T, r, sigma, q): call_price = S * norm.cdf(d1) - K * np.exp(-df * T) * norm.cdf(d2) return call_price + def black_scholes_delta_call(S, K, T, r, sigma, q): df = r - q d1 = (np.log(S / K) + (df + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T)) delta = norm.cdf(d1) return delta + def black_scholes_vega(S, K, T, r, sigma, q): df = r - q d1 = (np.log(S / K) + (df + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T)) vega = S * norm.pdf(d1) * np.sqrt(T) return vega + def calculate_vanna(s, k, t, r, sigma, q, h=0.001): # Vanna as the derivative of Delta with respect to Volatility delta_plus_h = black_scholes_delta_call(s, k, t, r, sigma + h, q) @@ -95,22 +97,25 @@ def calculate_vanna(s, k, t, r, sigma, q, h=0.001): vanna_vega_deriv = (vega_plus_h - vega_minus_h) / (2 * h) # In theory, these two should be equal for Black-Scholes - return vanna_vega_deriv # Or vanna_vega_deriv + return vanna_vega_deriv # Or vanna_vega_deriv + def d1(s, k, t, r, 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(s, k, t, r, sigma, q): - return d1(s, k, t, r, sigma, q) - sigma*np.sqrt(t) + return d1(s, k, t, r, sigma, q) - sigma * np.sqrt(t) + def vanna_from_vega(s, k, t, r, sigma, flag, q=0.0): - """ - """ + """ """ if t <= 0 or sigma <= 0 or s <= 0 or k <= 0: return np.nan v = vega(flag=flag, S=s, K=k, t=t, r=r, sigma=sigma, q=q) ## From https://en.wikipedia.org/wiki/Greeks_(finance)#Vanna - return (v/s) * (1 - (d1(s, k, t, r, sigma, q)/(sigma * np.sqrt(t)))) + return (v / s) * (1 - (d1(s, k, t, r, sigma, q) / (sigma * np.sqrt(t)))) + def volga_from_vega(s, k, t, r, sigma, flag, q=0.0): """ @@ -125,88 +130,89 @@ def volga_from_vega(s, k, t, r, sigma, flag, q=0.0): return v * _d1 * _d2 / sigma -def PatchedCalculateFunc( func: Callable, long_leg = [], short_leg = [], return_all = False, *args, **kwargs): - """ - - """ +def PatchedCalculateFunc(func: Callable, long_leg=[], short_leg=[], return_all=False, *args, **kwargs): + """ """ from trade.assets.Option import Option - alllowable_func = ['pct_spot_slides', 'attribution', 'pct_vol_slides' ] + + alllowable_func = ["pct_spot_slides", "attribution", "pct_vol_slides"] assert hasattr(Calculate, func.__name__), f"Function {func.__name__} not a member of {Calculate}" assert func.__name__ in alllowable_func, f"Function {func.__name__} not allowed for this operation" - structure_dict = {'long': [], 'short': []} + structure_dict = {"long": [], "short": []} return_both_data = False + def get_func_values(leg, leg_name, *args, **kwargs): values = [] with Context(): - for l in leg: - assert isinstance(l, Option) or l.__class__.__name__ == 'Option', "Leg must be an Option object" + for l in leg: + assert isinstance(l, Option) or l.__class__.__name__ == "Option", "Leg must be an Option object" - if leg_name == 'long': + if leg_name == "long": values.append(func(l, *args, **kwargs)) else: values.append(-func(l, *args, **kwargs)) - + structure_dict[leg_name] = values - - long_leg_thread = Thread(target=get_func_values, args=(long_leg, 'long', *args), kwargs=kwargs, name = f'{func.__name__}_long') - short_leg_thread = Thread(target=get_func_values, args=(short_leg, 'short', *args), kwargs=kwargs, name = f'{func.__name__}_long') + + long_leg_thread = Thread( + target=get_func_values, args=(long_leg, "long", *args), kwargs=kwargs, name=f"{func.__name__}_long" + ) + short_leg_thread = Thread( + target=get_func_values, args=(short_leg, "short", *args), kwargs=kwargs, name=f"{func.__name__}_long" + ) long_leg_thread.start() short_leg_thread.start() - long_leg_thread.join(timeout = 3 * 60) - short_leg_thread.join(timeout = 3 * 60) + long_leg_thread.join(timeout=3 * 60) + short_leg_thread.join(timeout=3 * 60) ## Quick fix for pct_spot_slides, Need underlier_spot_slides column to NOT be summed - - if func.__name__ == 'pct_spot_slides': + + if func.__name__ == "pct_spot_slides": try: - columns_to_sum = [x for x in structure_dict['long'][0].columns if x != 'underlier_spot_slides'] - key = 'long' + columns_to_sum = [x for x in structure_dict["long"][0].columns if x != "underlier_spot_slides"] + key = "long" except: - columns_to_sum = [x for x in structure_dict['short'][0].columns if x != 'underlier_spot_slides'] - key = 'short' + columns_to_sum = [x for x in structure_dict["short"][0].columns if x != "underlier_spot_slides"] + key = "short" - if key == 'long': - underlier_series = structure_dict[key][0]['underlier_spot_slides'] + if key == "long": + underlier_series = structure_dict[key][0]["underlier_spot_slides"] else: - underlier_series = -structure_dict[key][0]['underlier_spot_slides'] - structure_dict['total'] = sum(structure_dict['long']) + sum(structure_dict['short']) - structure_dict['total']['underlier_spot_slides'] = underlier_series + underlier_series = -structure_dict[key][0]["underlier_spot_slides"] + structure_dict["total"] = sum(structure_dict["long"]) + sum(structure_dict["short"]) + structure_dict["total"]["underlier_spot_slides"] = underlier_series else: if return_both_data: - structure_dict['total'] = sum([x[1] for x in structure_dict['long']]) + sum([x[1] for x in structure_dict['short']]) + structure_dict["total"] = sum([x[1] for x in structure_dict["long"]]) + sum( + [x[1] for x in structure_dict["short"]] + ) else: - structure_dict['total'] = sum(structure_dict['long']) + sum(structure_dict['short']) - - + structure_dict["total"] = sum(structure_dict["long"]) + sum(structure_dict["short"]) if return_all: return structure_dict else: - return structure_dict['total'] - - - + return structure_dict["total"] class Calculate: rf_rate = None # Initializing risk free rate rf_ts = None init_date = None - + def __init__(self): """ Calculate Class used for calculating Asset related items - + Static Class for handling calculations in Stock & Option Classes """ - + today = datetime.today() start_date_date = today - relativedelta(months=6) - start_date = datetime.strftime(start_date_date, format='%Y-%m-%d') - end_date = datetime.strftime(today, format='%Y-%m-%d') + start_date = datetime.strftime(start_date_date, format="%Y-%m-%d") + end_date = datetime.strftime(today, format="%Y-%m-%d") - ##Initializing Risk Free Rate as a variable across every Stock Intance, while Re-initializing after day has changed + ##Initializing Risk Free Rate as a variable across every Stock Intance, while Re-initializing after day has changed if Calculate.rf_rate is None: print("Calculate Initializing Risk Free Rate") self.init_rfrate_ts() @@ -218,80 +224,88 @@ def __init__(self): self.init_risk_free_rate() self.init_date_method() - @classmethod def init_rfrate_ts(cls): - cls.rf_ts = get_risk_free_rate_helper() + cls.rf_ts = get_risk_free_rate_helper_v2() @classmethod def init_risk_free_rate(cls): ts = cls.rf_ts - cls.rf_rate = ts.iloc[len(ts)-1, 0]/100 + cls.rf_rate = ts.iloc[len(ts) - 1, 0] / 100 @classmethod def init_date_method(cls): cls.init_date = datetime.today() - @staticmethod def pv( - asset = None, - K: Union[int, float] = None, - exp_date: str = None, - sigma: float = None, - S0: Union[int, float, None] = None, - put_call=None, - N: int = 100, - r: float = None, - y: float = None, - start: str = None, - model: str = None) -> float: - ''' - Returns the price of an american option - - Parameters: - model: - K: Strike price - exp_date: Expiration date - S0: Spot at current time (Optional) - r: Risk free rate (Optional). If no value passed, defaults to most recent risk free rate. - N: Number of steps to use in the calculation (Optional) - y: Dividend yield (Optional) - 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) - ''' + asset=None, + K: Union[int, float] = None, + exp_date: str = None, + sigma: float = None, + S0: Union[int, float, None] = None, + put_call=None, + N: int = 100, + r: float = None, + y: float = None, + start: str = None, + model: str = None, + ) -> float: + """ + Returns the price of an american option + + Parameters: + model: + K: Strike price + exp_date: Expiration date + S0: Spot at current time (Optional) + r: Risk free rate (Optional). If no value passed, defaults to most recent risk free rate. + N: Number of steps to use in the calculation (Optional) + y: Dividend yield (Optional) + 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) + """ from trade.assets.Option import Option + if asset is None: if all(arg is None for arg in (K, exp_date, sigma, S0, put_call, N, r, y, start)): raise Exception("Missing Assets & Missing params") else: return optionPV_helper(S0, K, exp_date, r, y, sigma, put_call, start) else: - if asset.__class__.__name__ == 'Stock': - raise Exception("Stock can't be priced with options model. This method isn't utilized for a Stock Instance") - + if asset.__class__.__name__ == "Stock": + raise Exception( + "Stock can't be priced with options model. This method isn't utilized for a Stock Instance" + ) + if isinstance(asset, Option): - if K is None: - K = asset.K - if exp_date is None: - exp_date = asset.exp - if sigma is None: - sigma = asset.sigma - if S0 is None: - S0 = asset.unadjusted_S0 - if y is None: - y = asset.y - if put_call is None: - put_call = asset.put_call - if r is None: - r = asset.rf_rate - if start is None: - start = asset.end_date - return optionPV_helper(S0, K, exp_date, r, y, sigma, put_call, start) + if K is None: + K = asset.K + if exp_date is None: + exp_date = asset.exp + if sigma is None: + sigma = asset.sigma + if S0 is None: + S0 = asset.unadjusted_S0 + if y is None: + y = asset.y + if put_call is None: + put_call = asset.put_call + if r is None: + r = asset.rf_rate + if start is None: + start = asset.end_date + return optionPV_helper(S0, K, exp_date, r, y, sigma, put_call, start) @staticmethod - def pct_spot_slides(asset = None, pct_spot=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], greeks_to_calc = ['delta', 'gamma'], asset_type = None, **kwargs): + def pct_spot_slides( + asset=None, + pct_spot=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], + greeks_to_calc=["delta", "gamma"], + asset_type=None, + **kwargs, + ): """ Calculates Slide scenario based on provided lists of slides This assumes a percent drop in Spot @@ -308,42 +322,54 @@ def pct_spot_slides(asset = None, pct_spot=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], """ from trade.assets.Option import Option from trade.assets.OptionStructure import OptionStructure + if asset is None: - assert asset_type is not None, f'asset_type needed' - assert asset_type.lower() in ['stock', 'option'], f'Invalid asset_type, expected "stock" or "option", recieved "{asset_type}"' - if asset_type.lower() == 'stock': + assert asset_type is not None, f"asset_type needed" + assert asset_type.lower() in ["stock", "option"], ( + f'Invalid asset_type, expected "stock" or "option", recieved "{asset_type}"' + ) + if asset_type.lower() == "stock": if kwargs: - s0 = kwargs.pop('S0') - slides = Calculate.scenario_helper('spot', pct_spot,S0 = s0, modelType = 'stock') + s0 = kwargs.pop("S0") + slides = Calculate.scenario_helper("spot", pct_spot, S0=s0, modelType="stock") return slides else: - raise TypeError('Expected S0 for stock, none given') + raise TypeError("Expected S0 for stock, none given") else: if kwargs: - k = kwargs['K'] - exp_date = kwargs['exp_date'] - sigma = kwargs['sigma'] - s0 = kwargs['S0'] - put_call = kwargs['put_call'] - r = kwargs['r'] - y = kwargs['y'] - start = kwargs['start'] - modelType = 'option' - return Calculate.scenario_helper('spot', pct_spot, K = k, - S0=s0, exp_date=exp_date, sigma = sigma, y = y, put_call=put_call, - r = r, start = start, modelType = modelType, greeks_to_calc = greeks_to_calc).set_index('shocks') + k = kwargs["K"] + exp_date = kwargs["exp_date"] + sigma = kwargs["sigma"] + s0 = kwargs["S0"] + put_call = kwargs["put_call"] + r = kwargs["r"] + y = kwargs["y"] + start = kwargs["start"] + modelType = "option" + return Calculate.scenario_helper( + "spot", + pct_spot, + K=k, + S0=s0, + exp_date=exp_date, + sigma=sigma, + y=y, + put_call=put_call, + r=r, + start=start, + modelType=modelType, + greeks_to_calc=greeks_to_calc, + ).set_index("shocks") else: - raise TypeError('Expected kwargs for Calculate.PV. None was recieved.') + raise TypeError("Expected kwargs for Calculate.PV. None was recieved.") - - elif asset.__class__.__name__ == 'Stock': + elif asset.__class__.__name__ == "Stock": s0 = list(asset.spot().values())[-1] - slides = Calculate.scenario_helper('spot', pct_spot,S0 = s0, modelType = 'stock').set_index('shocks') - modelType = 'stock' + slides = Calculate.scenario_helper("spot", pct_spot, S0=s0, modelType="stock").set_index("shocks") + modelType = "stock" return slides - - elif asset.__class__.__name__ == 'Option': + elif asset.__class__.__name__ == "Option": k = getattr(asset, OptionModelAttributes.K.value) exp_date = getattr(asset, OptionModelAttributes.exp_date.value) sigma = getattr(asset, OptionModelAttributes.sigma.value) @@ -352,27 +378,46 @@ def pct_spot_slides(asset = None, pct_spot=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], put_call = getattr(asset, OptionModelAttributes.put_call.value) r = getattr(asset, OptionModelAttributes.r.value) start = getattr(asset, OptionModelAttributes.start.value) - modelType = 'option' - return Calculate.scenario_helper('spot', pct_spot, K = k, - S0=s0, exp_date=exp_date, sigma = sigma, y = y, put_call=put_call, - r = r, start = start, modelType = modelType, greeks_to_calc = greeks_to_calc).set_index('shocks') - - elif asset.__class__.__name__ == 'OptionStructure': + modelType = "option" + return Calculate.scenario_helper( + "spot", + pct_spot, + K=k, + S0=s0, + exp_date=exp_date, + sigma=sigma, + y=y, + put_call=put_call, + r=r, + start=start, + modelType=modelType, + greeks_to_calc=greeks_to_calc, + ).set_index("shocks") + + elif asset.__class__.__name__ == "OptionStructure": from trade.assets.Option import Option - return_all = kwargs.get('return_all', False) - return PatchedCalculateFunc(Calculate.pct_spot_slides, - long_leg = asset.long, - short_leg = asset.short, - return_all = return_all, - pct_spot = pct_spot, - greeks_to_calc = greeks_to_calc) - - + + return_all = kwargs.get("return_all", False) + return PatchedCalculateFunc( + Calculate.pct_spot_slides, + long_leg=asset.long, + short_leg=asset.short, + return_all=return_all, + pct_spot=pct_spot, + greeks_to_calc=greeks_to_calc, + ) + @staticmethod - def pct_vol_slides(asset = None, pct_spot=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, +0.05], greeks_to_calc = ['delta', 'gamma'], asset_type = None, **kwargs): + def pct_vol_slides( + asset=None, + pct_spot=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, +0.05], + greeks_to_calc=["delta", "gamma"], + asset_type=None, + **kwargs, + ): """ Calculates Slide scenario based on provided lists of vol slides. - These assumes a drop in points + These assumes a drop in points Returns a dataframe containing both PnL and price post shock @@ -386,30 +431,43 @@ def pct_vol_slides(asset = None, pct_spot=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, from trade.assets.OptionStructure import OptionStructure if asset is None: - assert asset_type is not None, f'asset_type needed' - assert asset_type.lower() in ['stock', 'option'], f'Invalid asset_type, expected "stock" or "option", recieved "{asset_type}"' - if asset_type.lower() == 'stock': - raise TypeError('Cannot Calculate vol shocks for Stock') + assert asset_type is not None, f"asset_type needed" + assert asset_type.lower() in ["stock", "option"], ( + f'Invalid asset_type, expected "stock" or "option", recieved "{asset_type}"' + ) + if asset_type.lower() == "stock": + raise TypeError("Cannot Calculate vol shocks for Stock") else: if kwargs: - k = kwargs['K'] - exp_date = kwargs['exp_date'] - sigma = kwargs['sigma'] - s0 = kwargs['S0'] - put_call = kwargs['put_call'] - r = kwargs['r'] - y = kwargs['y'] - start = kwargs['start'] - modelType = 'option' - return Calculate.scenario_helper('vol', pct_spot, K = k, - S0=s0, exp_date=exp_date, sigma = sigma, y = y, put_call=put_call, - r = r, start = start, modelType = modelType, greeks_to_calc= greeks_to_calc).set_index('shocks') + k = kwargs["K"] + exp_date = kwargs["exp_date"] + sigma = kwargs["sigma"] + s0 = kwargs["S0"] + put_call = kwargs["put_call"] + r = kwargs["r"] + y = kwargs["y"] + start = kwargs["start"] + modelType = "option" + return Calculate.scenario_helper( + "vol", + pct_spot, + K=k, + S0=s0, + exp_date=exp_date, + sigma=sigma, + y=y, + put_call=put_call, + r=r, + start=start, + modelType=modelType, + greeks_to_calc=greeks_to_calc, + ).set_index("shocks") else: - raise TypeError('Expected kwargs for Calculate.PV. None was recieved.') - + raise TypeError("Expected kwargs for Calculate.PV. None was recieved.") + elif isinstance(asset, Stock): - raise TypeError('Cannot Calculate vol shocks for Stock') - elif isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + raise TypeError("Cannot Calculate vol shocks for Stock") + elif isinstance(asset, Option) or asset.__class__.__name__ == "Option": k = getattr(asset, OptionModelAttributes.K.value) exp_date = getattr(asset, OptionModelAttributes.exp_date.value) sigma = getattr(asset, OptionModelAttributes.sigma.value) @@ -418,28 +476,43 @@ def pct_vol_slides(asset = None, pct_spot=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, put_call = getattr(asset, OptionModelAttributes.put_call.value) r = getattr(asset, OptionModelAttributes.r.value) start = getattr(asset, OptionModelAttributes.start.value) - modelType = 'option' - return Calculate.scenario_helper('vol', pct_spot, K = k, - S0=s0, exp_date=exp_date, sigma = sigma, y = y, put_call=put_call, - r = r, start = start, modelType = modelType, greeks_to_calc = greeks_to_calc).set_index('shocks') - - elif isinstance(asset, OptionStructure) or asset.__class__.__name__ == 'OptionStructure': - return_all = kwargs.get('return_all', False) - return PatchedCalculateFunc(Calculate.pct_vol_slides, - long_leg = asset.long, - short_leg = asset.short, - return_all = return_all, - pct_spot = pct_spot, - greeks_to_calc = greeks_to_calc) + modelType = "option" + return Calculate.scenario_helper( + "vol", + pct_spot, + K=k, + S0=s0, + exp_date=exp_date, + sigma=sigma, + y=y, + put_call=put_call, + r=r, + start=start, + modelType=modelType, + greeks_to_calc=greeks_to_calc, + ).set_index("shocks") + + elif isinstance(asset, OptionStructure) or asset.__class__.__name__ == "OptionStructure": + return_all = kwargs.get("return_all", False) + return PatchedCalculateFunc( + Calculate.pct_vol_slides, + long_leg=asset.long, + short_leg=asset.short, + return_all=return_all, + pct_spot=pct_spot, + greeks_to_calc=greeks_to_calc, + ) else: - raise TypeError(f'Invalid Asset Type. Recieved {asset}') + raise TypeError(f"Invalid Asset Type. Recieved {asset}") @staticmethod - def spot_vol_grid(asset = None, - vol_pct=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, +0.05], - spot_pct=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], - greek_enum = 'pv', - **kwargs): + def spot_vol_grid( + asset=None, + vol_pct=[-0.05, -0.02, -0.01, 0, +0.01, +0.02, +0.05], + spot_pct=[0.8, 0.9, 0.95, 1, 1.05, 1.1, 1.2], + greek_enum="pv", + **kwargs, + ): """ Calculates a grid of spot and vol shocks Returns a dataframe containing both PnL and price post shock @@ -448,21 +521,23 @@ def spot_vol_grid(asset = None, spot_pct_spot = An iterable with price shocks **kwargs = Keyword arguments as seen in Calculate.PV() For asset_type == 'stock' kwargs is S0 - + """ parrallel_apply_func = get_parrallel_apply() if asset is None: - k = kwargs['K'] - exp_date = kwargs['exp_date'] - sigma = kwargs['sigma'] - s0 = kwargs['S0'] - put_call = kwargs['put_call'] - r = kwargs['r'] - y = kwargs['y'] - start = kwargs['start'] - modelType = 'option' + k = kwargs["K"] + exp_date = kwargs["exp_date"] + sigma = kwargs["sigma"] + s0 = kwargs["S0"] + put_call = kwargs["put_call"] + r = kwargs["r"] + y = kwargs["y"] + start = kwargs["start"] + modelType = "option" else: - assert asset.__class__.__name__ == 'Option', f'Invalid asset type, expected Option, recieved {asset.__class__.__name__}' + assert asset.__class__.__name__ == "Option", ( + f"Invalid asset type, expected Option, recieved {asset.__class__.__name__}" + ) k = getattr(asset, OptionModelAttributes.K.value) exp_date = getattr(asset, OptionModelAttributes.exp_date.value) sigma = getattr(asset, OptionModelAttributes.sigma.value) @@ -473,11 +548,12 @@ def spot_vol_grid(asset = None, start = getattr(asset, OptionModelAttributes.start.value) def spot_vol_helper(spot_shock, vol_shock, greek_enum): - if greek_enum not in ['pv', 'delta', 'gamma', 'vega', 'rho', 'theta', 'vanna', 'volga','pnl']: - raise TypeError(f'Invalid greek_enum, expected one of ["pv", "delta", "gamma", "vega", "rho", "theta", "vanna", "volga"], recieved {greek_enum}') - + if greek_enum not in ["pv", "delta", "gamma", "vega", "rho", "theta", "vanna", "volga", "pnl"]: + raise TypeError( + f'Invalid greek_enum, expected one of ["pv", "delta", "gamma", "vega", "rho", "theta", "vanna", "volga"], recieved {greek_enum}' + ) - if greek_enum == 'pnl': + if greek_enum == "pnl": fn = Calculate.pv else: fn = getattr(Calculate, greek_enum) @@ -485,152 +561,188 @@ def spot_vol_helper(spot_shock, vol_shock, greek_enum): shocked_vol = sigma + vol_shock ## Calculate shocked value - if greek_enum in ['pv', 'pnl']: - shocked_value = fn(K = k, exp_date = exp_date, sigma = shocked_vol, S0 = shocked_spot, - put_call = put_call, r =r, y = y, start = start) + if greek_enum in ["pv", "pnl"]: + shocked_value = fn( + K=k, exp_date=exp_date, sigma=shocked_vol, S0=shocked_spot, put_call=put_call, r=r, y=y, start=start + ) else: - shocked_value = fn(K = k, exp = exp_date, sigma = shocked_vol, S = shocked_spot, - flag = put_call, r =r,start = start, y = y) - + shocked_value = fn( + K=k, exp=exp_date, sigma=shocked_vol, S=shocked_spot, flag=put_call, r=r, start=start, y=y + ) + ## Calculate base value for Pnl and set value to shocked - base - if greek_enum == 'pnl': + if greek_enum == "pnl": # print(f"Calculating PnL for spot shock {spot_shock} and vol shock {vol_shock}") # print(f"k={k}, exp_date={exp_date}, sigma={sigma}, S0={s0}, put_call={put_call}, r={r}, y={y}, start={start}") - pv = fn(K = k, exp_date = exp_date, sigma = sigma, S0 = s0, - put_call = put_call, r =r, y = y, start = start) + pv = fn(K=k, exp_date=exp_date, sigma=sigma, S0=s0, put_call=put_call, r=r, y=y, start=start) value = shocked_value - pv # print(f"Base PV: {pv}, Shocked PV: {shocked_value}, PnL: {value}") ## Else just return shocked value - else: + else: value = shocked_value return value - - + ## Create dataframe - spv = pd.DataFrame(index = sorted(vol_pct), columns = sorted(spot_pct)) + spv = pd.DataFrame(index=sorted(vol_pct), columns=sorted(spot_pct)) for v in vol_pct: for s in spot_pct: spv.loc[v, s] = spot_vol_helper(s, v, greek_enum) - spv.index.name = 'vol_shock' - spv.columns.name = 'spot_shock' + spv.index.name = "vol_shock" + spv.columns.name = "spot_shock" return spv - - + @staticmethod def scenario_helper(type_, pct_spot, **kwargs): """ scenario_helper should return a dataframe with shocks as index This is a multi-purpose scenario calculator. It calculates the scenario for different shocks """ - - modelType = kwargs['modelType'] - if modelType == 'option': - k = kwargs['K'] - exp_date = kwargs['exp_date'] - sigma = kwargs['sigma'] - s0 = kwargs['S0'] - put_call = kwargs['put_call'] - r = kwargs['r'] - y = kwargs['y'] - start = kwargs['start'] - greeks_to_calc = kwargs['greeks_to_calc'] - - else: - s0 = kwargs['S0'] + + modelType = kwargs["modelType"] + if modelType == "option": + k = kwargs["K"] + exp_date = kwargs["exp_date"] + sigma = kwargs["sigma"] + s0 = kwargs["S0"] + put_call = kwargs["put_call"] + r = kwargs["r"] + y = kwargs["y"] + start = kwargs["start"] + greeks_to_calc = kwargs["greeks_to_calc"] + + else: + s0 = kwargs["S0"] greeks_to_calc = None - if type_ == 'spot': + if type_ == "spot": pct_spot.append(1) if 1 not in pct_spot else None - scen = pd.DataFrame(index = [x for x in range(len(pct_spot))], data = {'shocks':pct_spot}) - - if modelType =='option': - scen['underlier_spot_slides'] = scen.apply(lambda x:(x['shocks'] - 1)*s0, axis = 1) - scen['shocked_pv'] = scen.apply(lambda x:Calculate.pv(K = k, exp_date = exp_date, sigma = sigma, S0 = x['shocks']*s0, - put_call = put_call, r =r, y = y, start = start),axis = 1) - scen['pv'] = scen.apply(lambda x:Calculate.pv(K = k, exp_date = exp_date, sigma = sigma, S0 = s0, - put_call = put_call, r =r, y = y, start = start),axis = 1) - scen['pnl'] = scen['shocked_pv'] - scen['pv'] + scen = pd.DataFrame(index=[x for x in range(len(pct_spot))], data={"shocks": pct_spot}) + + if modelType == "option": + scen["underlier_spot_slides"] = scen.apply(lambda x: (x["shocks"] - 1) * s0, axis=1) + scen["shocked_pv"] = scen.apply( + lambda x: Calculate.pv( + K=k, + exp_date=exp_date, + sigma=sigma, + S0=x["shocks"] * s0, + put_call=put_call, + r=r, + y=y, + start=start, + ), + axis=1, + ) + scen["pv"] = scen.apply( + lambda x: Calculate.pv( + K=k, exp_date=exp_date, sigma=sigma, S0=s0, put_call=put_call, r=r, y=y, start=start + ), + axis=1, + ) + scen["pnl"] = scen["shocked_pv"] - scen["pv"] if greeks_to_calc: for greek in greeks_to_calc: fn = getattr(Calculate, greek) - scen[greek] = scen.apply(lambda x:fn(K = k, exp = exp_date, sigma = sigma, S = s0*x['shocks'], - flag = put_call, r =r,start = start, y = y),axis = 1) - - scen.sort_values('shocks', inplace = True) + scen[greek] = scen.apply( + lambda x: fn( + K=k, exp=exp_date, sigma=sigma, S=s0 * x["shocks"], flag=put_call, r=r, start=start, y=y + ), + axis=1, + ) + + scen.sort_values("shocks", inplace=True) return scen - elif modelType =='stock': - scen['shocked_pv'] = scen.apply(lambda x: s0 * x['shocks'], axis = 1) - scen['pv'] = s0 - scen['pnl'] = scen['shocked_pv'] - scen['pv'] - scen.sort_values('shocks', inplace = True) + elif modelType == "stock": + scen["shocked_pv"] = scen.apply(lambda x: s0 * x["shocks"], axis=1) + scen["pv"] = s0 + scen["pnl"] = scen["shocked_pv"] - scen["pv"] + scen.sort_values("shocks", inplace=True) return scen - if type_ == 'vol': + if type_ == "vol": pct_spot.append(0.0) if 0.0 not in pct_spot else None - scen = pd.DataFrame(index = [x for x in range(len(pct_spot))], data = {'shocks':pct_spot}) - - if modelType =='option': - scen['shocked_pv'] = scen.apply(lambda x:Calculate.pv(K = k, exp_date = exp_date, sigma = sigma + x['shocks'], S0 = s0, - put_call = put_call, r =r, y = y, start = start),axis = 1) - scen['pv'] = scen.apply(lambda x:Calculate.pv(K = k, exp_date = exp_date, sigma = sigma, S0 = s0, - put_call = put_call, r =r, y = y, start = start),axis = 1) - scen['pnl'] = scen['shocked_pv'] - scen['pv'] + scen = pd.DataFrame(index=[x for x in range(len(pct_spot))], data={"shocks": pct_spot}) + + if modelType == "option": + scen["shocked_pv"] = scen.apply( + lambda x: Calculate.pv( + K=k, + exp_date=exp_date, + sigma=sigma + x["shocks"], + S0=s0, + put_call=put_call, + r=r, + y=y, + start=start, + ), + axis=1, + ) + scen["pv"] = scen.apply( + lambda x: Calculate.pv( + K=k, exp_date=exp_date, sigma=sigma, S0=s0, put_call=put_call, r=r, y=y, start=start + ), + axis=1, + ) + scen["pnl"] = scen["shocked_pv"] - scen["pv"] if greeks_to_calc: for greek in greeks_to_calc: greek = greek.lower() fn = getattr(Calculate, greek) - scen[greek] = scen.apply(lambda x:fn(K = k, exp = exp_date, sigma = sigma+ x['shocks'], S = s0, - flag = put_call, r =r,start = start, y = y),axis = 1) - - scen.sort_values('shocks', inplace = True) + scen[greek] = scen.apply( + lambda x: fn( + K=k, exp=exp_date, sigma=sigma + x["shocks"], S=s0, flag=put_call, r=r, start=start, y=y + ), + axis=1, + ) + + scen.sort_values("shocks", inplace=True) return scen - - - - - @staticmethod - def delta(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def delta(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the Delta of an option """ from trade.assets.Option import Option - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - + 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] ) - elif model == 'binomial': - d = delta(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) + 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]) + elif model == "binomial": + d = delta(flag=flag.lower(), S=args[0], K=args[1], t=t, r=args[2], sigma=args[3], q=args[4]) return d 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" + 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" + ) if sigma == 0: logger.error("Sigma cannot be 0") logger.error(f"Kwargs: {locals()}") return 0.0 - + if sigma == 0: raise ValueError("Sigma cannot be 0") 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': - d = delta(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) - elif model == 'mcs': + if model == "bs": + d = delta(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "binomial": + d = delta(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") @@ -639,101 +751,103 @@ def delta(asset = None, S = None, K = None, r = None, sigma = None, start = None else: raise Exception(f"Delta cannot be Calculated for {asset} type") - @staticmethod - def vega(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def vega(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the Vega of an option """ from trade.assets.Option import Option - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - 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] ) - elif model == 'binomial': - d = vega(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) + 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]) + elif model == "binomial": + d = vega(flag=flag.lower(), S=args[0], K=args[1], t=t, r=args[2], sigma=args[3], q=args[4]) return d 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" + 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(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': - d = vega(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) - elif model == 'mcs': + if model == "bs": + d = vega(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "binomial": + d = vega(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") - + d = float(d) return float(d) else: raise Exception(f"Vega cannot be Calculated for {asset} type") - - @staticmethod - def vanna(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def vanna(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the vanna of an option """ from trade.assets.Option import Option - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - + t = time_distance_helper(end=asset.exp, start=asset.end_date) flag = getattr(asset, OptionModelAttributes.put_call.value) - if model == 'bs': + 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] ) - d = vanna_from_vega(s= args[0], k = args[1], t = t, r = args[2], sigma = args[3], q = args[4], flag= flag.lower()) + d = vanna_from_vega(s=args[0], k=args[1], t=t, r=args[2], sigma=args[3], q=args[4], flag=flag.lower()) # d = vanna_decimal(S= args[0], K = args[1], T= t, r = args[2], sigma = args[3], q = args[4]) - elif model == 'binomial': - + elif model == "binomial": # d = vanna(flag = flag.lower(),S = args[0], K = args[1], T = t, r = args[2], sigma = args[3], q = args[4] ) - d = vanna_from_vega(s= args[0], k = args[1], t = t, r = args[2], sigma = args[3], q = args[4], flag= flag.lower()) + d = vanna_from_vega(s=args[0], k=args[1], t=t, r=args[2], sigma=args[3], q=args[4], flag=flag.lower()) # d = vanna_decimal(S= args[0], K = args[1], T= t, r = args[2], sigma = args[3], q = args[4]) - elif model == 'mcs': + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") return d 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" + 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(end=exp, start=start) - if model == 'bs': - + if model == "bs": # d = vanna(flag = flag.lower(), S = S, K = K, T = t, r = r, sigma = sigma, q = y ) - d = vanna_from_vega(s= S, k= K, t= t, r= r, sigma= sigma, q= y, flag= flag.lower()) - elif model == 'binomial': + d = vanna_from_vega(s=S, k=K, t=t, r=r, sigma=sigma, q=y, flag=flag.lower()) + elif model == "binomial": # d = vanna(flag = flag.lower(), S = S, K = K, T = t, r = r, sigma = sigma, q = y ) - d = vanna_from_vega(s= S, k= K, t= t, r= r, sigma= sigma, q= y, flag= flag.lower()) - elif model == 'mcs': + d = vanna_from_vega(s=S, k=K, t=t, r=r, sigma=sigma, q=y, flag=flag.lower()) + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") @@ -743,43 +857,46 @@ def vanna(asset = None, S = None, K = None, r = None, sigma = None, start = None raise Exception(f"Vanna cannot be Calculated for {asset} type") @staticmethod - def volga(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def volga(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the volga of an option """ from trade.assets.Option import Option - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - - + 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()) - elif model == 'binomial': - d = volga_from_vega(s= args[0], k = args[1], t = t, r = args[2], sigma = args[3], q = args[4], flag= flag.lower()) + 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()) + elif model == "binomial": + d = volga_from_vega(s=args[0], k=args[1], t=t, r=args[2], sigma=args[3], q=args[4], flag=flag.lower()) return d 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" + 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(end=exp, start=start) - if model == 'bs': + 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()) - elif model == 'binomial': + d = volga_from_vega(s=S, k=K, t=t, r=r, sigma=sigma, q=y, flag=flag.lower()) + elif model == "binomial": # 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()) - elif model == 'mcs': + d = volga_from_vega(s=S, k=K, t=t, r=r, sigma=sigma, q=y, flag=flag.lower()) + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") @@ -789,44 +906,48 @@ def volga(asset = None, S = None, K = None, r = None, sigma = None, start = None raise Exception(f"Volga cannot be Calculated for {asset} type") @staticmethod - def gamma(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def gamma(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the gamma of an option """ from trade.assets.Option import Option - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) - + 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': - d = gamma(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) + 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": + d = gamma(flag=flag.lower(), S=args[0], K=args[1], t=t, r=args[2], sigma=args[3], q=args[4]) return d 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" + 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" + ) if sigma == 0: logger.error("Sigma cannot be 0") logger.error(f"Kwargs: {locals()}") return 0.0 - + 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': - d = gamma(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) - elif model == 'mcs': + if model == "bs": + d = gamma(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "binomial": + d = gamma(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") else: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") @@ -835,220 +956,244 @@ def gamma(asset = None, S = None, K = None, r = None, sigma = None, start = None return float(d) else: raise Exception(f"Gamma cannot be Calculated for {asset} type") - + @staticmethod - def theta(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def theta(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the theta of an option """ from trade.assets.Option import Option - if model =='mcs': + + if model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") - elif model not in ['bs', 'binomial']: + elif model not in ["bs", "binomial"]: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") - - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) 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': - d = theta(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) + 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": + d = theta(flag=flag.lower(), S=args[0], K=args[1], t=t, r=args[2], sigma=args[3], q=args[4]) return d 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" + 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(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': - d = theta(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) + if model == "bs": + d = theta(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "binomial": + d = theta(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) d = float(d) return float(d) else: raise Exception(f"Theta cannot be Calculated for {asset} type") - @staticmethod - def rho(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def rho(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns the rho of an option """ from trade.assets.Option import Option - if model =='mcs': + + if model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") - elif model not in ['bs', 'binomial']: + elif model not in ["bs", "binomial"]: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") - - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": args = [S, K, r, sigma, y, model] - args_str = [OptionModelAttributes.S0.value, - OptionModelAttributes.K.value, - OptionModelAttributes.r.value, - OptionModelAttributes.sigma.value, - OptionModelAttributes.y.value, - 'model'] + args_str = [ + OptionModelAttributes.S0.value, + OptionModelAttributes.K.value, + OptionModelAttributes.r.value, + OptionModelAttributes.sigma.value, + OptionModelAttributes.y.value, + "model", + ] for i in range(len(args)): if args[i] is None: args[i] = getattr(asset, args_str[i]) 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': - d = rho(flag = flag.lower(),S = args[0], K = args[1], t = t, r = args[2], sigma = args[3], q = args[4] ) + 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": + d = rho(flag=flag.lower(), S=args[0], K=args[1], t=t, r=args[2], sigma=args[3], q=args[4]) return d 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" + 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(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': - d = rho(flag = flag.lower(), S = S, K = K, t = t, r = r, sigma = sigma, q = y ) + if model == "bs": + d = rho(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) + elif model == "binomial": + d = rho(flag=flag.lower(), S=S, K=K, t=t, r=r, sigma=sigma, q=y) d = float(d) return float(d) else: raise Exception(f"Rho cannot be Calculated for {asset} type") @staticmethod - def greeks(asset = None, S = None, K = None, r = None, sigma = None, start = None, flag = None, exp = None, y = None, model = 'bs'): - + def greeks(asset=None, S=None, K=None, r=None, sigma=None, start=None, flag=None, exp=None, y=None, model="bs"): """ Returns all the greeks of an option as dictionary """ from trade.assets.Option import Option + kwargs = locals() - if model =='mcs': + if model == "mcs": raise NotImplementedError("Monte Carlo Simulation not implemented yet") - elif model not in ['bs', 'binomial']: + elif model not in ["bs", "binomial"]: raise ValueError(f"Invalid Model Type, recieved {model}, expected 'bs', 'binomial' or 'mcs'") - if isinstance(asset, Option) or asset.__class__.__name__ == 'Option': + if isinstance(asset, Option) or asset.__class__.__name__ == "Option": try: - greeks = {'Delta': Calculate.delta(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model), - 'Gamma': Calculate.gamma(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model), - 'Vega': Calculate.vega(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model), - 'Theta': Calculate.theta(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model) if Calculate.theta(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model) is not None else 0, - 'Rho': Calculate.rho(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model), - 'Vanna':Calculate.vanna(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model), - 'Volga':Calculate.volga(asset, S =S, K = K, r = r, sigma = sigma, start = start, model = model) + greeks = { + "Delta": Calculate.delta(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), + "Gamma": Calculate.gamma(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), + "Vega": Calculate.vega(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), + "Theta": Calculate.theta(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model) + if Calculate.theta(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model) is not None + else 0, + "Rho": Calculate.rho(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), + "Vanna": Calculate.vanna(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), + "Volga": Calculate.volga(asset, S=S, K=K, r=r, sigma=sigma, start=start, model=model), } return greeks - + except Exception as e: print(e) - return {'Delta': 0.0, 'Gamma': 0.0, 'Vega': 0.0, 'Theta': 0.0, 'Rho': 0.0, 'Vanna': 0.0, 'Volga': 0.0} - + return {"Delta": 0.0, "Gamma": 0.0, "Vega": 0.0, "Theta": 0.0, "Rho": 0.0, "Vanna": 0.0, "Volga": 0.0} + elif asset == None: try: - greeks = {'Delta': Calculate.delta(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model), - 'Gamma': Calculate.gamma(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model), - 'Vega': Calculate.vega(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model), - 'Theta': Calculate.theta(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model) if Calculate.theta(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model) is not None else 0, - 'Rho': Calculate.rho(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model), - 'Vanna':Calculate.vanna(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model), - 'Volga':Calculate.volga(S =S, K = K, r = r, sigma = sigma, start = start, flag = flag, exp = exp, y = y, model=model) + greeks = { + "Delta": Calculate.delta( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ), + "Gamma": Calculate.gamma( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ), + "Vega": Calculate.vega( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ), + "Theta": Calculate.theta( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ) + if Calculate.theta(S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model) + is not None + else 0, + "Rho": Calculate.rho(S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model), + "Vanna": Calculate.vanna( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ), + "Volga": Calculate.volga( + S=S, K=K, r=r, sigma=sigma, start=start, flag=flag, exp=exp, y=y, model=model + ), } return greeks except Exception as e: - logger.error('\nCalculate.greeks raised this error') - logger.error(e,exc_info=True) - logger.error(f'Kwargs:{kwargs}') + logger.error("\nCalculate.greeks raised this error") + logger.error(e, exc_info=True) + logger.error(f"Kwargs:{kwargs}") if isinstance(e, (AssertionError, NotImplementedError)): raise e - return {'Delta': 0, 'Gamma': 0, 'Vega': 0, 'Theta': 0, 'Rho': 0, 'Vanna': 0, 'Volga':0} + return {"Delta": 0, "Gamma": 0, "Vega": 0, "Theta": 0, "Rho": 0, "Vanna": 0, "Volga": 0} else: - raise Exception( - f"Greeks cannot be Calculated for {asset} type") - + raise Exception(f"Greeks cannot be Calculated for {asset} type") + @staticmethod - def attribution(asset, - ts_start = None, - ts_end= None, - ts_timeframe = 'day', - ts_timewidth = '1', - method = "GB", - replace = 'partial', - return_both_data = False, - **kwargs): - + def attribution( + asset, + ts_start=None, + ts_end=None, + ts_timeframe="day", + ts_timewidth="1", + method="GB", + replace="partial", + return_both_data=False, + **kwargs, + ): ## To do, add replace option. Either to fill close with midpoint, use only close or use only midpoint """ - Calculate attribution of option asset + Calculate attribution of option asset Parameter: ____________ ts_start (str | Datetime): Start date if timeseries - ts_end (str | Datetime): End date if timeseries + ts_end (str | Datetime): End date if timeseries ts_timewidth (int): Examples 1,2,3,4. The span over the timeframe ts_timeframe (str): The timeframe for aggregation, eg: Minute, Hour, Day, Month, Week, Year method (str): Available methods are 'GB' for Greek Based and 'RV' for Revaluation replace (str): Available options are 'partial', 'close', 'default_fill'. Partial replaces only the missing data, Close uses close data to fill, default_fill uses the default fill for all data return_both_data (bool): If True. Will return both the PnL Data and Full Data return_all: specific to OptionStructure. If True, will return all the data for the long and short leg - + """ from trade.assets.Option import Option from trade.assets.OptionStructure import OptionStructure - #GET OPTION TIMESERIES + # GET OPTION TIMESERIES today = datetime.today() ## Dates to allow ffill ts_start = ts_start if ts_start else asset.start_date ts_end = ts_end if ts_end else asset.end_date start = pd.to_datetime(ts_start) - BDay(2) end = pd.to_datetime(ts_end) + BDay(2) - if asset.__class__.__name__ == 'Option': - + if asset.__class__.__name__ == "Option": ## Designate the columns to be used - vol_col = ['Bs_iv' if asset.model == 'bsm' else 'Binomial_iv'] + vol_col = ["Bs_iv" if asset.model == "bsm" else "Binomial_iv"] if asset.default_fill: vol_col.append(f"{asset.default_fill.capitalize()}_{vol_col[0].lower()}") - spot_col = ['Close', asset.default_fill.capitalize()] - spot_col_2 = ['Option_Close', asset.default_fill.capitalize()] - greeks_col = ['Delta', 'Gamma', 'Vega', 'Theta', 'Rho', 'Vanna', 'Volga'] + spot_col = ["Close", asset.default_fill.capitalize()] + spot_col_2 = ["Option_Close", asset.default_fill.capitalize()] + greeks_col = ["Delta", "Gamma", "Vega", "Theta", "Rho", "Vanna", "Volga"] ## GET OPTION TIMESERIES - spot_ts = asset.spot(ts_start= start, - ts_end = end, - ts_timeframe= ts_timeframe, - ts_timewidth= ts_timewidth, - ts = True)[spot_col] - + spot_ts = asset.spot( + ts_start=start, ts_end=end, ts_timeframe=ts_timeframe, ts_timewidth=ts_timewidth, ts=True + )[spot_col] + ## Create mask for missing data full_data = spot_ts.copy() - if replace == 'partial': - close_fill_mask = full_data['Close'] == 0 + if replace == "partial": + close_fill_mask = full_data["Close"] == 0 - elif replace == 'close': - close_fill_mask = pd.Series([False]*len(full_data), index = full_data.index) + elif replace == "close": + close_fill_mask = pd.Series([False] * len(full_data), index=full_data.index) - elif replace == 'default_fill': - close_fill_mask = pd.Series([True]*len(full_data), index = full_data.index) + elif replace == "default_fill": + close_fill_mask = pd.Series([True] * len(full_data), index=full_data.index) else: - raise Exception(f"Invalid replace option. Expected 'partial', 'close', 'default_fill', recieved {replace}") + raise Exception( + f"Invalid replace option. Expected 'partial', 'close', 'default_fill', recieved {replace}" + ) ## Fill missing data + Rename columns to Option_Close - full_data.rename(columns = {'Close': 'Option_Close'}, inplace = True) - full_data.loc[close_fill_mask, 'Option_Close'] = full_data.loc[close_fill_mask, asset.default_fill.capitalize()] - + full_data.rename(columns={"Close": "Option_Close"}, inplace=True) + full_data.loc[close_fill_mask, "Option_Close"] = full_data.loc[ + close_fill_mask, asset.default_fill.capitalize() + ] ## Get Vol Timeseries - vol = asset.vol(ts_start= start, - ts_end = end, - ts_timeframe= ts_timeframe, - ts_timewidth= ts_timewidth, - ts = True)[vol_col] + vol = asset.vol(ts_start=start, ts_end=end, ts_timeframe=ts_timeframe, ts_timewidth=ts_timewidth, ts=True)[ + vol_col + ] full_data[vol_col] = vol ## Fill missing Vol data full_data.loc[close_fill_mask, vol_col[0]] = full_data.loc[close_fill_mask, vol_col[1]] @@ -1060,124 +1205,167 @@ def attribution(asset, full_data[vol_col] = full_data[vol_col].ffill() # # GET STOCK TIMESERIES - stock_ts = asset.asset.spot(ts = True, - ts_start = start, ts_end = end, ts_timewidth = ts_timewidth, - ts_timeframe= ts_timeframe, - spot_type = OptionModelAttributes.spot_type.value) - stock_ts.rename(columns = {x: x.capitalize() for x in stock_ts.columns}, inplace = True) - full_data['Stock_Close'] = stock_ts[['Close']] - full_data.ffill(inplace = True) - full_data.fillna(0, inplace = True) - + stock_ts = asset.asset.spot( + ts=True, + ts_start=start, + ts_end=end, + ts_timewidth=ts_timewidth, + ts_timeframe=ts_timeframe, + spot_type=OptionModelAttributes.spot_type.value, + ) + stock_ts.rename(columns={x: x.capitalize() for x in stock_ts.columns}, inplace=True) + full_data["Stock_Close"] = stock_ts[["Close"]] + full_data.ffill(inplace=True) + full_data.fillna(0, inplace=True) # GET rates timeseries - intv = identify_interval(ts_timewidth,ts_timeframe) - full_data['RF_rate'] = get_risk_free_rate_helper(intv)['annualized'] + intv = identify_interval(ts_timewidth, ts_timeframe) + full_data["RF_rate"] = get_risk_free_rate_helper_v2(intv)["annualized"] ## Add Mapping for the columns that were filled - full_data['DATA_FILL'] = 'NO' - full_data.loc[close_fill_mask, 'DATA_FILL'] = 'YES' + full_data["DATA_FILL"] = "NO" + full_data.loc[close_fill_mask, "DATA_FILL"] = "YES" # Calculate the percentage change, mark change and get prev day data - full_data[[f'prev_day_{col}' for col in full_data.columns]] = full_data.shift(1) - full_data['Stock_Close_Change_Mark'] = full_data['Stock_Close'] - full_data['Stock_Close'].shift(1) - full_data['Vol_Change_Mark'] = full_data[vol_col[0]] - full_data[vol_col[0]].shift(1) - full_data['Option_Close_Change_Mark'] = full_data['Option_Close'] - full_data['Option_Close'].shift(1) - full_data[[f'{x}_Change_Percent' for x in spot_col_2+['Stock_Close']] ] = full_data[spot_col_2+['Stock_Close']].pct_change() - full_data[['RF_rate_Change_Mark']] = full_data[['RF_rate']] - full_data[['RF_rate']].shift(periods = 1) - + full_data[[f"prev_day_{col}" for col in full_data.columns]] = full_data.shift(1) + full_data["Stock_Close_Change_Mark"] = full_data["Stock_Close"] - full_data["Stock_Close"].shift(1) + full_data["Vol_Change_Mark"] = full_data[vol_col[0]] - full_data[vol_col[0]].shift(1) + full_data["Option_Close_Change_Mark"] = full_data["Option_Close"] - full_data["Option_Close"].shift(1) + full_data[[f"{x}_Change_Percent" for x in spot_col_2 + ["Stock_Close"]]] = full_data[ + spot_col_2 + ["Stock_Close"] + ].pct_change() + full_data[["RF_rate_Change_Mark"]] = full_data[["RF_rate"]] - full_data[["RF_rate"]].shift(periods=1) if method == "GB": - ## ADD GREEKS. Make sure it is previous timestamp greeks - greeks = asset.greeks(ts_start= start, - ts_end = end, - ts_timeframe= ts_timeframe, - ts_timewidth= ts_timewidth, - greek_type='greeks') + greeks = asset.greeks( + ts_start=start, + ts_end=end, + ts_timeframe=ts_timeframe, + ts_timewidth=ts_timewidth, + greek_type="greeks", + ) ## Fill missing greeks - greek_replace_col = [f'{asset.default_fill.capitalize()}_{greek.lower()}' for greek in greeks_col] + greek_replace_col = [f"{asset.default_fill.capitalize()}_{greek.lower()}" for greek in greeks_col] greeks = greeks[greeks.index.isin(full_data.index)] - for index,replace_col in enumerate(greek_replace_col): + for index, replace_col in enumerate(greek_replace_col): greeks.loc[close_fill_mask, greeks_col[index]] = greeks.loc[close_fill_mask, replace_col] full_data = full_data.join(greeks.shift(1)) - full_data.reset_index(inplace = True) + full_data.reset_index(inplace=True) ## Convert date/time change to seconds, then to days. This is most useful for intraday theta PnL - full_data['total_seconds'] = (full_data['Datetime']-full_data['Datetime'].shift(1)).dt.total_seconds()/(24*60*60) - - - - PnL_Data = pd.DataFrame(index = full_data.index) - PnL_Data['Delta_PnL'] = (full_data['Delta']*100)*full_data['Stock_Close_Change_Mark'] - PnL_Data['Gamma_PnL'] = (full_data['Gamma']*100)*((full_data['Stock_Close_Change_Mark'])**2)*0.5 - PnL_Data['Vega_PnL'] = (full_data['Vega']*100)*full_data['Vol_Change_Mark'] * 100 - PnL_Data['Theta_PnL'] = (full_data['Theta']*100) * full_data['total_seconds'] - PnL_Data['Rho_PnL'] = (full_data['Rho']*100)*full_data['RF_rate_Change_Mark'] * 100 - PnL_Data['Volga_PnL'] = (full_data['Volga']*100)*((full_data['Vol_Change_Mark'])**2) - PnL_Data['Vanna_PnL'] = (full_data['Vanna']*100)*full_data['Stock_Close_Change_Mark']*full_data['Vol_Change_Mark'] - PnL_Data['Total_PnL'] = PnL_Data.sum(axis = 1) - PnL_Data['Datetime'] = full_data['Datetime'] - PnL_Data['Option_Close_Change_percent'] = full_data['Option_Close_Change_Percent'] - PnL_Data['Stock_Close_Change_percent'] = full_data['Stock_Close_Change_Percent'] - PnL_Data['Option_Close_Change_Mark'] = full_data['Option_Close_Change_Mark']*100 - PnL_Data['Vol_Change_Diff'] = full_data['Vol_Change_Mark'] - PnL_Data['Unexplained_PnL'] = PnL_Data['Option_Close_Change_Mark'] - PnL_Data['Total_PnL'] - PnL_Data['Price'] = full_data['Option_Close']*100 - PnL_Data['DATA_FILL'] = full_data['DATA_FILL'] - PnL_Data.set_index('Datetime', inplace = True) - PnL_Data = PnL_Data[['Delta_PnL', 'Gamma_PnL', 'Theta_PnL', 'Vega_PnL','Volga_PnL', 'Vanna_PnL', 'Rho_PnL', 'Total_PnL', 'Unexplained_PnL', 'Option_Close_Change_Mark','Price' ]] - PnL_Data.rename(columns= {'Option_Close_Change_Mark': 'Actual_PnL'}, inplace = True) + full_data["total_seconds"] = ( + full_data["Datetime"] - full_data["Datetime"].shift(1) + ).dt.total_seconds() / (24 * 60 * 60) + + PnL_Data = pd.DataFrame(index=full_data.index) + PnL_Data["Delta_PnL"] = (full_data["Delta"] * 100) * full_data["Stock_Close_Change_Mark"] + PnL_Data["Gamma_PnL"] = (full_data["Gamma"] * 100) * ((full_data["Stock_Close_Change_Mark"]) ** 2) * 0.5 + PnL_Data["Vega_PnL"] = (full_data["Vega"] * 100) * full_data["Vol_Change_Mark"] * 100 + PnL_Data["Theta_PnL"] = (full_data["Theta"] * 100) * full_data["total_seconds"] + PnL_Data["Rho_PnL"] = (full_data["Rho"] * 100) * full_data["RF_rate_Change_Mark"] * 100 + PnL_Data["Volga_PnL"] = (full_data["Volga"] * 100) * ((full_data["Vol_Change_Mark"]) ** 2) + PnL_Data["Vanna_PnL"] = ( + (full_data["Vanna"] * 100) * full_data["Stock_Close_Change_Mark"] * full_data["Vol_Change_Mark"] + ) + PnL_Data["Total_PnL"] = PnL_Data.sum(axis=1) + PnL_Data["Datetime"] = full_data["Datetime"] + PnL_Data["Option_Close_Change_percent"] = full_data["Option_Close_Change_Percent"] + PnL_Data["Stock_Close_Change_percent"] = full_data["Stock_Close_Change_Percent"] + PnL_Data["Option_Close_Change_Mark"] = full_data["Option_Close_Change_Mark"] * 100 + PnL_Data["Vol_Change_Diff"] = full_data["Vol_Change_Mark"] + PnL_Data["Unexplained_PnL"] = PnL_Data["Option_Close_Change_Mark"] - PnL_Data["Total_PnL"] + PnL_Data["Price"] = full_data["Option_Close"] * 100 + PnL_Data["DATA_FILL"] = full_data["DATA_FILL"] + PnL_Data.set_index("Datetime", inplace=True) + PnL_Data = PnL_Data[ + [ + "Delta_PnL", + "Gamma_PnL", + "Theta_PnL", + "Vega_PnL", + "Volga_PnL", + "Vanna_PnL", + "Rho_PnL", + "Total_PnL", + "Unexplained_PnL", + "Option_Close_Change_Mark", + "Price", + ] + ] + PnL_Data.rename(columns={"Option_Close_Change_Mark": "Actual_PnL"}, inplace=True) PnL_Data = PnL_Data[(PnL_Data.index >= ts_start) & (PnL_Data.index <= ts_end)] - full_data = full_data[(full_data['Datetime'] >= ts_start) & (full_data['Datetime'] <= ts_end)] - + full_data = full_data[(full_data["Datetime"] >= ts_start) & (full_data["Datetime"] <= ts_end)] + elif method == "RV": - full_data.reset_index(inplace = True) + full_data.reset_index(inplace=True) ## Convert date/time change to seconds, then to days. This is most useful for intraday theta PnL - full_data['total_seconds'] = (full_data['Datetime']-full_data['Datetime'].shift(1)).dt.total_seconds()/(24*60*60) - full_data['prev_day_Datetime'] = full_data.Datetime.shift(1) - - full_data.dropna(inplace = True) - PnL_Data = full_data.apply(lambda x: fullRevalPnL( - {'S0': x['prev_day_Stock_Close'], - 'K' : asset.K, 'rf_rate' : x['prev_day_RF_rate'], - 'sigma' : x[f'prev_day_{vol_col[0]}'], 'start' : x['prev_day_Datetime'], - 'put_call' : asset.put_call, 'exp' : asset.exp, - 'y' : asset.y, 'price' : x['prev_day_Option_Close'] }, - - - {'S0': x['Stock_Close'], - 'K' : asset.K, 'rf_rate' : x['RF_rate'], - 'sigma' : x[vol_col[0]], 'start' : x['Datetime'], - 'put_call' : asset.put_call, 'exp' : asset.exp, - 'y' : asset.y, 'price' : x['Option_Close']}) - , axis = 1, result_type = 'expand') - PnL_Data.set_index('Datetime', inplace = True) + full_data["total_seconds"] = ( + full_data["Datetime"] - full_data["Datetime"].shift(1) + ).dt.total_seconds() / (24 * 60 * 60) + full_data["prev_day_Datetime"] = full_data.Datetime.shift(1) + + full_data.dropna(inplace=True) + PnL_Data = full_data.apply( + lambda x: fullRevalPnL( + { + "S0": x["prev_day_Stock_Close"], + "K": asset.K, + "rf_rate": x["prev_day_RF_rate"], + "sigma": x[f"prev_day_{vol_col[0]}"], + "start": x["prev_day_Datetime"], + "put_call": asset.put_call, + "exp": asset.exp, + "y": asset.y, + "price": x["prev_day_Option_Close"], + }, + { + "S0": x["Stock_Close"], + "K": asset.K, + "rf_rate": x["RF_rate"], + "sigma": x[vol_col[0]], + "start": x["Datetime"], + "put_call": asset.put_call, + "exp": asset.exp, + "y": asset.y, + "price": x["Option_Close"], + }, + ), + axis=1, + result_type="expand", + ) + PnL_Data.set_index("Datetime", inplace=True) PnL_Data.index = pd.to_datetime(PnL_Data.index) - full_data = full_data[(full_data['Datetime'] >= ts_start) & (full_data['Datetime'] <= ts_end)] + full_data = full_data[(full_data["Datetime"] >= ts_start) & (full_data["Datetime"] <= ts_end)] PnL_Data = PnL_Data[(PnL_Data.index >= ts_start) & (PnL_Data.index <= ts_end)] - - elif asset.__class__.__name__ == 'OptionStructure': - return_all = kwargs.get('return_all', False) - return PatchedCalculateFunc(Calculate.attribution,long_leg = asset.long, short_leg = asset.short, - return_all = return_all, ts_start = ts_start, - ts_end = ts_end, ts_timeframe = ts_timeframe, - ts_timewidth = ts_timewidth, method = method, - replace = replace, return_both_data = return_both_data) - + elif asset.__class__.__name__ == "OptionStructure": + return_all = kwargs.get("return_all", False) + return PatchedCalculateFunc( + Calculate.attribution, + long_leg=asset.long, + short_leg=asset.short, + return_all=return_all, + ts_start=ts_start, + ts_end=ts_end, + ts_timeframe=ts_timeframe, + ts_timewidth=ts_timewidth, + method=method, + replace=replace, + return_both_data=return_both_data, + ) else: raise Exception(f"Asset type {type(asset)} not supported") - + if return_both_data: return full_data, PnL_Data else: return PnL_Data -def fullRevalPnL( - start_dict, - end_dict, +def fullRevalPnL( + start_dict, + end_dict, ): """ Both dictionaries should have the same keys. As follows @@ -1198,72 +1386,88 @@ def fullRevalPnL( Rturns a dictionary containing the PnL attribution """ - assert start_dict.keys() == end_dict.keys(), f"Keys in both dictionaries must be the same. Expected {start_dict.keys()} recieved {end_dict.keys()}" - assert start_dict['K'] == end_dict['K'], f"Strike Price must be the same. Expected {start_dict['K']} recieved {end_dict['K']}" - - S0, S1, price0, price1 = start_dict['S0'], end_dict['S0'], start_dict['price'], end_dict['price'] - K, r0, r1, sigma0, sigma1, y0, y1 = start_dict['K'], start_dict['rf_rate'], end_dict['rf_rate'], start_dict['sigma'], end_dict['sigma'], start_dict['y'], end_dict['y'] - start, end = start_dict['start'], end_dict['start'] - put_call, exp = start_dict['put_call'], start_dict['exp'] + assert start_dict.keys() == end_dict.keys(), ( + f"Keys in both dictionaries must be the same. Expected {start_dict.keys()} recieved {end_dict.keys()}" + ) + assert start_dict["K"] == end_dict["K"], ( + f"Strike Price must be the same. Expected {start_dict['K']} recieved {end_dict['K']}" + ) + + S0, S1, price0, price1 = start_dict["S0"], end_dict["S0"], start_dict["price"], end_dict["price"] + K, r0, r1, sigma0, sigma1, y0, y1 = ( + start_dict["K"], + start_dict["rf_rate"], + end_dict["rf_rate"], + start_dict["sigma"], + end_dict["sigma"], + start_dict["y"], + end_dict["y"], + ) + start, end = start_dict["start"], end_dict["start"] + put_call, exp = start_dict["put_call"], start_dict["exp"] ## Delta PnL - + ## To get the Delta PnL, we start by applying a very minute bump to the spot price, then we calculate the new option price ## ie we use T_1 data to calculate the new option price, but with S0 + bump - S0_bump = S0 + 0.000001 - pv0 = Calculate.pv(S0 = S0, K = K, r = r0, sigma = sigma0, start = start, put_call = put_call, exp_date = exp, y = y0) - pv1 = Calculate.pv(S0 = S1, K = K, r = r1, sigma = sigma1, start = end, put_call = put_call, exp_date = exp, y = y1) - S0_pv_bump = Calculate.pv(S0 = S0_bump, K = K, r = r0, sigma = sigma0, start = start, put_call = put_call, exp_date = exp, y = y0) - spot_change_pv = Calculate.pv(S0 = S1, K = K, r = r0, sigma = sigma0, start = start, put_call = put_call, exp_date = exp, y = y0) + pv0 = Calculate.pv(S0=S0, K=K, r=r0, sigma=sigma0, start=start, put_call=put_call, exp_date=exp, y=y0) + pv1 = Calculate.pv(S0=S1, K=K, r=r1, sigma=sigma1, start=end, put_call=put_call, exp_date=exp, y=y1) + S0_pv_bump = Calculate.pv(S0=S0_bump, K=K, r=r0, sigma=sigma0, start=start, put_call=put_call, exp_date=exp, y=y0) + spot_change_pv = Calculate.pv(S0=S1, K=K, r=r0, sigma=sigma0, start=start, put_call=put_call, exp_date=exp, y=y0) spot_change_pnl = spot_change_pv - pv0 S0_bump_pnl = S0_pv_bump - pv0 delta_pnl = (S1 - S0) * S0_bump_pnl * 1000000 gamma_pnl = spot_change_pnl - delta_pnl - ## Vega PnL ## Similar to the Delta PnL, Vega PnL starts by applying a very minute bump to vols, then we calculate the new option price - + bump = 0.0000001 sigma_bump = sigma0 + bump - sigma0_pv_bump = Calculate.pv(S0 = S0_bump, K = K, r = r0, sigma = sigma_bump, start = start, put_call = put_call, exp_date = exp, y = y0) - sigma_change_pv = Calculate.pv(S0 = S0, K = K, r = r0, sigma = sigma1, start = start, put_call = put_call, exp_date = exp, y = y0) - sigma_plus_spot_pv = Calculate.pv(S0 = S1, K = K, r = r0, sigma = sigma1, start = start, put_call = put_call, exp_date = exp, y = y0) + sigma0_pv_bump = Calculate.pv( + S0=S0_bump, K=K, r=r0, sigma=sigma_bump, start=start, put_call=put_call, exp_date=exp, y=y0 + ) + sigma_change_pv = Calculate.pv(S0=S0, K=K, r=r0, sigma=sigma1, start=start, put_call=put_call, exp_date=exp, y=y0) + sigma_plus_spot_pv = Calculate.pv( + S0=S1, K=K, r=r0, sigma=sigma1, start=start, put_call=put_call, exp_date=exp, y=y0 + ) sigma_plus_spot_pnl = sigma_plus_spot_pv - pv0 sigma_change_pnl = sigma_change_pv - pv0 sigma_bump_pnl = sigma0_pv_bump - pv0 - vega_pnl = (sigma1 - sigma0) * sigma_bump_pnl * 1/bump + vega_pnl = (sigma1 - sigma0) * sigma_bump_pnl * 1 / bump volga_pnl = sigma_change_pnl - vega_pnl vanna_pnl = sigma_plus_spot_pnl - delta_pnl - vega_pnl - gamma_pnl - volga_pnl - + ## Theta PnL pv0 - pv0_tplus1 = Calculate.pv(S0 = S0, K = K, r = r0, sigma = sigma0, start = end, put_call = put_call, exp_date = exp, y = y0) + pv0_tplus1 = Calculate.pv(S0=S0, K=K, r=r0, sigma=sigma0, start=end, put_call=put_call, exp_date=exp, y=y0) theta_pnl = pv0_tplus1 ## Rho PnL - rho_tplus1 = Calculate.pv(S0 = S0, K = K, r = r1, sigma = sigma0, start = start, put_call = put_call, exp_date = exp, y = y0) + rho_tplus1 = Calculate.pv(S0=S0, K=K, r=r1, sigma=sigma0, start=start, put_call=put_call, exp_date=exp, y=y0) rho_pnl = rho_tplus1 ## Dividend PnL - div_tplus1 = Calculate.pv(S0 = S0, K = K, r = r0, sigma = sigma0, start = start, put_call = put_call, exp_date = exp, y = y1) + div_tplus1 = Calculate.pv(S0=S0, K=K, r=r0, sigma=sigma0, start=start, put_call=put_call, exp_date=exp, y=y1) div_pnl = div_tplus1 ## Total PnL - total_pnl = delta_pnl + gamma_pnl + vega_pnl + volga_pnl + theta_pnl + rho_pnl + vanna_pnl + d - pnl = pv1-pv0 + total_pnl = delta_pnl + gamma_pnl + vega_pnl + volga_pnl + theta_pnl + rho_pnl + vanna_pnl + div_pnl + pnl = pv1 - pv0 pnl = price1 - price0 - return {'Delta_PnL': delta_pnl*100, - 'Gamma_PnL': gamma_pnl* 100, - 'Vega_PnL': vega_pnl*100, - 'Volga_PnL': volga_pnl*100, - 'Theta_PnL': theta_pnl*100, - 'Rho_PnL': rho_pnl*100, - 'Vanna_PnL': vanna_pnl*100, - 'Dividend_PnL': div_pnl*100, - 'Total_PnL': total_pnl*100, - 'Unexplained_PnL': ((pnl)*100) - total_pnl*100, - 'Actual_PnL': (pnl*100), - 'Datetime': end, - 'Price': price1*100} + return { + "Delta_PnL": delta_pnl * 100, + "Gamma_PnL": gamma_pnl * 100, + "Vega_PnL": vega_pnl * 100, + "Volga_PnL": volga_pnl * 100, + "Theta_PnL": theta_pnl * 100, + "Rho_PnL": rho_pnl * 100, + "Vanna_PnL": vanna_pnl * 100, + "Dividend_PnL": div_pnl * 100, + "Total_PnL": total_pnl * 100, + "Unexplained_PnL": ((pnl) * 100) - total_pnl * 100, + "Actual_PnL": (pnl * 100), + "Datetime": end, + "Price": price1 * 100, + } diff --git a/trade/helpers/helper_types.py b/trade/helpers/helper_types.py index 56c7012..509f841 100644 --- a/trade/helpers/helper_types.py +++ b/trade/helpers/helper_types.py @@ -12,7 +12,7 @@ from dataclasses import dataclass, fields from functools import lru_cache from typeguard import check_type - +from typeguard._exceptions import TypeCheckError logger = setup_logger(__name__) DATE_HINT = Union[datetime, str] @@ -28,7 +28,12 @@ class TypeValidatedMixin: def _validate_field(self, name: str, value: Any) -> None: hint = _hints(type(self)).get(name) if hint is not None: - check_type(value, hint) + try: + check_type(value, hint) + except TypeCheckError as e: + raise IncorrectTypeError( + f"Field '{name}' in {type(self).__name__} expected type {hint}, but got value {value!r} of type {type(value)}. Original error: {e}" + ) from e def _validate_all_fields(self) -> None: for f in fields(self): diff --git a/trade/helpers/pricing.py b/trade/helpers/pricing.py index a5934ec..1e94a20 100644 --- a/trade/helpers/pricing.py +++ b/trade/helpers/pricing.py @@ -1,4 +1,4 @@ -from assets.rates import get_risk_free_rate_helper +from assets.rates import get_risk_free_rate_helper_v2 from helpers.parse import parse_date from bin.asset import Stock from bin.asset import Stock @@ -9,6 +9,7 @@ from helpers.Configuration import Configuration from typing import Union import warnings + warnings.filterwarnings("ignore") @@ -21,12 +22,23 @@ def time_distance_helper(exp: str, strt: str = None) -> float: parsed_dte = parse_date(exp) parsed_dte = parsed_dte.date() days = (parsed_dte - start_date).days - T = days/365 + T = days / 365 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: - ''' +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: @@ -39,7 +51,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 @@ -56,39 +68,39 @@ def binomial(K: Union[int, float], exp_date: str, sigma: float, r: float = None, else: y = 0 if r is None: - rates = get_risk_free_rate_helper() - r = rates.iloc[len(rates)-1, 0]/100 + rates = get_risk_free_rate_helper_v2() + 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) + 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) diff --git a/trade/optionlib/greeks/analytical/black_scholes.py b/trade/optionlib/greeks/analytical/black_scholes.py index 966027a..3c1ad24 100644 --- a/trade/optionlib/greeks/analytical/black_scholes.py +++ b/trade/optionlib/greeks/analytical/black_scholes.py @@ -1,8 +1,5 @@ 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 typing import List from ...core.black_scholes_math import ( black_scholes_analytic_greeks_vectorized, ) diff --git a/trade/optionlib/greeks/numerical/finite_diff.py b/trade/optionlib/greeks/numerical/finite_diff.py index cc1beff..dbf506c 100644 --- a/trade/optionlib/greeks/numerical/finite_diff.py +++ b/trade/optionlib/greeks/numerical/finite_diff.py @@ -139,5 +139,5 @@ def all_first_order(self) -> Dict[str, float]: def all_second_order(self) -> Dict[str, float]: return { 'gamma': self.second_order('S'), - 'volga': self.second_order('sigma') * 0.0001, + 'volga': self.second_order('sigma') * 0.01, } diff --git a/trade/optionlib/vol/implied_vol.py b/trade/optionlib/vol/implied_vol.py index 2391d35..4110a03 100644 --- a/trade/optionlib/vol/implied_vol.py +++ b/trade/optionlib/vol/implied_vol.py @@ -287,10 +287,10 @@ def intrinsic_check(F, K, T, r, sigma, market_price, option_type) -> bool: df = np.exp(-r * T) if option_type == "c": - intrinsic_value = df * max(F - K, 0.0) + intrinsic_value = df * max(F - K, 0.0) * 0.95 # Adding a 5% buffer to intrinsic value for calls upper_bound = df * F else: - intrinsic_value = df * max(K - F, 0.0) + intrinsic_value = df * max(K - F, 0.0) * 0.95 # Adding a 5% buffer to intrinsic value for puts upper_bound = df * K # Lower bound (intrinsic) violation @@ -454,7 +454,8 @@ def vector_vol_estimation( 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 + # Transpose args to create list of tuples + list_input = list(zip(*args)) # noqa if len(list_input) == 0: return [] From bd6f7389bcea5297b2160459b1503c5c687e5e2c Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Thu, 26 Mar 2026 08:31:05 -0400 Subject: [PATCH 10/81] Update local tooling and demo workspace files --- .github/copilot-instructions.md | 48 ++++++++++++++++++++++++++++++++ .vscode/settings.json | 5 ++-- EventDriven/demos/bkt_test.ipynb | 5 ++-- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4643047..c707e35 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -41,6 +41,54 @@ date_obj = to_datetime(datetime.now()) - Include Args, Returns, Raises, and Examples sections - Examples should be executable and demonstrate real-world usage +### Module-Level Docstring Format +- All Python modules must start with a module docstring as the very first statement. +- Use this schema for module docstrings: + 1) First line: one-sentence summary title ending with a period. + 2) Blank line. + 3) One short overview paragraph (2-4 lines) describing scope and purpose. + 4) Blank line. + 5) Structured sections for module schema using heading labels with trailing colons. +- Preferred section labels (choose those that apply): + - Core Classes: + - Core Dataclasses: + - Core Functions: + - Processing Flow: + - Risk/Assumptions: + - Caching Strategy: + - Usage: +- Keep section content concise and domain-specific. +- For Usage, use a short executable doctest-style snippet when practical. + +**Module docstring example:** +```python +"""Position attribution workflows for EventDriven backtests. + +Provides quantity normalization, option attribution loading, and position-level +PnL decomposition with trade-aware adjustments (fills, commission, and slippage). + +Core Dataclasses: + QuantityTimeSeries: Daily quantity state and execution metadata. + BacktestPositionAttribution: Attribution output for a single trade. + +Core Functions: + create_position_attribution: Loads and combines leg-level attribution. + compute_position_attribution: Applies quantity and trade adjustments. + compute_backtest_position_attribution: End-to-end portfolio integration. + +Processing Flow: + 1. Build trade quantity time series from ledgers. + 2. Load/aggregate raw leg attribution by date. + 3. Scale by quantity and apply open/close trade adjustments. + 4. Return normalized attribution components. + +Usage: + >>> analyzer = PositionAttributionAnalyzer(portfolio) + >>> result = analyzer.analyze_trade(trade_id) + >>> daily_attr = result.attribution +""" +``` + **Example:** ```python def get_forward_timeseries( diff --git a/.vscode/settings.json b/.vscode/settings.json index 8619b8c..a7c6353 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -22,11 +22,12 @@ "python.languageServer": "Pylance", "python.analysis.useLibraryCodeForTypes": true, "python.analysis.typeCheckingMode": "off", - "python.analysis.diagnosticMode": "workspace", + "python.analysis.diagnosticMode": "openFilesOnly", "python.analysis.indexing": true, "python.analysis.autoImportCompletions": true, "python.analysis.includeAliasesFromUserFiles": true, "python.analysis.userFileIndexingLimit": 5000, + "editor.colorDecorators": true, "python.analysis.packageIndexDepths": [ { "name": "pydantic", @@ -36,7 +37,7 @@ ], "python-envs.pythonProjects": [ { - "path": "", + "path": ".", "envManager": "ms-python.python:conda", "packageManager": "ms-python.python:conda" } diff --git a/EventDriven/demos/bkt_test.ipynb b/EventDriven/demos/bkt_test.ipynb index 2a3489a..aef8aa7 100644 --- a/EventDriven/demos/bkt_test.ipynb +++ b/EventDriven/demos/bkt_test.ipynb @@ -2929,7 +2929,7 @@ }, { "cell_type": "code", - "execution_count": 88, + "execution_count": null, "id": "c6c66607", "metadata": {}, "outputs": [ @@ -2958,7 +2958,8 @@ ], "source": [ "pm.current_positions\n", - "# pm.trades_map[\"&L:META20240920C450&S:META20240920C460\"].entries()" + "# pm.tra\n", + "#des_map[\"&L:META20240920C450&S:META20240920C460\"].entries()" ] }, { From afae83a1a329e302eefa150fb7bb7181f9f66e05 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:00 -0400 Subject: [PATCH 11/81] Refactor DataclassAssignmentValidationMixin and type utilities --- EventDriven/types.py | 38 ++++- trade/datamanager/utils/data_structure.py | 2 +- trade/helpers/helper_types.py | 165 ++++++++++------------ 3 files changed, 108 insertions(+), 97 deletions(-) diff --git a/EventDriven/types.py b/EventDriven/types.py index be2e662..04a11b6 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -10,16 +10,18 @@ logger = setup_logger("EventDriven.types") + class OptionFloat(float): """Custom float type for option-related values to allow for future extensions or validations.""" - + def __new__(cls, value): return super().__new__(cls, value) - + def __init__(self, value, dollar_normalized=False): super().__init__() self.dollar_normalized = dollar_normalized + class Metrics(TypedDict): spread_pct_ratio: Optional[float] spread_oi: Optional[float] @@ -29,6 +31,15 @@ class Metrics(TypedDict): max_moneyness: Optional[float] +class Scores(TypedDict): + moneyness_score: Optional[float] + dte_score: Optional[float] + mid_score: Optional[float] + pct_spread_score: Optional[float] + oi_score: Optional[float] + theta_burden_score: Optional[float] + + class SignalID(str): """Unique identifier for a trading signal. @@ -99,6 +110,7 @@ class OrderDict(TypedDict): date: date data: OrderDataDict metrics: Metrics | None + scores: Scores | None class PositionsDict(TypedDict): @@ -283,6 +295,7 @@ class Order: date: date data: OrderData metrics: Metrics = None + scores: Scores = None def __getitem__(self, key): """Get item like a dict, dict[key]""" @@ -294,7 +307,7 @@ def __setitem__(self, key, value): def __repr__(self): """String representation of Order""" - return f"Order(signal_id={self.signal_id}), data={self.data}, result={self.result}, metrics={self.metrics})" + return f"Order(signal_id={self.signal_id}), data={self.data}, result={self.result}, metrics={self.metrics}, scores={self.scores})" def get(self, key: str, default: Any = None) -> Any: """Get item like a dict, dict.get()""" @@ -326,6 +339,7 @@ def to_dict(self) -> OrderDict: date=self.date, data=data_dict, metrics=self.metrics, + scores=self.scores, ) # Return the main dictionary @@ -337,6 +351,7 @@ def from_dict(d: Dict[str, Any]) -> "Order": # Extract the nested data dict data_dict = d["data"] metrics = d.get("metrics", None) + scores = d.get("scores", None) # Convert nested data dict to OrderData object if data_dict is None: @@ -344,8 +359,8 @@ def from_dict(d: Dict[str, Any]) -> "Order": if metrics is not None: d["metrics"] = Metrics( - spread_pct_ratio=metrics["spread_pct_ratio"], - spread_oi=metrics["spread_oi"], + spread_pct_ratio=metrics["spread_pct_ratio"], + spread_oi=metrics["spread_oi"], min_dte=metrics.get("min_dte", None), max_dte=metrics.get("max_dte", None), min_moneyness=metrics.get("min_moneyness", None), @@ -354,6 +369,18 @@ def from_dict(d: Dict[str, Any]) -> "Order": else: d["metrics"] = None + if scores is not None: + d["scores"] = Scores( + moneyness_score=scores.get("moneyness_score", None), + dte_score=scores.get("dte_score", None), + mid_score=scores.get("mid_score", None), + pct_spread_score=scores.get("pct_spread_score", None), + oi_score=scores.get("oi_score", None), + theta_burden_score=scores.get("theta_burden_score", None), + ) + else: + d["scores"] = None + order_data = OrderData( trade_id=data_dict["trade_id"], long=data_dict.get("long", []), @@ -372,4 +399,5 @@ def from_dict(d: Dict[str, Any]) -> "Order": date=date, data=order_data, metrics=d["metrics"], + scores=d["scores"], ) diff --git a/trade/datamanager/utils/data_structure.py b/trade/datamanager/utils/data_structure.py index 3c56803..88a8d76 100644 --- a/trade/datamanager/utils/data_structure.py +++ b/trade/datamanager/utils/data_structure.py @@ -9,7 +9,7 @@ from trade import HOLIDAY_SET logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) - +PANDAS_DATA_HINT = Union[pd.Series, pd.DataFrame] def _data_structure_sanitize( df: Union[pd.Series, pd.DataFrame], diff --git a/trade/helpers/helper_types.py b/trade/helpers/helper_types.py index 509f841..5358099 100644 --- a/trade/helpers/helper_types.py +++ b/trade/helpers/helper_types.py @@ -1,13 +1,12 @@ - from typing import Iterable, TypedDict, Any from enum import Enum from datetime import datetime +import numbers 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, Type, Dict -import types +from typing import Union, get_type_hints, Type, Dict, get_origin, get_args from trade.helpers.Logging import setup_logger from dataclasses import dataclass, fields from functools import lru_cache @@ -18,16 +17,38 @@ DATE_HINT = Union[datetime, str] - @lru_cache(maxsize=None) def _hints(cls: Type[Any]) -> Dict[str, Any]: return get_type_hints(cls) +def _hint_allows_numbers_number(hint: Any) -> bool: + """Return True when a type hint includes numbers.Number.""" + if hint is numbers.Number: + return True + + origin = get_origin(hint) + if origin is None: + return False + + return any(_hint_allows_numbers_number(arg) for arg in get_args(hint)) + + class TypeValidatedMixin: + """Backward-compatible alias mixin for dataclass type validation. + + Prefer `DataclassTypeValidationMixin` for new code. This class remains to + avoid breaking existing imports. + """ + def _validate_field(self, name: str, value: Any) -> None: hint = _hints(type(self)).get(name) if hint is not None: + # bool is a subclass of int; reject it explicitly for numeric hints. + if isinstance(value, bool) and _hint_allows_numbers_number(hint): + raise IncorrectTypeError( + f"Field '{name}' in {type(self).__name__} expected a numeric value, but got boolean {value!r}." + ) try: check_type(value, hint) except TypeCheckError as e: @@ -43,107 +64,69 @@ def __post_init__(self) -> None: self._validate_all_fields() -@dataclass -class MutableValidated(TypeValidatedMixin): +class DataclassTypeValidationMixin(TypeValidatedMixin): + """Validate annotated dataclass fields after initialization. + + This mixin is designed for both stdlib dataclasses and pydantic dataclasses. + It performs full-object validation in `__post_init__` using type annotations. + + Usage: + - Mutable models should combine this with + `DataclassAssignmentValidationMixin`. + - Frozen models should use this mixin (or `FrozenTypeValidationMixin`) and + rely on the dataclass decorator's `frozen=True` behavior. + """ + + +class DataclassAssignmentValidationMixin(DataclassTypeValidationMixin): + """Add post-init assignment validation for mutable dataclasses. + + A constructor gate is used so assignment checks only run after initialization + has completed. This avoids false positives while dataclass/pydantic populates + fields during construction. + """ + + _validation_ready: bool = False + + def __post_init__(self, *args: Any, **kwargs: Any) -> None: + super().__post_init__() + object.__setattr__(self, "_validation_ready", True) + def __setattr__(self, name: str, value: Any) -> None: - self._validate_field(name, value) + ready = getattr(self, "_validation_ready", False) + if ready and name in getattr(self, "__dict__", {}): + self._validate_field(name, value) super().__setattr__(name, value) -@dataclass(frozen=True) -class FrozenValidated(TypeValidatedMixin): +class FrozenTypeValidationMixin(DataclassTypeValidationMixin): + """Marker mixin for frozen dataclasses. + + This class intentionally adds no assignment behavior. Immutability is + provided by `@dataclass(frozen=True)` or pydantic dataclass `frozen=True`. + """ + pass -# frozen update pattern: -# new_obj = replace(old_obj, some_field=new_value) +@dataclass +class MutableValidated(DataclassAssignmentValidationMixin): + """Legacy mutable validated dataclass kept for compatibility.""" + pass -class IncorrectTypeError(Exception): - """Custom exception for incorrect type errors in configuration validation.""" +@dataclass(frozen=True) +class FrozenValidated(FrozenTypeValidationMixin): + """Legacy frozen validated dataclass kept for compatibility.""" 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 +class IncorrectTypeError(Exception): + """Custom exception for incorrect type errors in configuration validation.""" - 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 + pass class OptionTickMetaData(TypedDict): @@ -233,4 +216,4 @@ def is_iterable(obj: Any, include_str: bool = False) -> bool: if include_str: return isinstance(obj, Iterable) else: - return isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)) \ No newline at end of file + return isinstance(obj, Iterable) and not isinstance(obj, (str, bytes)) From 9015f5ea44910976a09f53271aca7d31a71898e6 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:10 -0400 Subject: [PATCH 12/81] Add ScoringConfigs and migrate BaseConfigs to validation mixin --- EventDriven/configs/base.py | 22 +++++--------- EventDriven/configs/core.py | 58 +++++++++++++++++++++++++++++++------ 2 files changed, 56 insertions(+), 24 deletions(-) diff --git a/EventDriven/configs/base.py b/EventDriven/configs/base.py index ec56c4e..87d0e6f 100644 --- a/EventDriven/configs/base.py +++ b/EventDriven/configs/base.py @@ -4,7 +4,9 @@ from pydantic import ConfigDict, Field from pydantic.dataclasses import dataclass as _pydantic_dataclass from typing_extensions import dataclass_transform -from trade.helpers.helper_types import validate_inputs +from trade.helpers.helper_types import ( + DataclassAssignmentValidationMixin, +) from weakref import WeakSet from EventDriven.exceptions import BacktestConfigAttributeError @@ -21,7 +23,7 @@ def pydantic_dataclass(*args: Any, **kwargs: Any) -> Callable[..., Any]: @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True), kw_only=True) -class BaseConfigs: +class BaseConfigs(DataclassAssignmentValidationMixin): """Base configuration class for all modules.""" _registry: ClassVar[WeakSet[type]] = WeakSet() @@ -41,17 +43,7 @@ def get(self, key: str): return getattr(self, key) def __post_init__(self, ctx=None): - pass - - def validate_inputs(self): - """Validate configuration inputs based on type hints.""" - validate_inputs(self) - - def __setattr__(self, name, value): - super().__setattr__(name, value) - - ## Validate inputs after setting attribute - self.validate_inputs() + super().__post_init__(ctx) def __init_subclass__(cls, **kwargs): super().__init_subclass__(**kwargs) @@ -133,7 +125,7 @@ def display_and_describe_all_configs(cls): return for config_cls in cls._registry: class_desc = get_config_class_description(config_cls.__name__) - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"Configuration Class: {config_cls.__name__}") if class_desc: print(f"Description: {class_desc}") @@ -150,7 +142,7 @@ class _CustomFrozenBaseConfigs(BaseConfigs): def __setattr__(self, name, value): allow_name_changes = ["run_name"] if name in allow_name_changes: - logger.warning(f"Attempting to set attribute '{name}' to '{value}' in {self.__class__.__name__}...") + logger.info(f"Attempting to set attribute '{name}' to '{value}' in {self.__class__.__name__}...") super().__setattr__(name, value) return if name in self.__dict__: diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 98698fa..ed78ece 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -1,7 +1,7 @@ from EventDriven.configs.base import pydantic_dataclass from pydantic import ConfigDict import numbers -from typing import Union, Tuple, List, Literal, Dict +from typing import Union, Tuple, List, Literal, Dict, Optional from datetime import datetime, date import pandas as pd from abc import ABC @@ -12,6 +12,7 @@ ) from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE from trade.helpers.Logging import setup_logger + logger = setup_logger("EventDriven.configs.core") @@ -22,8 +23,8 @@ class ChainConfig(BaseConfigs): Ultimately, it would be used to filter the chain data retrieved from the data source. """ - max_pct_width: numbers.Number = None - min_oi: numbers.Number = None + max_pct_width: numbers.Number = 0.2 + min_oi: numbers.Number = 25 enable_delta_filter: bool = False @@ -87,7 +88,7 @@ class ZscoreSizerConfigs(BaseSizerConfigs): """ sizing_lev: numbers.Number = 1.0 - rvol_window: Union[numbers.Number, Tuple[numbers.Number, ...]] = None + rvol_window: Optional[Union[numbers.Number, Tuple[numbers.Number, ...]]] = None rolling_window: numbers.Number = 100 weights: Tuple[numbers.Number, numbers.Number, numbers.Number] = (0.5, 0.3, 0.2) vol_type: str = "mean" @@ -156,7 +157,7 @@ class BaseCogConfig(BaseConfigs): Each cog will usually subclass this for its specific settings. """ - name: str = None + name: Optional[str] = None enabled: bool = True @@ -223,7 +224,6 @@ class BacktesterConfig(BaseConfigs): min_slippage_pct: float = 0.075 max_slippage_pct: float = 0.15 commission_per_contract_in_units: float = 0.0065 - @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) @@ -265,16 +265,56 @@ class RiskManagerConfig(BaseConfigs): cache_order_requests: bool = False -@pydantic_dataclass +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) class MeanReversionSizerConfigs(BaseCogConfig): beta: float = 0.5 name: str = "custom_mean_reversion_sizer" min_scale: float = 0.5 max_scale: float = 2.0 - sizing_lev: int = 3 + sizing_lev: int = 2 default_dte: int = 10 enabled_limits: StrategyLimitsEnabled = Field( default_factory=lambda: StrategyLimitsEnabled(delta=False, dte=True, moneyness=False) ) # Minimum z-score threshold to trigger scaling adjustments - min_zscore: float = 2.5 + min_zscore: float = 2.5 + + +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) +class ScoringConfigs(BaseConfigs): + tilt_strength: numbers.Number = 0.2 + spread_ticks: int = 2 + structure_direction: Literal["long", "short"] = "long" + strategy: Literal["vertical", "naked"] = "vertical" + + # Moneyness + m_target: numbers.Number = 0.8 + min_moneyness: numbers.Number = 0.45 + max_moneyness: numbers.Number = 1.05 + m_sigma: numbers.Number = 0.2 + m_tilt: Literal["otm", "itm", "atm"] = "otm" + + # DTE + target_dte: numbers.Number = 200 + dte_tolerance: numbers.Number = 100 + dte_sigma: numbers.Number = 10 + dte_tilt: Literal["flat", "short", "long"] = "short" + + # Mid price + mid_min: numbers.Number = 0.5 + mid_max: numbers.Number = 3.0 + mid_upper_limit: numbers.Number = 5 + mid_lower_limit: numbers.Number = 0.25 + mid_sigma: numbers.Number = 1.0 + + # Spread + pct_spread_max: numbers.Number = 1.0 + target_spread_pct: numbers.Number = 0.2 + pct_spread_sigma: numbers.Number = 0.10 + + # Open Interest + oi_target: int = 1000 + + # Theta burden + theta_burden_max: numbers.Number = 0.03 + theta_burden_sigma: numbers.Number = 0.02 From d4dbaf7588ff726e96d3386359428748f99c847a Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:19 -0400 Subject: [PATCH 13/81] Add continuous dividend adjustment to chain Greeks calculation --- EventDriven/riskmanager/picker/iv_helper.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/EventDriven/riskmanager/picker/iv_helper.py b/EventDriven/riskmanager/picker/iv_helper.py index e357472..66d807b 100644 --- a/EventDriven/riskmanager/picker/iv_helper.py +++ b/EventDriven/riskmanager/picker/iv_helper.py @@ -6,11 +6,16 @@ from trade.optionlib.greeks.analytical.black_scholes import black_scholes_analytic_greeks_vectorized from trade.datamanager import RatesDataManager import pandas as pd +from trade.datamanager import DividendDataManager +from trade.datamanager._enums import DivType +import numpy as np CHAIN_GREEKS_CACHE = load_riskmanager_cache(target="chain_greeks_cache", create_on_missing=True, clear_on_exit=False) rates_cache = {} + + def get_rates_on_date(date): string_date = pd.to_datetime(date).strftime("%Y-%m-%d") if string_date in rates_cache: @@ -22,6 +27,8 @@ def get_rates_on_date(date): def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chain_spot: float) -> pd.DataFrame: + if filtered.empty: + return filtered date = pd.to_datetime(date).date() ## get rates data for the date at_time = get_rates_on_date(date) @@ -50,13 +57,17 @@ def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chai ## Filter out the contracts that were found in the cache to avoid redundant calculations if cached_data: nan_iv_chain = nan_iv_chain[~nan_iv_chain.index.isin(cached_data.keys())] + ## Get dividend data + div_dm = DividendDataManager(filtered["root"].iloc[0]) # Assuming all contracts in the chain have the same root + q = div_dm.get_schedule(date, date, DivType.CONTINUOUS).timeseries.values[0] ## Calculate forward price for the contracts with NaN iv - t = time_distance_helper([date] * len(nan_iv_chain["expiration"].values), nan_iv_chain["expiration"].values) + t = np.array(time_distance_helper([date] * len(nan_iv_chain["expiration"].values), nan_iv_chain["expiration"].values)) + q_factor = np.exp(-q * t) f = vectorized_forward_continuous( S=[chain_spot] * len(nan_iv_chain["expiration"].values), r=[at_time] * len(nan_iv_chain["expiration"].values), - q_factor=[1] * len(nan_iv_chain["expiration"].values), + q_factor=q_factor, T=t, ) From f444fef750116ee310212cf24acdb08271f3abff Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:28 -0400 Subject: [PATCH 14/81] Add chain scoring framework for score-based order selection --- EventDriven/riskmanager/picker/builder.py | 157 +++++++++++-- .../riskmanager/picker/chain_scoring.py | 213 ++++++++++++++++++ 2 files changed, 353 insertions(+), 17 deletions(-) create mode 100644 EventDriven/riskmanager/picker/chain_scoring.py diff --git a/EventDriven/riskmanager/picker/builder.py b/EventDriven/riskmanager/picker/builder.py index 2ea1bea..0b37d3d 100644 --- a/EventDriven/riskmanager/picker/builder.py +++ b/EventDriven/riskmanager/picker/builder.py @@ -1,16 +1,22 @@ +import numpy as np from typing import Optional from trade.helpers.helper import parse_option_tick from EventDriven.riskmanager.picker import filter_contracts, OrderSchema +from EventDriven.configs.core import ScoringConfigs import pandas as pd from trade.helpers.Logging import setup_logger -import numpy as np -from .vertical_spread import vertical_spread_order_builder -from .naked_option import naked_option_order_builder +from EventDriven.riskmanager.utils import populate_cache_with_chain +from EventDriven.dataclasses.orders import OrderRequest +from .vertical_spread import _extract_order_for_vertical_spread, vertical_spread_order_builder, vertical_spread_pairer_by_exp +from .naked_option import _extract_order_for_naked_option, naked_option_order_builder, naked_option_by_exp from ...types import ResultsEnum, OrderData from .iv_helper import _add_greeks_and_iv_to_chain +from .chain_scoring import _score_chain + + -logger = setup_logger("EventDriven.riskmanager.picker.builder") +logger = setup_logger("EventDriven.riskmanager.picker.builder", stream_log_level="WARNING") BUILDER_FACTORY = { "vertical": vertical_spread_order_builder, @@ -72,11 +78,14 @@ def validate_order(order: dict, date: pd.Timestamp, spot: Optional[float] = None def order_builder( - unfiltered_chain: pd.DataFrame, - schema: OrderSchema, - spot: float, - date: pd.Timestamp, + unfiltered_chain: pd.DataFrame = None, + schema: OrderSchema = None, + spot: float = None, + date: pd.Timestamp = None, delta_lmt: Optional[float] = None, + *, + req: Optional[OrderRequest] = None, + configs: Optional[ScoringConfigs] = None, ) -> OrderData: """ Build an order based on the unfiltered option chain and the provided schema. @@ -86,6 +95,11 @@ def order_builder( Returns: OrderData: Detailed trade execution data including positions and pricing. """ + if req is not None and configs is not None: + return _order_builder_with_scoring(req=req, configs=configs) + + if unfiltered_chain is None or schema is None or spot is None or date is None: + raise ValueError("unfiltered_chain, schema, spot, and date must all be provided if req and configs are not used.") # Step 1: Filter contracts based on schema filtered_chain = filter_contracts( df=unfiltered_chain, @@ -93,16 +107,8 @@ def order_builder( spot=spot, ) logger.info(f"Recieved {len(unfiltered_chain)} contracts from data source. Delta limit for this order: {delta_lmt}") - - ## If delta filter is enabled, calculate Greeks and IV for the filtered chain to apply delta-based filtering in the builder functions. - ## If the chain is empty after initial filtering, we can skip this step to save computation. - ## If delta_lmt is None, it means the position manager did not provide a delta limit, so we should skip delta-based filtering in the builder functions as well. - if schema.get("enable_delta_filter", False) and not filtered_chain.empty and delta_lmt is not None: - logger.info(f"Calculating Greeks and IV for {len(filtered_chain)} contracts to apply delta filter...") + if not filtered_chain.empty: filtered_chain = _add_greeks_and_iv_to_chain(filtered_chain, date, spot) - else: - logger.info("Delta filter not enabled, skipping Greeks and IV calculation.") - filtered_chain[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = np.inf # Ensure these columns exist for builder functions, even if we are not calculating them. logger.info(f"Filtered chain size: {len(filtered_chain)} contracts after applying schema filters.") @@ -128,3 +134,120 @@ def order_builder( raise ValueError(f"Order validation failed: {e}") from e return order + +def _order_builder_with_scoring( + req: OrderRequest, + configs: ScoringConfigs, +) -> OrderData: + """ + Build an order using the scored chain, which includes additional scoring metrics for better decision-making. + This function is currently not used in the main builder flow but can be integrated for enhanced order construction based on scoring. + """ + + ## Step 1: Build scored chain + scored_chain = build_scored_chain(req=req, configs=configs) + if scored_chain.empty: + logger.warning(f"Following are used for filtering but resulted in empty chain: min_moneyness={configs.min_moneyness}, max_moneyness={configs.max_moneyness}, target_dte={configs.target_dte}, dte_tolerance={configs.dte_tolerance}, spread_ratio_max={configs.pct_spread_max}, mid_price_range=({configs.mid_lower_limit}, {configs.mid_upper_limit})") + order = _extract_order_with_scoring_config(chain_row=pd.Series(), configs=configs) + order["result"] = ResultsEnum.NO_CONTRACTS_FOUND.value + return order + + ## Step 2: Extract top scored row and build order from it + top_scored_row = scored_chain.iloc[0] + + ## Note: The order extraction functions will need to be updated to accept the additional scoring metrics if we want to utilize them in the order construction logic. For now, we will just pass the top scored row as is, and the extraction functions can decide how to use the scoring metrics if needed. + order = _extract_order_with_scoring_config(chain_row=top_scored_row, configs=configs) + validate_order(order, date=req.date, spot=req.chain_spot) + return order + +def build_scored_chain(req: OrderRequest, configs: ScoringConfigs) -> pd.DataFrame: + """ + Build a scored option chain based on the request and scoring configurations. This involves fetching the chain, filtering it, adding Greeks and IV, pairing contracts if necessary, and then applying the scoring functions to rank the contracts. + The resulting DataFrame will include the original contract data along with the calculated scores for each contract, allowing for informed decision-making in order construction. + Steps: + 1. Fetch the option chain data for the given ticker and date. + 2. Filter the chain based on the request parameters (option type, moneyness, DTE, etc.). + 3. Add Greeks and implied volatility to the filtered chain. + 4. If the strategy involves spreads, pair the contracts accordingly. + 5. Apply the scoring functions to the resulting chain to calculate scores for moneyness, DTE, mid price, percentage spread, open interest, and theta burden. + 6. Return the scored chain sorted by total score in descending order. + + Additional scoring functions can be defined in the chain_scoring module, and the _score_chain function will apply these functions to the appropriate columns in the DataFrame to calculate the scores. The final scored chain will have additional columns for each score, as well as a total score that can be used for ranking the contracts. + Args: + req (OrderRequest): The order request containing parameters for fetching and filtering the option chain. + configs (ScoringConfigs): The scoring configurations that define how the scores are calculated and weighted + Returns: + pd.DataFrame: A DataFrame containing the scored option chain, with additional columns for each + """ + + ## Step 1: Get chain + chain = populate_cache_with_chain(tick=req.symbol, date=req.date, chain_spot=req.chain_spot) + + ## Step 2: Filter chain + filtered = filter_contracts( + df=chain, + option_type=req.option_type, + spot=req.chain_spot, + min_moneyness=configs.min_moneyness, + max_moneyness=configs.max_moneyness, + target_dte=configs.target_dte, + dte_tol=configs.dte_tolerance, + ) + if filtered.empty: + return filtered + + ## Step 3: Add Greeks and IV + filtered = _add_greeks_and_iv_to_chain(filtered=filtered, date=req.date, chain_spot=req.chain_spot) + + ## Step 4: Pair vertical spreads + is_call = req.option_type.lower() == "c" + filtered = filtered.sort_values(by="strike", ascending=is_call).reset_index(drop=True) + vertical_chain = ( + filtered.groupby("expiration") + .apply( + naked_option_by_exp if configs.strategy == "naked" else vertical_spread_pairer_by_exp, + spread_tick=configs.spread_ticks, + min_total_price=configs.mid_lower_limit, + max_total_price=configs.mid_upper_limit, + max_pct_width=configs.pct_spread_max, + min_oi=-25, + delta_lmt=np.inf, + ) + .reset_index(drop=True) + ) + + ## Step 5: Score pairs + scored_chain = _score_chain(vertical_chain, configs=configs) + scored_chain = scored_chain[ + [ + "long_leg_opttick", + "short_leg_opttick", + "spread_moneyness", + "moneyness_score", + "dte_score", + "spread_dte", + "mid_score", + "spread_mid", + "spread_pct_ratio", + "pct_spread_score", + "oi_score", + "spread_oi", + "theta_burden_score", + "spread_delta", + "total_score", + ] + ].sort_values("total_score", ascending=False) + return scored_chain + +def _extract_order_with_scoring_config(chain_row: pd.Series, configs: ScoringConfigs) -> OrderSchema: + """ + Extract order schema from a scored chain row, applying any necessary adjustments based on scoring configs. + """ + + schema = {"structure_direction": configs.structure_direction} + return ( + _extract_order_for_naked_option(chain_row, schema=schema) + if configs.strategy == "naked" + else _extract_order_for_vertical_spread(chain_row, schema=schema) + ) + diff --git a/EventDriven/riskmanager/picker/chain_scoring.py b/EventDriven/riskmanager/picker/chain_scoring.py new file mode 100644 index 0000000..486ec69 --- /dev/null +++ b/EventDriven/riskmanager/picker/chain_scoring.py @@ -0,0 +1,213 @@ +import numpy as np +from EventDriven.configs.core import ScoringConfigs +import pandas as pd +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.picker.chain_scoring") + + +def directional_moneyness(strike, forward, option_type): + option_type = option_type.lower() + if option_type == "put": + return strike / forward + elif option_type == "call": + return forward / strike + raise ValueError("option_type must be 'call' or 'put'") + + +def moneyness_score( + m, + target_m=0.8, + sigma=0.1, + tilt="otm", + tilt_strength=0.1, +): + + base = np.exp(-((m - target_m) ** 2) / (2 * sigma**2)) + + if tilt == "flat": + mult = 1.0 + elif tilt == "otm": + mult = 1.0 + tilt_strength if m < 1.0 else 1.0 - tilt_strength + elif tilt == "itm": + mult = 1.0 + tilt_strength if m > 1.0 else 1.0 - tilt_strength + elif tilt == "atm": + mult = 1.0 + tilt_strength * np.exp(-((m - 1.0) ** 2) / (2 * sigma**2)) + else: + raise ValueError("tilt must be one of: flat, otm, itm, atm") + + return 10.0 * base * mult + + +def dte_score( + dte, + target_dte=60, + sigma=10, + tilt="flat", + tilt_strength=0.1, +): + """ + Score DTE around a target using a Gaussian-style score. + + Parameters + ---------- + dte : float + Contract DTE in calendar days. + target_dte : float + Desired DTE target. + sigma : float + Width / tolerance in days. + tilt : str + One of: 'flat', 'short', 'long' + tilt_strength : float + Preference strength when DTE is on one side of target. + + Returns + ------- + float + Score scaled to max about 10. + """ + base = np.exp(-((dte - target_dte) ** 2) / (2 * sigma**2)) + + if tilt == "flat": + mult = 1.0 + elif tilt == "short": + mult = 1.0 + tilt_strength if dte < target_dte else 1.0 - tilt_strength + elif tilt == "long": + mult = 1.0 + tilt_strength if dte > target_dte else 1.0 - tilt_strength + else: + raise ValueError("tilt must be one of: 'flat', 'short', 'long'") + + return 10.0 * base * mult + + +def mid_band_score(mid, min_mid=1.0, max_mid=5.0, sigma=1.0): + if mid <= 0: + return 0.0 + + if min_mid <= mid <= max_mid: + return 10.0 + + if mid < min_mid: + diff = min_mid - mid + else: + diff = mid - max_mid + + return 10.0 * np.exp(-(diff**2) / (2 * sigma**2)) + + +def pct_spread_score(bid, ask, max_pct_spread=0.20, sigma=0.10): + if bid <= 0 or ask <= 0 or ask < bid: + return 0.0 + + mid = 0.5 * (bid + ask) + if mid <= 0: + return 0.0 + + pct_spread = (ask - bid) / mid + + if pct_spread <= max_pct_spread: + return 10.0 + + excess = pct_spread - max_pct_spread + return 10.0 * np.exp(-(excess**2) / (2 * sigma**2)) + + +def oi_score(oi, target_oi=500): + """ + Soft score for open interest. + - 0 if oi <= 0 + - approaches 10 as oi gets large + - log scaling avoids huge OI dominating + """ + if oi is None or oi <= 0: + return 0.0 + + return 10.0 * min(np.log1p(oi) / np.log1p(target_oi), 1.0) + + +def theta_burden_score_from_mid(theta, mid, max_theta_burden=0.03, sigma=0.02): + if mid is None or mid <= 0: + return 0.0 + + if np.isnan(theta) or np.isnan(mid): + return 0.0 + + burden = abs(theta) / mid + + if burden <= max_theta_burden: + return 10.0 + + excess = burden - max_theta_burden + return 10.0 * np.exp(-(excess**2) / (2 * sigma**2)) + + +def _score_chain(structure_chain: pd.DataFrame, configs: ScoringConfigs) -> pd.DataFrame: + """ + Score the chain using the defined scoring functions and configs. + """ + + structure_chain["moneyness_score"] = structure_chain["spread_moneyness"].apply( + lambda x: ( + moneyness_score( + m=np.log(x), + target_m=np.log(configs.m_target), + sigma=configs.m_sigma, + tilt=configs.m_tilt, + tilt_strength=configs.tilt_strength, + ) + if pd.notna(x) and x > 0 + else np.nan + ) + ) + structure_chain["dte_score"] = structure_chain["spread_dte"].apply( + lambda x: ( + dte_score( + dte=x, + target_dte=configs.target_dte, + sigma=configs.dte_sigma, + tilt=configs.dte_tilt, + tilt_strength=configs.tilt_strength, + ) + if pd.notna(x) + else np.nan + ) + ) + structure_chain["mid_score"] = structure_chain["spread_mid"].apply( + lambda x: mid_band_score(mid=x, min_mid=configs.mid_min, max_mid=configs.mid_max, sigma=configs.mid_sigma) + ) + + structure_chain["pct_spread_score"] = structure_chain.apply( + lambda row: pct_spread_score( + bid=row["spread_bid"], + ask=row["spread_ask"], + max_pct_spread=configs.target_spread_pct, + sigma=configs.pct_spread_sigma, + ), + axis=1, + ) + structure_chain["oi_score"] = structure_chain["spread_oi"].apply( + lambda x: oi_score( + oi=x, + target_oi=configs.oi_target, + ) + ) + + structure_chain["theta_burden_score"] = structure_chain.apply( + lambda row: theta_burden_score_from_mid( + theta=row["spread_theta"], + mid=row["spread_mid"], + max_theta_burden=configs.theta_burden_max, + sigma=configs.theta_burden_sigma, + ), + axis=1, + ) + structure_chain["total_score"] = ( + structure_chain["moneyness_score"] + + structure_chain["dte_score"] + + structure_chain["mid_score"] + + structure_chain["pct_spread_score"] + + structure_chain["oi_score"] + + structure_chain["theta_burden_score"] + ) + return structure_chain From 3ee9e23c2200c435a8cce515b9b5b38fbb943b5b Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:38 -0400 Subject: [PATCH 15/81] Extend picker with scoring columns, common pair mask, and flexible filter_contracts API --- EventDriven/riskmanager/picker/__init__.py | 21 ++-- .../riskmanager/picker/naked_option.py | 95 +++++++++++++++++-- .../riskmanager/picker/order_picker.py | 77 ++++++++++++++- EventDriven/riskmanager/picker/utils.py | 29 +++++- .../riskmanager/picker/vertical_spread.py | 60 ++++++++++-- 5 files changed, 248 insertions(+), 34 deletions(-) diff --git a/EventDriven/riskmanager/picker/__init__.py b/EventDriven/riskmanager/picker/__init__.py index 7f7bd5b..3308dfa 100644 --- a/EventDriven/riskmanager/picker/__init__.py +++ b/EventDriven/riskmanager/picker/__init__.py @@ -160,25 +160,30 @@ def get(self, key, default=None): def filter_contracts( df: pd.DataFrame, - schema: OrderSchema, - spot: float, + schema: OrderSchema = None, + spot: float = None, + option_type: str = None, min_moneyness: float = 0.5, max_moneyness: float = 1.5, increment=0.05, + dte_tol: int = 5, + target_dte: int = 30, ) -> pd.DataFrame: + schema = schema or {} df = df.copy() - df = df[df["right"].str.lower() == schema["option_type"].lower()] + right = option_type if option_type else schema.get("option_type") + df = df[df["right"].str.lower() == right.lower()] if df.empty: - raise ValueError(f"No contracts found for {schema['option_type']} in the provided chain.") + raise ValueError(f"No contracts found for {right} in the provided chain.") ## Calculate Moneyness - if schema["option_type"].lower() == "c": + if right.lower() == "c": df["moneyness"] = spot / df["strike"] else: df["moneyness"] = df["strike"] / spot - target_dte = schema["target_dte"] - dte_tol = schema["dte_tolerance"] + target_dte = schema.get("target_dte", target_dte) + dte_tol = schema.get("dte_tolerance", dte_tol) filtered = pd.DataFrame() attempt = 0 min_moneyness = schema.get("min_moneyness", min_moneyness) @@ -199,7 +204,7 @@ def filter_contracts( if filtered.empty: logger.critical( - f"Failed to filter contracts: No contracts found for {schema['option_type']} with DTE {target_dte} ± {dte_tol} and strike range [{min_moneyness:.2f}, {max_moneyness:.2f}] after {attempt} attempts." + f"Failed to filter contracts: No contracts found for {right} with DTE {target_dte} ± {dte_tol} and strike range [{min_moneyness:.2f}, {max_moneyness:.2f}] after {attempt} attempts." ) return filtered.reset_index(drop=True) diff --git a/EventDriven/riskmanager/picker/naked_option.py b/EventDriven/riskmanager/picker/naked_option.py index bda3b43..b0da851 100644 --- a/EventDriven/riskmanager/picker/naked_option.py +++ b/EventDriven/riskmanager/picker/naked_option.py @@ -5,19 +5,20 @@ from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema from trade.helpers.Logging import setup_logger -from .utils import _verify_delta_in_chain, _delta_lmt +from .utils import _verify_delta_in_chain, _delta_lmt, _build_common_pair_mask logger = setup_logger("EventDriven.riskmanager.picker.naked_option") ## Utility function to pair naked option legs and calculate spread metrics by expiration date. def naked_option_by_exp( - row: pd.Series, + row: pd.DataFrame, min_total_price: float = 0.5, max_total_price: float = 1.0, max_pct_width: float = np.inf, min_oi: int = 0, delta_lmt: Optional[float] = None, + **kwargs, ) -> pd.DataFrame: """ For a given row (option contract), find the corresponding leg of the naked option based on the spread_tick. @@ -44,43 +45,93 @@ def naked_option_by_exp( bid_ask_spread = spread_ask - spread_bid spread_pct_ratio = abs(spread_bid - spread_ask) / spread_mid.replace(0, np.nan) # Avoid division by zero. spread_oi = abs(long_leg_details["open_interest"]) + spread_moneyness = ( + row["moneyness"].reset_index(drop=True) + if "moneyness" in row.columns + else pd.Series(np.nan, index=long_leg_details.index) + ) + spread_dte = ( + row["dte"].reset_index(drop=True) if "dte" in row.columns else pd.Series(np.nan, index=long_leg_details.index) + ) + spread_theta = ( + row["theta"].reset_index(drop=True) + if "theta" in row.columns + else pd.Series(np.nan, index=long_leg_details.index) + ) + spread_volume = ( + abs(row["volume"].reset_index(drop=True)) + if "volume" in row.columns + else pd.Series(np.nan, index=long_leg_details.index) + ) + short_leg_opttick = pd.Series(np.nan, index=long_leg_details.index) ## Combine into a DataFrame for analysis. paired_opttick = pd.concat( ( long_leg_details["opttick"], + short_leg_opttick, spread_mid, spread_bid, spread_ask, bid_ask_spread, spread_pct_ratio, spread_oi, + spread_volume, spread_delta, + spread_moneyness, + spread_dte, + spread_theta, ), axis=1, ) paired_opttick.columns = [ "long_leg_opttick", + "short_leg_opttick", "spread_mid", "spread_bid", "spread_ask", "bid_ask_spread", "spread_pct_ratio", "spread_oi", + "spread_volume", "spread_delta", + "spread_moneyness", + "spread_dte", + "spread_theta", ] + shrunk_delta_lmt = round(_delta_lmt(delta_lmt) * 0.95, 2) + + full_mask = _build_common_pair_mask( + spread_mid=paired_opttick["spread_mid"], + spread_bid=paired_opttick["spread_bid"], + spread_ask=paired_opttick["spread_ask"], + spread_pct_ratio=paired_opttick["spread_pct_ratio"], + spread_oi=paired_opttick["spread_oi"], + spread_delta=paired_opttick["spread_delta"], + min_total_price=min_total_price, + max_total_price=max_total_price, + max_pct_width=max_pct_width, + min_oi=min_oi, + delta_lmt=shrunk_delta_lmt, + ) mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) spread_oi_mask = paired_opttick["spread_oi"] >= min_oi pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width spread_bid_mask = paired_opttick["spread_bid"] > 0 spread_ask_mask = paired_opttick["spread_ask"] > 0 - delta_mask = paired_opttick["spread_delta"].abs() <= abs(_delta_lmt(delta_lmt)) - full_mask = mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask - logger.debug(f"Number of naked options after applying all filters: {full_mask.sum()}") ## DEBUG + delta_mask = ( + paired_opttick["spread_delta"].abs() <= abs(shrunk_delta_lmt) + ) # Manually shrinking the delta limit by 10% to be more conservative. And avoid issues in picking options that are right on the edge of the delta limit logger.debug( - f"mid_mask: {mid_mask.sum()}, spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" + f"Number of naked options after applying all filters: {full_mask.sum()}. Number before filtering: {len(paired_opttick)}" ) ## DEBUG + logger.debug(f"mid_mask: {mid_mask.sum()} (between {min_total_price} and {max_total_price}), ") + logger.debug(f"spread_oi_mask: {spread_oi_mask.sum()} (>= {min_oi}), ") + logger.debug(f"pct_width_mask: {pct_width_mask.sum()} (<= {max_pct_width}), ") + logger.debug(f"spread_bid_mask: {spread_bid_mask.sum()}, (>0) ") + logger.debug(f"spread_ask_mask: {spread_ask_mask.sum()}, (>0) ") + logger.debug(f"delta_mask: {delta_mask.sum()} (<= {abs(shrunk_delta_lmt)})") ## DEBUG return paired_opttick[full_mask].reset_index(drop=True) @@ -102,7 +153,6 @@ def _naked_option_finder( logger.debug(f"_naked_option_finder recieved delta_lmt: {delta_lmt}") ## DEBUG if filtered_chain.empty: return pd.Series() # Return empty Series if no contracts are available after filtering. - # Start by ordering by strike, from ITM to OTM. # For calls, ITM is lower strike, for puts, ITM is higher strike. max_pct_width = schema.get("max_pct_width", 0.10) ## NOTE: Add to schema @@ -134,9 +184,26 @@ def _naked_option_finder( ## 1. spread_pct_ratio (we want this to be low, meaning the spread is relatively tight compared to its midpoint) ## 2. spread_oi (we want this to be high, meaning there's good liquidity in the spread) ## Finally, pick the top row as our chosen spread. - naked_option_chain.sort_values(by=["spread_pct_ratio", "spread_oi"], ascending=[True, False], inplace=True) + order_delta_ascending = ( + True if is_call else False + ) # For calls, we want lower delta (more negative), for puts we want higher delta (less negative). + logger.debug( + "Verfiying delta has been integrated into the naked option chain before sorting. This is crucial for ensuring the correct ordering of options based on their delta values." + ) ## DEBUG + naked_option_chain.sort_values( + by=[ + "spread_pct_ratio", + "spread_oi", + "spread_delta", + ], + ascending=[ + True, + False, + order_delta_ascending, + ], + inplace=True, + ) picked_spread = naked_option_chain.iloc[0] if not naked_option_chain.empty else pd.Series() - return picked_spread @@ -170,6 +237,7 @@ def _extract_order_for_naked_option( ## Other details from spread pct_ratio = picked_spread["spread_pct_ratio"] spread_oi = picked_spread["spread_oi"] + delta = picked_spread["spread_delta"] close_price = picked_spread["spread_mid"] trade_id = create_trade_id( @@ -185,6 +253,15 @@ def _extract_order_for_naked_option( "metrics": { "spread_pct_ratio": pct_ratio, "spread_oi": spread_oi, + "delta": delta, + }, + "scores": { + "moneyness_score": picked_spread.get("moneyness_score", np.nan), + "dte_score": picked_spread.get("dte_score", np.nan), + "mid_score": picked_spread.get("mid_score", np.nan), + "pct_spread_score": picked_spread.get("pct_spread_score", np.nan), + "oi_score": picked_spread.get("oi_score", np.nan), + "theta_burden_score": picked_spread.get("theta_burden_score", np.nan), }, } else: diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index 73bf198..369409f 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -179,6 +179,7 @@ - print_url param useful for debugging data sources """ +from copy import deepcopy from datetime import datetime import pandas as pd from EventDriven.riskmanager._order_validator import OrderInputs @@ -188,8 +189,8 @@ populate_cache_with_chain, precompute_lookbacks, ) -from EventDriven.configs.core import ChainConfig, OrderSchemaConfigs, OrderPickerConfig, OrderResolutionConfig -from EventDriven.types import ResultsEnum +from EventDriven.configs.core import ChainConfig, OrderSchemaConfigs, OrderPickerConfig, OrderResolutionConfig, ScoringConfigs +from EventDriven.types import OrderData, ResultsEnum from ..utils import ( dynamic_memoize, # noqa @@ -212,6 +213,7 @@ class OrderPicker: This class is solely responsible for picking orders based on predefined schemas and configurations. All it does is find the right order based on the schema provided. It does not execute trades or manage risk. """ + BUILD_WITH_SCORING_METHOD: bool = True # Class variable to toggle between standard builder and scoring builder methods def __init__(self, start_date: str | datetime, end_date: str | datetime): """ @@ -227,6 +229,7 @@ def __init__(self, start_date: str | datetime, end_date: str | datetime): self._order_picker_config = OrderPickerConfig(start_date=start_date, end_date=end_date) self._order_schema_config = OrderSchemaConfigs() self._order_resolution_config = OrderResolutionConfig() + self._scoring_config = ScoringConfigs() ## Others self.preset_orders = {} @@ -238,6 +241,51 @@ def __repr__(self): def lookback(self): return self.__lookback + def _overwrite_scoring_configs_with_schema_config(self, configs: ScoringConfigs = None) -> ScoringConfigs: + """ + This function takes the scoring configs and overwrites any values with the corresponding values from the order schema config if they exist. This allows us to use the order schema config as a source of truth for scoring parameters, while still allowing for dynamic overrides through the scoring configs. + """ + if configs is None: + configs = ScoringConfigs() + + ## key mapping is attr in schema config : attr in scoring config + mapping = { + "spread_ticks": "spread_ticks", + "strategy": "strategy", + "structure_direction": "structure_direction", + "min_moneyness": "min_moneyness", + "max_moneyness": "max_moneyness", + "target_dte": "target_dte", + "dte_tolerance": "dte_tolerance", + "min_total_price": "mid_min", + } + for schema_attr, scoring_attr in mapping.items(): + schema_value = getattr(self._order_schema_config, schema_attr, None) + if schema_value is not None: + setattr(configs, scoring_attr, schema_value) + return configs + + def _overwrite_configs(self): + """ + This function overwrites the scoring configs with the values from the order schema config. This is useful for ensuring that the scoring functions use the same parameters as defined in the order schema config, which is the source of truth for our order selection criteria. + """ + self._scoring_config = self._overwrite_scoring_configs_with_schema_config(self._scoring_config) + + def _get_order_with_scoring(self, req: OrderRequest) -> OrderData: + """ + This function is an alternative to the standard _get_order function that incorporates scoring into the order selection process. It builds the order using the builder function that includes scoring, and then validates the resulting order before returning it. + """ + + ## Create a copy per request. + ## This is because we'll be changing mid_max from req + configs = deepcopy(self._scoring_config) + configs.mid_upper_limit = req.max_close + order = order_builder( + req=req, + configs=configs, + ) + return order + def register_preset_order(self, signal_id: str, trade_id: str, @@ -350,6 +398,9 @@ def get_order(self, request: OrderRequest) -> Order: """ Get the order based on the request. """ + + + schema = self.get_order_schema( ticker=request.symbol, option_type=request.option_type, max_total_price=request.max_close ) @@ -357,6 +408,21 @@ def get_order(self, request: OrderRequest) -> Order: inputs = self.construct_inputs( request=request, schema=schema, order_resolution_config=self._order_resolution_config ) + + if self.BUILD_WITH_SCORING_METHOD: + order = self._get_order_with_scoring(request) + + ## Add necessary tags for identification + order["signal_id"] = inputs.signal_id + order["map_signal_id"] = inputs.signal_id + if order_failed(order): + logger.warning(f"Order failed to resolve for request: {request} with schema: {schema}") + return Order.from_dict(order) + order["data"]["quantity"] = 1 + order["date"] = pd.to_datetime(request.date).date() + order = Order.from_dict(order) + return order + if not self._chain_config.enable_delta_filter: logger.info("Delta filter not enabled in chain config. Setting delta_lmt to None for order builder.") request.delta_lmt = None # Ensure delta_lmt is None if delta filtering is not enabled @@ -377,12 +443,13 @@ def construct_inputs( if order_resolution_config is None: order_resolution_config = self._order_resolution_config - if request.max_close > request.tick_cash/100: ## Tick cash is scaled + tick_cash = request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash / 100 + if request.max_close > tick_cash: logger.warning( - f"Request max_close {request.max_close} is greater than tick_cash {request.tick_cash}. Adjusting max_close to tick_cash." + f"Request max_close {request.max_close} is greater than tick_cash {tick_cash}. Adjusting max_close to tick_cash." ) - request.max_close = request.tick_cash + request.max_close = tick_cash inputs = OrderInputs( tick=request.symbol, diff --git a/EventDriven/riskmanager/picker/utils.py b/EventDriven/riskmanager/picker/utils.py index 5fa126d..4ee0f15 100644 --- a/EventDriven/riskmanager/picker/utils.py +++ b/EventDriven/riskmanager/picker/utils.py @@ -4,6 +4,8 @@ from trade.helpers.Logging import setup_logger logger = setup_logger("EventDriven.riskmanager.picker.utils") + + def _verify_delta_in_chain(chain: pd.DataFrame) -> pd.DataFrame: """ Verify if the 'delta' column exists in the option chain DataFrame. If it does not exist, add it with NaN values. @@ -31,7 +33,7 @@ def _delta_lmt(f: float) -> Union[float, np.float64]: """ if f is None: return np.inf # If no delta limit percentage is provided, we set it to infinity to not filter based on delta. - + if isinstance(f, str): try: f = float(f) @@ -44,4 +46,27 @@ def _delta_lmt(f: float) -> Union[float, np.float64]: if np.isnan(f): return np.inf # If no delta limit percentage is provided, we set it to infinity to not filter based on delta. - return np.float64(f) \ No newline at end of file + return np.float64(f) + + +def _build_common_pair_mask( + spread_mid: pd.Series, + spread_bid: pd.Series, + spread_ask: pd.Series, + spread_pct_ratio: pd.Series, + spread_oi: pd.Series, + spread_delta: pd.Series, + min_total_price: float, + max_total_price: float, + max_pct_width: float, + min_oi: int, + delta_lmt: Union[float, np.float64], +) -> pd.Series: + """Build a shared mask used by pairers for spread contract filtering.""" + mid_mask = spread_mid.between(min_total_price, max_total_price) + spread_oi_mask = spread_oi >= min_oi + pct_width_mask = spread_pct_ratio <= max_pct_width + spread_bid_mask = spread_bid > 0 + spread_ask_mask = spread_ask > 0 + delta_mask = spread_delta.abs() <= abs(delta_lmt) + return mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask diff --git a/EventDriven/riskmanager/picker/vertical_spread.py b/EventDriven/riskmanager/picker/vertical_spread.py index 95ef2ed..ea90c14 100644 --- a/EventDriven/riskmanager/picker/vertical_spread.py +++ b/EventDriven/riskmanager/picker/vertical_spread.py @@ -2,7 +2,7 @@ import pandas as pd from typing import Optional from EventDriven.riskmanager.picker import _order_formatting, create_trade_id -from EventDriven.riskmanager.picker.utils import _delta_lmt, _verify_delta_in_chain +from EventDriven.riskmanager.picker.utils import _delta_lmt, _verify_delta_in_chain, _build_common_pair_mask from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema from trade.helpers.Logging import setup_logger @@ -11,7 +11,7 @@ def vertical_spread_pairer_by_exp( - row: pd.Series, + row: pd.DataFrame, spread_tick: int = 1, min_total_price: float = 0.5, max_total_price: float = 1.0, @@ -34,7 +34,7 @@ def vertical_spread_pairer_by_exp( pd.DataFrame: A DataFrame containing the paired legs of the vertical spread and their calculated metrics, filtered by the total spread mid price. """ logger.debug(f"vertical_spread_pairer_by_exp recieved delta_lmt: {delta_lmt}") ## DEBUG - tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest", "delta"] + tgt_details = ["opttick", "midpoint", "closebid", "closeask", "open_interest", "delta", "moneyness", "dte", "theta"] long_leg_details = row[tgt_details].reset_index(drop=True) short_leg_details = row[tgt_details].shift(-spread_tick).reset_index(drop=True) @@ -48,12 +48,15 @@ def vertical_spread_pairer_by_exp( spread_bid = long_leg_details["closebid"] - short_leg_details["closeask"] spread_ask = long_leg_details["closeask"] - short_leg_details["closebid"] spread_mid = long_leg_details["midpoint"] - short_leg_details["midpoint"] + spread_moneyness = long_leg_details["moneyness"] + dte = long_leg_details["dte"] spread_delta = (long_leg_details["delta"] - short_leg_details["delta"]).fillna( np.inf ) # If delta is missing, set to infinity to fail delta filter. bid_ask_spread = spread_ask - spread_bid spread_pct_ratio = abs(spread_bid - spread_ask) / spread_mid.replace(0, np.nan) # Avoid division by zero. spread_oi = abs(long_leg_details["open_interest"] + short_leg_details["open_interest"]) + spread_theta = long_leg_details["theta"] - short_leg_details["theta"] if "volume" in long_leg_details.columns and "volume" in short_leg_details.columns: spread_volume = abs(long_leg_details["volume"] + short_leg_details["volume"]) @@ -73,6 +76,10 @@ def vertical_spread_pairer_by_exp( spread_oi, spread_volume, spread_delta, + spread_moneyness, + dte, + spread_theta, + ), axis=1, ) @@ -87,18 +94,36 @@ def vertical_spread_pairer_by_exp( "spread_oi", "spread_volume", "spread_delta", + "spread_moneyness", + "spread_dte", + "spread_theta", ] + shrunk_delta_lmt = round(_delta_lmt(delta_lmt) * 0.95, 2) + + full_mask = _build_common_pair_mask( + spread_mid=paired_opttick["spread_mid"], + spread_bid=paired_opttick["spread_bid"], + spread_ask=paired_opttick["spread_ask"], + spread_pct_ratio=paired_opttick["spread_pct_ratio"], + spread_oi=paired_opttick["spread_oi"], + spread_delta=paired_opttick["spread_delta"], + min_total_price=min_total_price, + max_total_price=max_total_price, + max_pct_width=max_pct_width, + min_oi=min_oi, + delta_lmt=shrunk_delta_lmt, + ) + mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) spread_oi_mask = paired_opttick["spread_oi"] >= min_oi pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width spread_bid_mask = paired_opttick["spread_bid"] > 0 spread_ask_mask = paired_opttick["spread_ask"] > 0 - delta_mask = paired_opttick["spread_delta"].abs() <= abs(_delta_lmt(delta_lmt)) - full_mask = mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask + delta_mask = paired_opttick["spread_delta"].abs() <= abs(shrunk_delta_lmt) logger.debug(f"Number of spreads after applying all filters: {full_mask.sum()}") ## DEBUG logger.debug( - f"spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" + f"mid_mask: {mid_mask.sum()} (between {min_total_price} and {max_total_price}), spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" ) ## DEBUG return paired_opttick[full_mask].reset_index(drop=True) @@ -153,7 +178,12 @@ def _vertical_spread_pairer( ## 1. spread_pct_ratio (we want this to be low, meaning the spread is relatively tight compared to its midpoint) ## 2. spread_oi (we want this to be high, meaning there's good liquidity in the spread) ## Finally, pick the top row as our chosen spread. - vertical_chain.sort_values(by=["spread_pct_ratio", "spread_oi"], ascending=[True, False], inplace=True) + order_delta_ascending = True if is_call else False + vertical_chain.sort_values( + by=["spread_pct_ratio", "spread_oi", "spread_delta"], + ascending=[True, False, order_delta_ascending], + inplace=True, + ) picked_spread = vertical_chain.iloc[0] if not vertical_chain.empty else pd.Series() return picked_spread @@ -177,6 +207,7 @@ def _extract_order_for_vertical_spread(picked_spread: pd.Series, schema: OrderSc ## Other details from spread pct_ratio = picked_spread["spread_pct_ratio"] spread_oi = picked_spread["spread_oi"] + delta = picked_spread["spread_delta"] trade_id = create_trade_id( legs={ "long": long, @@ -190,6 +221,15 @@ def _extract_order_for_vertical_spread(picked_spread: pd.Series, schema: OrderSc "metrics": { "spread_pct_ratio": pct_ratio, "spread_oi": spread_oi, + "delta": delta, + }, + "scores": { + "moneyness_score": picked_spread.get("moneyness_score", np.nan), + "dte_score": picked_spread.get("dte_score", np.nan), + "mid_score": picked_spread.get("mid_score", np.nan), + "pct_spread_score": picked_spread.get("pct_spread_score", np.nan), + "oi_score": picked_spread.get("oi_score", np.nan), + "theta_burden_score": picked_spread.get("theta_burden_score", np.nan), }, } else: @@ -215,13 +255,13 @@ def vertical_spread_order_builder( dict: A dictionary containing the result status and order data. """ logger.debug(f"vertical_spread_order_builder recieved delta_lmt: {delta_lmt}") ## DEBUG + filtered_chain = _verify_delta_in_chain( + filtered_chain + ) # Ensure delta column exists before proceeding to finder function. picked_spread = _vertical_spread_pairer( filtered_chain=filtered_chain, schema=schema, delta_lmt=delta_lmt, ) - filtered_chain = _verify_delta_in_chain( - filtered_chain - ) # Ensure delta column exists before proceeding to finder function. order = _extract_order_for_vertical_spread(picked_spread, schema=schema) return order From 587ace65eabb3b5bfac60f2ae2cf6777ad18c405 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:47 -0400 Subject: [PATCH 16/81] Add bid-ask spread liquidity multiplier for order quantity adjustment --- EventDriven/riskmanager/new_base.py | 72 ++++++++++++++++++++++++++++- EventDriven/riskmanager/utils.py | 4 +- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index f3b039c..34c85c1 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -195,7 +195,7 @@ from EventDriven._vars import get_use_temp_cache from trade.helpers.helper import CustomCache, is_USholiday import pandas as pd -from typing import List +from typing import List, Optional from datetime import datetime from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.picker.order_picker import OrderPicker, order_failed @@ -340,6 +340,70 @@ def clear_caches(self): else: logger.critical("USE_TEMP_CACHE set to False. Cache will not be cleared") + def _liquity_multiplier(self, pct_ratio: float, good: Optional[float] = None, bad: Optional[float] = None) -> float: + """ + Calculate a liquidity multiplier based on the bid-ask spread percentage. + + Parameters + ---------- + pct_ratio : float + The percentage ratio of the bid-ask spread to the mid price. + good : float, optional + The multiplier value when the spread is very good (default is 1.0). + bad : float, optional + The multiplier value when the spread is very bad (default is 0.5). + + Returns + ------- + float + A liquidity multiplier between `bad` and `good` based on the `pct_ratio`. + + Notes + ----- + - The function uses an exponential decay to assign higher multipliers to better spreads. + - If `good` and `bad` are not provided, it defaults to a range of [0.5, 1.0]. + - The `pct_ratio` is expected to be a positive value representing the spread as a percentage of the mid price. + """ + if good is None: + good = self.order_picker._scoring_config.target_spread_pct + if bad is None: + bad = self.order_picker._scoring_config.pct_spread_max + + if pct_ratio <= good: + return 1.0 + elif pct_ratio >= bad: + return 0.25 + else: + x = (pct_ratio - good) / (bad - good) # Normalize to [0, 1] + return 1.0 - x * 0.75 # Scale to [1.0, 0.25] + + def _quantiy_liquidity_adjustment(self, quantity: int, pct_ratio: float) -> int: + """ + Adjust the order quantity based on the liquidity of the option, as measured by the bid-ask spread percentage. + + Parameters + ---------- + quantity : int + The original order quantity before adjustment. + pct_ratio : float + The percentage ratio of the bid-ask spread to the mid price. + + Returns + ------- + int + The adjusted order quantity after applying the liquidity multiplier. + + Notes + ----- + - The function calculates a liquidity multiplier using `_liquity_multiplier` and applies it to the original quantity. + - The resulting quantity is rounded to the nearest integer and cannot be negative. + - This adjustment helps to reduce position size for options with poor liquidity (wide spreads). + """ + multiplier = self._liquity_multiplier(pct_ratio) + adjusted_quantity = int(round(quantity * multiplier)) + q = max(adjusted_quantity, 1) # Ensure quantity is at least 1 + return q + def get_order(self, req: OrderRequest) -> NewPositionState: """ Generates an order based on the provided OrderRequest dataclass and returns @@ -453,6 +517,12 @@ def get_order(self, req: OrderRequest) -> NewPositionState: order = updated_pos_state.order.to_dict() + ## Update order for liquidity + if not order_failed(order) and q > 0: + pct_ratio = updated_pos_state.order["metrics"]["spread_pct_ratio"] + q = self._quantiy_liquidity_adjustment(q, pct_ratio) + updated_pos_state.order.data["quantity"] = q + logger.info(f"Order quantity after liquidity adjustment: {q} for position ID: {position_id}") return updated_pos_state def analyze_position(self, context: PositionAnalysisContext) -> StrategyChangeMeta: diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 3404dc5..7a71105 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -208,7 +208,9 @@ ## Vars TIMESERIES_START = pd.to_datetime(OPTION_TIMESERIES_START_DATE) -TIMESERIES_END = datetime.now().strftime("%Y-%m-%d") +_NOW = datetime.now() +# Set end date to one year ago to avoid lookahead bias and ensure we have enough data for backtesting +TIMESERIES_END = _NOW.replace(year=_NOW.year - 1, month=12, day=31).strftime("%Y-%m-%d") LOOKBACKS = {} ## Paths From fda33564a364595b679c7d3ca750cb954ba95bab Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:29:57 -0400 Subject: [PATCH 17/81] Fix mean reversion sizer, attribution, and minor backtest/execution cleanups --- EventDriven/attribution.py | 18 ++++++------ EventDriven/backtest.py | 2 +- EventDriven/execution.py | 1 - .../position/cogs/mean_reversion.py | 28 +++++++++++++++++++ 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index e24a8f0..e37847d 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -280,7 +280,7 @@ def compute_position_attribution( attribution = attribution.copy() commission = qty_ts.commission slippage = qty_ts.slippage - + ## Ensure attribution has necessary columns, if not create them with default values if "commission_cost" not in attribution.columns: attribution["commission_cost"] = commission.fillna(0) @@ -303,13 +303,13 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: """ if qty > 0: # OPEN: entry is execution price on this date, close is current position price - entry_p = abs(exec_price.loc[date]) + entry_p = abs(exec_price.loc[date]) #+ slippage.loc[date] + commission.loc[date] close_p = get_position_price_func(_id=trade_id, date=date, force=True) else: # CLOSE: entry is previous day's position price, close is execution price on this date prev_date = change_to_last_busday(date - BDay(1)) entry_p = get_position_price_func(_id=trade_id, date=prev_date, force=True) - close_p = abs(exec_price.loc[date]) + close_p = abs(exec_price.loc[date]) #- slippage.loc[date] - commission.loc[date] pnl = (close_p - entry_p) * abs(qty) return pnl, entry_p, close_p @@ -340,7 +340,7 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: trade_pnl, entry_p, close_p = _compute_pnl_for_change(date, qty_change) commission_cost = commission.get(date, 0) * abs(qty_change) slippage_cost = slippage.get(date, 0) * abs(qty_change) - trade_pnl -= commission_cost + slippage_cost + # trade_pnl -= commission_cost + slippage_cost # Decide whether to include costs in the trade PnL or keep them separate for attribution purposes # if fully closed or just opened, zero other components on that date if fully_closed or just_opened: @@ -348,14 +348,13 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: # apply adjustments attribution.loc[date, "trade_pnl_adjustment"] += trade_pnl - attribution.loc[date, "commission_cost"] += commission_cost - attribution.loc[date, "slippage_cost"] += slippage_cost - attribution.loc[date, "total_pnl"] += trade_pnl + attribution.loc[date, "commission_cost"] -= commission_cost + attribution.loc[date, "slippage_cost"] -= slippage_cost + attribution.loc[date, "total_pnl"] += trade_pnl - commission_cost - slippage_cost logger.info( f"Date: {date.date()}, Qty: {qty_change}, Entry: {entry_p}, Close: {close_p}, PnL: {trade_pnl}, PrevQty: {prev_qty}, Commission: {commission_cost}, Slippage: {slippage_cost}" ) - - attribution["opt_plus_adj"] = attribution["opt_dod_change"] + attribution["trade_pnl_adjustment"] + attribution["opt_plus_adj"] = attribution["opt_dod_change"] + attribution["trade_pnl_adjustment"] + attribution["commission_cost"] + attribution["slippage_cost"] attribution = attribution[ [ "opt_dod_change", @@ -371,6 +370,7 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: "theta_pnl", "volga_pnl", "vanna_pnl", + "rho_pnl", ] ] diff --git a/EventDriven/backtest.py b/EventDriven/backtest.py index 3a6dcdd..99013ca 100644 --- a/EventDriven/backtest.py +++ b/EventDriven/backtest.py @@ -240,7 +240,7 @@ def __init__with_equity_strategy( ## We will not use trades dataframe in this process. self.start_date = pd.to_datetime(eq_strategy.start_date).date() if is_USholiday(self.start_date): - self.logger.warning(f"Start date {self.start_date} is a US holiday. Adjusting to previous business day.") + self.logger.info(f"Start date {self.start_date} is a US holiday. Adjusting to previous business day.") self.start_date = change_to_last_busday(self.start_date, -1).date() start_date, end_date = self.start_date, self.end_date diff --git a/EventDriven/execution.py b/EventDriven/execution.py index 7dcfe45..7f7572e 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -95,7 +95,6 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): assert order_event.type == 'ORDER', f"Event type must be 'ORDER' received {order_event.type}" assert order_event.direction == 'BUY' or order_event.direction == 'SELL', f"Event direction must be 'BUY' or 'SELL' received {order_event.direction}" exec_cache['order'][f'{order_event.signal_id}_{order_event.datetime.strftime("%Y-%m-%d")}'] = deepcopy(order_event) - # Generate slippage as a percentage ## Slippage improvement if order_event.direction == 'BUY': diff --git a/EventDriven/riskmanager/position/cogs/mean_reversion.py b/EventDriven/riskmanager/position/cogs/mean_reversion.py index fcb5935..e634d46 100644 --- a/EventDriven/riskmanager/position/cogs/mean_reversion.py +++ b/EventDriven/riskmanager/position/cogs/mean_reversion.py @@ -1,3 +1,5 @@ +import math + from EventDriven.riskmanager.position.base import BaseCog from EventDriven.configs.core import MeanReversionSizerConfigs from typing import Optional @@ -122,3 +124,29 @@ def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestam ## Update order quantity based on scaler limit = base_delta * scaler return limit + + +class SeniorMeanReversionSizerCog(MeanReversionSizerCog): + def __init__(self, eq_strategy: MultiAssetStrategy, config: Optional[MeanReversionSizerConfigs] = None): + super().__init__(eq_strategy, config) + + def on_new_position(self, state): + """ + Overrides the on_new_position method to add logging and handle cases where the order quantity is reduced to 0 after mean reversion sizing. + If the order quantity is 0, it logs this information and checks if based on the available cash and option price, at least 1 contract could be afforded. If so, it logs this insight and optionally sets the""" + + ret = super().on_new_position(state) + if state.order["data"]["quantity"] == 0: + logger.info(f"Order quantity is 0 after mean reversion sizing for trade_id {state.order['data']['trade_id']}. This means the position will not be opened based on the current z-score and cash constraints.") + cash_available = state.request.tick_cash + option_price_at_time = state.at_time_data.get_price() + max_size_cash_can_buy = abs(math.floor(cash_available / (option_price_at_time * 100))) + if max_size_cash_can_buy >= 1: + logger.info(f"However, based on the available cash of {cash_available} and option price of {option_price_at_time}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Consider adjusting the beta or z-score thresholds to allow for smaller position sizes in such scenarios.") + state.order["data"]["quantity"] = 1 # Optionally set to 1 to allow for at least a small position, or keep at 0 to strictly follow the sizing logic. + else: + logger.info(f"Based on the available cash of {cash_available} and option price of {option_price_at_time}, even a single contract cannot be afforded. The order will remain at quantity 0.") + return ret + + + From 978b9764fd42e4adbea47bd5049906742e7aebb5 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:15:33 -0400 Subject: [PATCH 18/81] QuantTools: decommission legacy order route and enforce hard-fail compatibility contract --- .gitignore | 8 +- EventDriven/riskmanager/_order_validator.py | 315 ------------ EventDriven/riskmanager/_orders.py | 334 ------------- .../riskmanager/picker/order_picker.py | 461 +++--------------- .../tests/test_order_picker_deprecations.py | 55 +++ 5 files changed, 131 insertions(+), 1042 deletions(-) delete mode 100644 EventDriven/riskmanager/_order_validator.py delete mode 100644 EventDriven/riskmanager/_orders.py create mode 100644 EventDriven/tests/test_order_picker_deprecations.py diff --git a/.gitignore b/.gitignore index 723838a..fe59e85 100644 --- a/.gitignore +++ b/.gitignore @@ -206,4 +206,10 @@ trade/assets/notebooks # allow notebooks in any demo folder (demo or demos) !**/demo/**/*.ipynb -!**/demos/**/*.ipynb \ No newline at end of file +!**/demos/**/*.ipynb + +# Local decommission staging (keep out of remote) +.decomission/ +.decommission/ +EventDriven/riskmanager/.decomission/ +EventDriven/riskmanager/.decommission/ \ No newline at end of file diff --git a/EventDriven/riskmanager/_order_validator.py b/EventDriven/riskmanager/_order_validator.py deleted file mode 100644 index c66e955..0000000 --- a/EventDriven/riskmanager/_order_validator.py +++ /dev/null @@ -1,315 +0,0 @@ -"""Order Validation and Input Schema for Risk Manager. - -This module provides validation infrastructure for order requests and strategy configurations -within the risk management system. It defines input schemas, type checking, and configuration -management for order generation and execution. - -Key Components: - OrderInputs: Container class holding all order parameters with type validation - _SlugConfig: Abstract base class for strategy configuration management - INPUTS: Schema definition mapping for order parameters with types and descriptions - -Order Input Parameters: - Trading Parameters: - - tick: Stock ticker symbol - - date: Trade date - - spot: Current underlying price - - signal_id: Unique signal identifier - - Strategy Parameters: - - option_strategy: Strategy type (e.g., 'vertical_spread', 'iron_condor') - - option_type: Call ('C') or Put ('P') - - structure_direction: Long or short structure - - direction: LONG or SHORT position - - Risk Parameters: - - initial_cash: Total capital allocated - - tick_cash: Per-ticker allocation - - max_close: Maximum acceptable option price - - min_total_price: Minimum structure price threshold - - Selection Criteria: - - target_dte: Target days to expiration - - dte_tolerance: Acceptable DTE deviation - - min_moneyness: Minimum strike/spot ratio - - max_moneyness: Maximum strike/spot ratio - - spread_ticks: Number of strikes between legs - - otm_moneyness_width: OTM selection width - - itm_moneyness_width: ITM selection width - - Execution Control: - - max_tries: Maximum order search attempts - - max_dte_tolerance: Maximum DTE relaxation allowed - -Validation: - - Type checking for all input parameters - - Range validation for numerical values - - Enum validation for categorical parameters - - Strategy compatibility verification - -Usage: - inputs = OrderInputs( - tick='AAPL', - date='2024-01-15', - spot=185.50, - option_strategy='put_spread', - target_dte=45, - min_moneyness=0.90, - max_moneyness=1.10 - ) - -Notes: - - All parameters are validated on instantiation - - Invalid keys are logged and ignored - - Type mismatches raise descriptive errors - - Configuration can be loaded from various sources (YAML, JSON, database) -""" - -from datetime import datetime -from typing import Any, Dict, Optional, TYPE_CHECKING, Union, Literal -import pandas as pd -import numbers -from dataclasses import dataclass, field -from abc import ABC, abstractmethod -from trade.helpers.Logging import setup_logger -from EventDriven.riskmanager.picker.builder import BUILDER_FACTORY -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") - - -@dataclass(kw_only=True) -class _SlugConfig(ABC): - initial_capital: float - config: Dict[str, Any] = field(default_factory=dict) - portfolio_settings: Dict[str, Any] = field(default_factory=dict) - strategy_settings: Dict[str, Any] = field(default_factory=dict) - rm_settings: Dict[str, Any] = field(default_factory=dict) - sizer_settings: Dict[str, Any] = field(default_factory=dict) - executor_settings: Dict[str, Any] = field(default_factory=dict) - haircut_weights: Dict[str, float] = field(default_factory=dict) - option_settings: Dict[str, Any] = field(default_factory=dict) - cash_weights: Dict[str, float] = field(default_factory=dict) - order_settings: Dict[str, Any] = field(default_factory=dict) - cash_map: Dict[str, float] = field(default_factory=dict) - - @abstractmethod - def load(self, source: Optional[str] = None) -> None: - pass - - -INPUTS = { - "tick": ( - str, - "This should be a string representing the stock ticker.", - ), - "date": ( - [str, pd.Timestamp, datetime], - "This should be a date in string format or a pandas Timestamp/datetime object.", - ), - "spot": (numbers.Number, "This should be a float representing chain_spot for the tick."), - "signal_id": (str, "This should be a string representing the signal ID."), - "max_close": (numbers.Number, "Max price for the order search engine."), - "option_strategy": ( - str, - f"This should be a string representing the option strategy. Available: {BUILDER_FACTORY.keys()}", - ), - "initial_cash": (numbers.Number, "This should be a float representing the initial cash for the strategy."), - "option_type": (str, "This should be a string representing the option type, e.g., 'standard'.", ["C", "P"]), - "structure_direction": (str, "This should be a string representing the structure direction.", ["long", "short"]), - "spread_ticks": (numbers.Number, "This should be an integer representing the spread ticks."), - "dte_tolerance": (numbers.Number, "This should be an integer representing the DTE tolerance."), - "min_moneyness": (numbers.Number, "This should be a float representing the minimum moneyness."), - "max_moneyness": (numbers.Number, "This should be a float representing the maximum moneyness."), - "target_dte": (numbers.Number, "This should be an integer representing the target DTE."), - "min_total_price": (numbers.Number, "This should be a float representing the minimum total price."), - "direction": (str, "This should be a str of either LONG or SHORT"), - "max_dte_tolerance": (numbers.Number, "This should be an integer representing the maximum DTE tolerance."), - "max_tries": (numbers.Number, "This should be an integer representing the maximum number of tries."), - "otm_moneyness_width": ( - numbers.Number, - "This should be a float representing the OTM moneyness width max for ATM against OTM.", - ), - "itm_moneyness_width": ( - numbers.Number, - "This should be a float representing the ITM moneyness width max for ATM against ITM.", - ), - "tick_cash": (numbers.Number, "This should be a float representing the cash allocated to this tick."), -} - - -class OrderInputs: - __slots__ = list(INPUTS.keys()) - if TYPE_CHECKING: - tick: str - date: Union[str, pd.Timestamp, datetime] - spot: float - signal_id: str - max_close: float - option_strategy: str - initial_cash: float - option_type: Literal["C", "P"] - structure_direction: Literal["long", "short"] - spread_ticks: numbers.Number - dte_tolerance: numbers.Number - min_moneyness: float - max_moneyness: float - target_dte: numbers.Number - min_total_price: float - direction: Literal["LONG", "SHORT"] - max_dte_tolerance: numbers.Number - max_tries: numbers.Number - otm_moneyness_width: float - itm_moneyness_width: float - tick_cash: float - - def __init__(self, **kwargs): - for k in kwargs: - if k in self.__slots__: - setattr(self, k, kwargs.get(k)) - else: - logger.info(f"Unknown input key: {k}. This key will be ignored.") - - -OrderInputs.__doc__ = f""" -Order Container that holds all necessary variables in one place. -Args: -{chr(10).join([f" {key} ({' | '.join([t.__name__ if isinstance(t, type) else str(t) for t in (types if isinstance(types, list) else [types])])}): {desc}" for key, (types, desc, *_) in INPUTS.items()])} -""" - - -def verify_order_selection_inputs(**kwargs): - ## Key: - # {input: (type, description, Optional[expected values])} - - for key, (expected_type, description, *expected_values) in INPUTS.items(): - if key not in kwargs: - raise ValueError(f"Missing required input: '{key}'. Desc: {description}") - if not isinstance(kwargs[key], tuple(expected_type) if isinstance(expected_type, list) else expected_type): - raise TypeError( - f"Input '{key}' must be of type {expected_type}, but got {type(kwargs[key])}. {description}" - ) - if expected_values and kwargs[key] not in expected_values[0]: - raise ValueError( - f"Input '{key}' must be one of {expected_values[0]}, but got '{kwargs[key]}'. {description}" - ) - - -def build_inputs_with_config( - config: _SlugConfig, max_close: float, row: pd.Series, tick_cash: float, tick: str -) -> tuple[OrderSchema, OrderInputs]: - """ - Builds the inputs for the order selection engine based on the strategy config and trade row. - Args: - config (SlugConfig): The strategy configuration. - max_close (float): The maximum price for the order selection engine. - row (pd.Series): The trade row containing trade details. Expected keys: ['PT_BKTEST_SIG_ID', 'Size', 'EntryTime'] - tick (str): The stock ticker symbol. - Returns: - Tuple[OrderSchema, OrderInputs]: A tuple containing the OrderSchema and OrderInputs dataclass. - """ - ## Necessary inputs - assert isinstance(config, _SlugConfig), "config must be an instance of SlugConfig" - assert all( - k in row for k in ["PT_BKTEST_SIG_ID", "Size", "EntryTime"] - ), "row must contain 'PT_BKTEST_SIG_ID', 'Size', and 'EntryTime' keys" - - ## Date is signal entry date + t+n days - date = pd.to_datetime(row.EntryTime).strftime("%Y-%m-%d") - signal_id = row.PT_BKTEST_SIG_ID - - ## The option strategy to deploy. Eg: 'vertical' - option_strategy = config.order_settings.get("strategy", "Unknown") - if option_strategy == "Unknown": - raise ValueError("Unknown strategy. Not set in config?") - - ## The option type to deploy. Eg: 'C' or 'P' - option_type = "C" if row.Size > 0 else "P" - - ## The structure direction. Eg: 'long' or 'short'. E.g., long vertical call spread - structure_direction = config.order_settings.get("structure_direction", "Unknown") - if structure_direction == "Unknown": - raise ValueError("Unknown structure_direction. Not set in config?") - - ## Spread btwn strikes in ticks - spread_ticks = config.order_settings.get("spread_ticks", 1) - - ## The Min & Max DTE tolerance - dte_tolerance = config.order_settings.get("dte_tolerance", 90) - - ## Min & Max moneyness for the options to consider - min_moneyness = config.order_settings.get("min_moneyness", 0.5) - max_moneyness = config.order_settings.get("max_moneyness", 1.25) - - ## Target DTE for the options to consider - target_dte = config.order_settings.get("target_dte", "Unknown") - if target_dte == "Unknown": - raise ValueError("Unknown target_dte. Not set in config?") - - ## Minimum total price for the order selection - min_total_price = config.order_settings.get("min_total_price", max_close / 2) - - ## Direction - if option_type.upper() == "C": - direction = "LONG" - elif option_type.upper() == "P": - direction = "SHORT" - else: - raise ValueError("Invalid option type. Must be 'C' or 'P'.") - - 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 - - ## Min DTE Threshold - max_dte_tolerance = config.rm_settings.get("max_dte_tolerance", 180) - - ## Amount of tries for the order selection engine if no orders are found - max_tries = config.rm_settings.get("max_tries", 3) - - ## OTM & ITM moneyness width for multi-leg strategies - otm_moneyness_width = config.rm_settings.get("otm_moneyness_width", 0.2) - itm_moneyness_width = config.rm_settings.get("itm_moneyness_width", 0.2) - initial_cash = config.initial_capital - inputs = dict( - tick=tick, - date=date, - spot=spot, - signal_id=signal_id, - max_close=max_close, - option_strategy=option_strategy, - initial_cash=initial_cash, - option_type=option_type, - structure_direction=structure_direction, - spread_ticks=spread_ticks, - dte_tolerance=dte_tolerance, - min_moneyness=min_moneyness, - max_moneyness=max_moneyness, - target_dte=target_dte, - min_total_price=min_total_price, - direction=direction, - max_dte_tolerance=max_dte_tolerance, - max_tries=max_tries, - otm_moneyness_width=otm_moneyness_width, - itm_moneyness_width=itm_moneyness_width, - tick_cash=tick_cash, - ) - verify_order_selection_inputs(**inputs) - return 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, - "min_total_price": min_total_price, - } - ), OrderInputs(**inputs) diff --git a/EventDriven/riskmanager/_orders.py b/EventDriven/riskmanager/_orders.py deleted file mode 100644 index c30d370..0000000 --- a/EventDriven/riskmanager/_orders.py +++ /dev/null @@ -1,334 +0,0 @@ -"""Order Resolution and Retry Logic for Risk Manager. - -This module implements sophisticated order resolution strategies to handle failed order -attempts by progressively relaxing constraints. It manages the order search process, -retry logic, and schema adaptation when initial order criteria cannot be met. - -Core Functions: - order_failed: Quick check if an order result indicates failure - resolve_schema: Progressive constraint relaxation algorithm - order_resolve_loop: Main retry loop with adaptive schema modification - -Resolution Strategy (Priority Order): - 1. DTE Tolerance Expansion: - - Increases acceptable days-to-expiration range by 20 days - - Applied when current DTE tolerance < max_dte_tolerance - - Allows finding options with different expirations - - 2. Min Moneyness Relaxation (OTM Expansion): - - Decreases minimum moneyness by 0.1 (moves further OTM) - - Applied when OTM width <= otm_moneyness_width threshold - - Expands search to include more out-of-the-money options - - 3. Max Moneyness Relaxation (ITM Expansion): - - Increases maximum moneyness by 0.1 (moves further ITM) - - Applied when ITM width <= itm_moneyness_width threshold - - Expands search to include more in-the-money options - - 4. Price Ceiling Increase: - - Raises max_total_price limit by $1.00 - - Applied when current max price <= max_close threshold - - Allows consideration of more expensive structures - - 5. Max Tries Limit: - - Terminates after max_tries resolution attempts - - Prevents infinite loops and excessive computation - - Returns False when exhausted - -Key Features: - - Incremental constraint relaxation preserves strategy intent - - Logging at each resolution step for debugging - - Schema caching to avoid redundant searches - - Type-safe order handling with ResultsEnum - - Integration with OrderPicker for actual order selection - -Usage: - schema, tries = resolve_schema( - schema=original_schema, - tries=0, - max_dte_tolerance=90, - otm_moneyness_width=0.15, - itm_moneyness_width=0.15, - max_close=5.00, - max_tries=6 - ) - -Workflow: - 1. Initial order attempt with base schema - 2. Check result status with order_failed() - 3. If failed, enter order_resolve_loop() - 4. Apply resolve_schema() for next attempt - 5. Repeat until success or max_tries reached - 6. Return final order or failure indication - -Notes: - - Each resolution step logged for transparency - - Schema modifications are cumulative across attempts - - Failed orders may indicate illiquid markets or extreme constraints - - Consider relaxing initial constraints if failures are frequent -""" - -from typing import Dict, Any, TYPE_CHECKING, Optional, Tuple -from datetime import datetime -import pandas as pd -from trade.helpers.Logging import setup_logger -from EventDriven.riskmanager.picker import OrderSchema, ResultsEnum -from EventDriven.riskmanager._order_validator import OrderInputs -from EventDriven.riskmanager.utils import get_persistent_cache -from EventDriven._vars import set_use_temp_cache -from EventDriven.dataclasses.orders import OrderRequest - -if TYPE_CHECKING: - from EventDriven.riskmanager.picker.order_picker import OrderPicker - -logger = setup_logger("EventDriven.riskmanager._orders", stream_log_level="WARNING") - - -def order_failed(order: Dict[str, Any]) -> bool: - """ - Check if the order result indicates a failure. - - Args: - order (Dict[str, Any]): The order dictionary containing the result. - Returns: - bool: True if the order result indicates failure, False otherwise. - """ - return order["result"] != ResultsEnum.SUCCESSFUL.value - - -def resolve_schema( - schema: OrderSchema, - tries: int, - max_dte_tolerance: int, - otm_moneyness_width: float, - itm_moneyness_width: float, - max_close: float, - max_tries: int = 6, - tick_cash: int = 10, - *, - starting_min_moneyness: float = None, - starting_max_moneyness: float = None, -) -> Tuple[OrderSchema, int]: - """ - Resolving schema by order of importance - 1. DTE Tolerance - 2. Min Moneyness width - 3. Max Moneyness width - 4. Max Close Price - 5. Max Schema Tries - If no schema is found after max tries, return False and the number of tries. - - Args: - schema (OrderSchema): The schema to resolve. - tries (int): The number of tries already made. - max_dte_tolerance (int): The maximum DTE tolerance to allow. - otm_moneyness_width (float): In future iteration, this serve as the max width min moneyness can grow from starting_min_moneyness. It is the max width btwn new min moneyness and starting_min_moneyness. It is the max width btwn OTM strikes and 1 - itm_moneyness_width (float): In future iteration, this serve as the max width max moneyness can grow from starting_max_moneyness. It is the max width btwn new max moneyness and starting_max_moneyness. It is the max width btwn ITM strikes and 1. - max_close (float): The maximum close price to allow. - max_tries (int): The maximum number of tries allowed. - - Returns: - tuple: A tuple containing the resolved schema or False if no schema was found, and the number of tries made. - """ - if starting_min_moneyness is None: - raise ValueError("Missing starting_min_moneyness. This is required to ensure we do not relax constraints too much and to serve as a reference point for how much we can relax the min moneyness.") - - if starting_max_moneyness is None: - raise ValueError("Missing starting_max_moneyness. This is required to ensure we do not relax constraints too much and to serve as a reference point for how much we can relax the max moneyness.") - current_min_moneyness = schema["min_moneyness"] - current_max_moneyness = schema["max_moneyness"] - current_min_moneyness_width = starting_min_moneyness - current_min_moneyness - current_max_moneyness_width = current_max_moneyness - starting_max_moneyness - tick = schema["tick"] - ##0). Max schema tries - if tries >= max_tries: - return False, tries - - - # 1). DTE Resolve - tries += 1 - if schema["dte_tolerance"] <= max_dte_tolerance: - logger.info( - f"Resolving Schema ({tick}): {schema['dte_tolerance']} <= {max_dte_tolerance}, increasing DTE Tolerance by 10 from {schema['dte_tolerance']} to {schema['dte_tolerance'] + 20}" - ) - schema["dte_tolerance"] += 20 - return schema, tries - - # 2). Min Moneyness Resolve - elif current_min_moneyness_width < otm_moneyness_width: - logger.info( - f"Resolving Schema ({tick}): {current_min_moneyness_width} <= {otm_moneyness_width}, decreasing Min Moneyness by 0.1 from {schema['min_moneyness']} to {schema['min_moneyness'] - 0.1}" - ) - schema["min_moneyness"] -= 0.05 - return schema, tries - - # 3). Max Moneyness Resolve - elif current_max_moneyness_width < itm_moneyness_width: - logger.info( - f"Resolving Schema ({tick}): {current_max_moneyness_width} <= {itm_moneyness_width}, increasing Max Moneyness by 0.1 from {schema['max_moneyness']} to {schema['max_moneyness'] + 0.1}" - ) - schema["max_moneyness"] += 0.05 - return schema, tries - - # 4). Close Resolve - elif schema["max_total_price"] <= max_close: - logger.info( - f"Resolving Schema ({tick}): {schema['max_total_price']} <= {max_close}, increasing Max Close by 0.5 from {schema['max_total_price']} to {schema['max_total_price'] + 1}" - ) - new_max = schema["max_total_price"] + 1 - schema["max_total_price"] = min(new_max, tick_cash) - return schema, tries - - return False, tries - - -def order_resolve_loop( - order: Dict[str, Any], - schema: OrderSchema, - date: pd.Timestamp | str | datetime, - spot: float, - max_close: float, - max_dte_tolerance: int, - max_tries: int, - otm_moneyness_width: float, - itm_moneyness_width: float, - logger, - signalID: str, - schema_cache: dict, - picker: "OrderPicker", - request: OrderRequest = None, - tick_cash: int = 10, - delta_lmt: Optional[float] = None, -): - """ - Attempt to resolve an order schema until a successful order is produced or maximum tries are exceeded. - Args: - - order (Dict[str, Any]): The initial order dictionary. - schema (OrderSchema): The initial order schema. - date (pd.Timestamp|str|datetime): The date for the order. - spot (float): The current chain spot price. - max_close (float): The maximum total price for the order. - max_dte_tolerance (int): The maximum DTE tolerance for the order. (This should actually be minimum. It is the least we can tolerate) - max_tries (int): The maximum number of tries to resolve the schema before giving up. - otm_moneyness_width (float): The max width btwn OTM strikes and 1 to tolerate. - itm_moneyness_width (float): The max width btwn ITM strikes and 1 to tolerate. - logger: Logger object for logging information. - signalID (str): Unique identifier for the signal/order. - schema_cache (dict): Cache to store resolved schemas for specific dates and signal IDs. It will store in place. - picker (OrderPicker): The OrderPicker instance to use for getting the order. - - Returns: - Dict[str, Any]: The final order dictionary, either successful or indicating failure. - """ - if picker.__class__.__name__ != "OrderPicker": - raise ValueError("picker must be an instance of OrderPicker") - - tries = 0 - use_request = False - - if request is not None: - ## This is a patch to use new get order method with request - ## Technically it is still relevant to the previous method, but we use request to get schema - ## And transform to tuple for old method - logger.info(f"Attempting to resolve order for request: {request}") - 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()) - use_request = True - min_moneyness = schema.get("min_moneyness", None) - max_moneyness = schema.get("max_moneyness", None) - - while order_failed(order): - 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=max_dte_tolerance, - max_close=max_close, - max_tries=max_tries, - otm_moneyness_width=otm_moneyness_width, - itm_moneyness_width=itm_moneyness_width, - tick_cash=tick_cash, - starting_min_moneyness=min_moneyness, - starting_max_moneyness=max_moneyness, - ) - schema, tries = pack - - if schema is False: - logger.info(f"Unable to resolve schema after {tries} tries, returning None") - schema_cache.setdefault(date, {}).update({signalID: schema}) - return {"result": ResultsEnum.NO_CONTRACTS_FOUND.value, "data": None} - logger.info(f"Resolved Schema: {schema}, tries: {tries}") - - if use_request: - ## When using request, we have to use the _get_order method directly - ## Previously, .get_order_new took in schema as a dict, converted to tuple internally - ## And then called _get_order. But here we already have the tuple form - ## So we call _get_order directly - order = picker._get_order( - schema=schema_as_tuple, - date=request.date, - spot=request.spot, - chain_spot=request.chain_spot, - print_url=False, - delta_lmt=delta_lmt, - ) - else: - order = picker.get_order_new(schema, date, spot, print_url=False, delta_lmt=delta_lmt) ## Get the order from the OrderPicker - schema_cache.setdefault(date, {}).update({signalID: schema}) - return order - - -def get_open_order( - picker: "OrderPicker", - spot: float, - date: datetime | str, - schema: OrderSchema, - inputs: OrderInputs, - schema_cache: dict = None, - test: bool = False, -) -> dict: - ## Initialize schema cache - if schema_cache is None: - schema_cache = {} - - ## Set caching parameters to temp - set_use_temp_cache(True) - - ## Clear persistent cache if not in test mode - if not test: - print("INFO: Not in test mode, clearing persistent cache") - get_persistent_cache().clear() - else: - print("WARNING: In test mode, using persistent cache") - - ## Utilize picker to get order - order = picker.get_order_new( - schema=schema, - date=date, - spot=spot, - ) - - ## Resole order if failed - order = order_resolve_loop( - order=order, - schema=schema, - date=inputs.date, - spot=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=schema_cache, - picker=picker, - ) - - ## Add necessary tags for identification - order["signal_id"] = inputs.signal_id - order["map_signal_id"] = inputs.signal_id - return order diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index 369409f..ae09144 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -1,219 +1,60 @@ -"""Intelligent Order Selection and Option Chain Filtering. +"""Scoring-based option order picker. -This module implements the OrderPicker class responsible for selecting optimal option -orders from available chains based on strategy criteria, risk constraints, and market -conditions. It handles order schema creation, chain filtering, strategy building, and -retry logic when initial criteria cannot be met. +This module contains the scoring-only `OrderPicker` runtime used by RiskManager. +Legacy schema-driven helpers remain only as hard-error compatibility entrypoints. Core Class: - OrderPicker: Main order selection engine with caching and retry logic - -Key Features: - - Strategy-aware option selection (spreads, condors, butterflies, etc.) - - Moneyness-based filtering (ITM, ATM, OTM ranges) - - DTE (days to expiration) targeting with tolerance bands - - Price constraints (min/max total price limits) - - Liquidity filtering (volume, open interest, bid-ask spreads) - - Automatic retry with relaxed constraints on failure - - Memoization for performance optimization - - Call/Put moneyness adjustment logic - -Order Selection Process: - 1. Schema Creation: - - Build OrderSchema from request parameters - - Set target DTE, moneyness range, price limits - - Configure strategy type and structure direction - - 2. Chain Retrieval: - - Fetch option chain for date and underlying - - Cache chains to avoid redundant API calls - - Filter by expiration dates within tolerance - - 3. Strike Filtering: - - Apply moneyness bounds (min/max strike/spot ratios) - - Special handling for calls vs puts - - ITM/OTM width constraints - - 4. Strategy Building: - - Select strikes for strategy legs - - Apply spread_ticks for multi-leg structures - - Validate structure meets requirements - - 5. Price Validation: - - Check total structure price against limits - - Consider bid-ask spreads - - Verify liquidity thresholds - - 6. Result Extraction: - - Package successful order with all details - - Include order metadata and trade_id - - Return ResultsEnum status - -Moneyness Adjustment for Calls: - Puts: moneyness = strike / spot - - OTM: < 1.0 - - ATM: ~1.0 - - ITM: > 1.0 - - Calls: moneyness inverted for consistency - - Original put range: [0.90, 1.10] - - Call range: [2-1.10, 2-0.90] = [0.90, 1.10] - - Maintains same OTM/ITM interpretation - -Configuration Objects: - ChainConfig: - - Data source settings - - Chain retrieval parameters - - Filtering options - - OrderSchemaConfigs: - - Default DTE, moneyness, prices - - Strategy defaults - - Structure direction defaults - - OrderPickerConfig: - - Start/end dates for data range - - Cache configuration - - Retry settings - - OrderResolutionConfig: - - Max tries - - Tolerance expansions - - Resolution priorities - -Lookback Management: - - Precomputed date lookback dictionaries - - Efficient date arithmetic for DTE calculations - - Configurable lookback ranges (30, 60, 90 days) - - Global LOOKBACKS dict for performance - -Caching Strategy: - Memoization: - - @dynamic_memoize decorator on _get_order() - - Schema tuple used as cache key - - Avoids redundant chain fetching - - Cleared between backtest runs - - Chain Caching: - - populate_cache_with_chain() for data - - Persistent across order requests - - Indexed by (ticker, date, spot) - -Retry Logic Integration: - - Uses order_resolve_loop from _orders module - - Progressive constraint relaxation - - Logged attempts for debugging - - Configurable max_tries limit - - Returns best available or failure - -Order Schema Structure: - Required Fields: - - tick: Underlying ticker - - target_dte: Target days to expiration - - strategy: Strategy name (e.g., 'vertical_spread') - - structure_direction: 'long' or 'short' - - spread_ticks: Strike separation for spreads - - dte_tolerance: Acceptable DTE deviation - - min_moneyness: Lower bound for strike/spot - - max_moneyness: Upper bound for strike/spot - - min_total_price: Minimum structure price - - option_type: 'C' or 'P' - - max_total_price: Upper price limit - - Chain Config Fields (merged): - - Liquidity filters - - Data source settings - - Additional constraints - -Usage Examples: - # Initialize order picker - picker = OrderPicker( - start_date='2024-01-01', - end_date='2024-12-31' - ) - - # Create order request - request = OrderRequest( - symbol='AAPL', - date='2024-03-15', - spot=185.50, - option_type='P', - target_dte=45, - max_close=3.50 - ) - - # Get order - order = picker.get_order(request) - - # Check result - if not order_failed(order): - print(f"Selected: {order['data']['trade_id']}") - print(f"Price: ${order['data']['total_price']:.2f}") - else: - print(f"Failed: {order['result']}") - -Performance Optimizations: - - Schema memoization prevents redundant calculations - - Chain caching reduces API calls - - Precomputed lookbacks for date math - - Efficient pandas filtering operations - - Lazy loading of option data - -Integration Points: - - Called by RiskManager for new position orders - - Used in position rolling for replacement orders - - Feeds OrderRequest dataclasses - - Returns Order type for execution - -Error Handling: - - Invalid option types caught and corrected - - Missing chains logged and handled gracefully - - Failed orders return descriptive ResultsEnum - - Schema validation before processing - -Notes: - - Lookback setter triggers precomputation if needed - - get_order_new() is the public interface - - _get_order() is the memoized implementation - - Schema converted to tuple for hashability in cache - - print_url param useful for debugging data sources + OrderPicker: Selects and formats orders from `OrderRequest` using scoring. + +Processing Flow: + 1. Normalize request-level cash constraints. + 2. Build scored candidate order via `order_builder`. + 3. Attach signal metadata and normalize output shape. + 4. Return typed `Order` with `ResultsEnum` status. + +Risk/Assumptions: + - `get_order(request=...)` is the only supported selection API. + - Legacy methods (`get_order_schema`, `get_order_new`, `_get_order`, + `construct_inputs`) raise hard deprecation errors. + - Scoring limits are derived from `ScoringConfigs` with per-request caps. + +Usage: + >>> picker = OrderPicker(start_date="2024-01-01", end_date="2024-12-31") + >>> order = picker.get_order(request) """ from copy import deepcopy from datetime import datetime import pandas as pd -from EventDriven.riskmanager._order_validator import OrderInputs from trade.datamanager.market_data import Optional from ..utils import ( LOOKBACKS, - populate_cache_with_chain, precompute_lookbacks, ) -from EventDriven.configs.core import ChainConfig, OrderSchemaConfigs, OrderPickerConfig, OrderResolutionConfig, ScoringConfigs +from EventDriven.configs.core import OrderPickerConfig, ScoringConfigs from EventDriven.types import OrderData, ResultsEnum -from ..utils import ( - dynamic_memoize, # noqa - parse_position_id -) +from ..utils import parse_position_id from .builder import order_builder from trade.helpers.Logging import setup_logger -from trade.helpers.decorators import timeit # noqa from EventDriven.riskmanager.picker import OrderSchema, _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") +def order_failed(order: dict) -> bool: + """Return True when the picker result is not successful.""" + return order.get("result") != ResultsEnum.SUCCESSFUL.value + + class OrderPicker: """ This class is solely responsible for picking orders based on predefined schemas and configurations. All it does is find the right order based on the schema provided. It does not execute trades or manage risk. """ - BUILD_WITH_SCORING_METHOD: bool = True # Class variable to toggle between standard builder and scoring builder methods def __init__(self, start_date: str | datetime, end_date: str | datetime): """ @@ -225,10 +66,7 @@ def __init__(self, start_date: str | datetime, end_date: str | datetime): self.__lookback = 30 ## Setting up configs - self._chain_config = ChainConfig() self._order_picker_config = OrderPickerConfig(start_date=start_date, end_date=end_date) - self._order_schema_config = OrderSchemaConfigs() - self._order_resolution_config = OrderResolutionConfig() self._scoring_config = ScoringConfigs() ## Others @@ -240,58 +78,25 @@ def __repr__(self): @property def lookback(self): return self.__lookback - - def _overwrite_scoring_configs_with_schema_config(self, configs: ScoringConfigs = None) -> ScoringConfigs: - """ - This function takes the scoring configs and overwrites any values with the corresponding values from the order schema config if they exist. This allows us to use the order schema config as a source of truth for scoring parameters, while still allowing for dynamic overrides through the scoring configs. - """ - if configs is None: - configs = ScoringConfigs() - - ## key mapping is attr in schema config : attr in scoring config - mapping = { - "spread_ticks": "spread_ticks", - "strategy": "strategy", - "structure_direction": "structure_direction", - "min_moneyness": "min_moneyness", - "max_moneyness": "max_moneyness", - "target_dte": "target_dte", - "dte_tolerance": "dte_tolerance", - "min_total_price": "mid_min", - } - for schema_attr, scoring_attr in mapping.items(): - schema_value = getattr(self._order_schema_config, schema_attr, None) - if schema_value is not None: - setattr(configs, scoring_attr, schema_value) - return configs - - def _overwrite_configs(self): - """ - This function overwrites the scoring configs with the values from the order schema config. This is useful for ensuring that the scoring functions use the same parameters as defined in the order schema config, which is the source of truth for our order selection criteria. - """ - self._scoring_config = self._overwrite_scoring_configs_with_schema_config(self._scoring_config) - + def _get_order_with_scoring(self, req: OrderRequest) -> OrderData: """ This function is an alternative to the standard _get_order function that incorporates scoring into the order selection process. It builds the order using the builder function that includes scoring, and then validates the resulting order before returning it. """ - ## Create a copy per request. + ## Create a copy per request. ## This is because we'll be changing mid_max from req configs = deepcopy(self._scoring_config) - configs.mid_upper_limit = req.max_close + + ## Limit the mid price to the tick cash value from the request. Don't entertain any orders that are priced above the cash available + configs.mid_upper_limit = req.tick_cash / 100 if req.is_tick_cash_scaled else req.tick_cash order = order_builder( req=req, configs=configs, ) return order - - def register_preset_order(self, - signal_id: str, - trade_id: str, - date: str | datetime, - close_price: float = np.nan): - + + 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. @@ -299,7 +104,7 @@ def register_preset_order(self, self.preset_orders[signal_id] = { "trade_id": trade_id, "date": pd.to_datetime(date, format="%Y-%m-%d").date(), - "close_price": close_price + "close_price": close_price, } def clear_preset_orders(self): @@ -317,11 +122,7 @@ def get_preset_order(self, signal_id: str, date: str | datetime) -> dict: 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"] - ) + data = _order_formatting(trade_id=preset_order["trade_id"], legs=legs, close=preset_order["close_price"]) return { "result": ResultsEnum.SUCCESSFUL.value, "data": data, @@ -331,27 +132,10 @@ def get_preset_order(self, signal_id: str, date: str | datetime) -> dict: return {} def get_order_schema(self, ticker: str, option_type: str = "P", max_total_price: float = None) -> OrderSchema: - """ - Get the current order schema based on the order schema configurations. - """ - schema = OrderSchema( - { - "tick": ticker, - "target_dte": self._order_schema_config.target_dte, - "strategy": self._order_schema_config.strategy, - "structure_direction": self._order_schema_config.structure_direction, - "spread_ticks": self._order_schema_config.spread_ticks, - "dte_tolerance": self._order_schema_config.dte_tolerance, - "min_moneyness": self._order_schema_config.min_moneyness, - "max_moneyness": self._order_schema_config.max_moneyness, - "min_total_price": self._order_schema_config.min_total_price, - "option_type": option_type, # Default to Put options - "max_total_price": max_total_price, - "max_attempts": self._order_schema_config.max_attempts, - } + raise AttributeError( + "OrderPicker.get_order_schema is deprecated and has been removed. " + "Use OrderPicker.get_order(request=OrderRequest(...)) instead." ) - schema.data.update(self._chain_config.__dict__) - return schema @lookback.setter def lookback(self, value): @@ -370,9 +154,10 @@ def get_order_new( print_url: bool = False, delta_lmt: Optional[float] = None, ): - schema = tuple(schema.data.items()) - chain_spot = spot - return self._get_order(schema, date, spot, chain_spot, print_url=print_url, delta_lmt=delta_lmt) + raise AttributeError( + "OrderPicker.get_order_new is deprecated and has been removed. " + "Use OrderPicker.get_order(request=OrderRequest(...)) instead." + ) # @dynamic_memoize def _get_order( @@ -384,157 +169,49 @@ def _get_order( print_url: bool = False, delta_lmt: Optional[float] = None, ) -> dict: - """ - 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)) - chain = populate_cache_with_chain(schema["tick"], date, chain_spot, print_url=print_url) - return order_builder(unfiltered_chain=chain.copy(deep=True), schema=schema, spot=chain_spot, date=date, delta_lmt=delta_lmt) + raise AttributeError( + "OrderPicker._get_order is deprecated and has been removed. " + "Use OrderPicker.get_order(request=OrderRequest(...)) instead." + ) # @timeit def get_order(self, request: OrderRequest) -> Order: """ Get the order based on the request. """ - - - - schema = self.get_order_schema( - ticker=request.symbol, option_type=request.option_type, max_total_price=request.max_close - ) - - inputs = self.construct_inputs( - request=request, schema=schema, order_resolution_config=self._order_resolution_config - ) - - if self.BUILD_WITH_SCORING_METHOD: - order = self._get_order_with_scoring(request) - - ## Add necessary tags for identification - order["signal_id"] = inputs.signal_id - order["map_signal_id"] = inputs.signal_id - if order_failed(order): - logger.warning(f"Order failed to resolve for request: {request} with schema: {schema}") - return Order.from_dict(order) - order["data"]["quantity"] = 1 - order["date"] = pd.to_datetime(request.date).date() - order = Order.from_dict(order) - return order - - if not self._chain_config.enable_delta_filter: - logger.info("Delta filter not enabled in chain config. Setting delta_lmt to None for order builder.") - request.delta_lmt = None # Ensure delta_lmt is None if delta filtering is not enabled - else: - logger.info(f"Delta filter enabled in chain config. Using delta_lmt {request.delta_lmt} for order builder.") - return _get_open_order_backtest( - picker=self, - request=request, - inputs=inputs, - ) - - def construct_inputs( - self, request: OrderRequest, schema: OrderSchema, order_resolution_config: OrderResolutionConfig = None - ) -> OrderInputs: - """ - Construct OrderInputs dataclass from OrderRequest and OrderSchema. - """ - if order_resolution_config is None: - order_resolution_config = self._order_resolution_config - tick_cash = request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash / 100 if request.max_close > tick_cash: - logger.warning( f"Request max_close {request.max_close} is greater than tick_cash {tick_cash}. Adjusting max_close to tick_cash." ) request.max_close = tick_cash - inputs = OrderInputs( - tick=request.symbol, - date=request.date, - option_type=request.option_type, - signal_id=request.signal_id, - spot=request.spot, - option_strategy=schema["strategy"], - structure_direction=schema["structure_direction"], - spread_ticks=schema.data.get("spread_ticks", 0), - dte_tolerance=schema.data.get("dte_tolerance", 0), - min_moneyness=schema.data.get("min_moneyness", 0), - max_moneyness=schema.data.get("max_moneyness", float("inf")), - target_dte=schema.data.get("target_dte", 0), - min_total_price=schema.data.get("min_total_price", 0), - direction=request.direction, - tick_cash=request.tick_cash, - **order_resolution_config.__dict__, + order = self._get_order_with_scoring(request) + + ## Add necessary tags for identification + order["signal_id"] = request.signal_id + order["map_signal_id"] = request.signal_id + if order_failed(order): + logger.warning(f"Order failed to resolve for request: {request}") + return Order.from_dict(order) + order["data"]["quantity"] = 1 + order["date"] = pd.to_datetime(request.date).date() + order = Order.from_dict(order) + return order + + def construct_inputs(self, request: OrderRequest, schema: OrderSchema) -> None: + """Deprecated legacy entrypoint retained for API compatibility.""" + raise AttributeError( + "OrderPicker.construct_inputs is deprecated and has been removed. " + "Use OrderPicker.get_order(request=OrderRequest(...)) instead." ) - return inputs def _get_open_order_backtest( picker: OrderPicker, request: OrderRequest, - inputs: OrderInputs, ) -> Order: - """ - Helper function to get open order in backtest mode. - OR at least with order picker. - - params: - picker: OrderPicker: The OrderPicker instance to use for getting the order. - request: OrderRequest: The order request containing necessary parameters. - inputs: OrderInputs: The order inputs constructed from the request and schema. - - returns: - Order: The resolved order object. - """ - delta_lmt = request.delta_lmt - 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, - delta_lmt=delta_lmt, - 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=request.chain_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, - tick_cash=request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash / 100, - delta_lmt=delta_lmt, - ) - 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 - order["map_signal_id"] = inputs.signal_id - if order_failed(order): - logger.warning(f"Order failed to resolve for request: {request} with schema: {schema}") - return Order.from_dict(order) - order["data"]["quantity"] = 1 - order["date"] = pd.to_datetime(request.date).date() - order = Order.from_dict(order) - return order + raise AttributeError( + "_get_open_order_backtest is deprecated and has been removed. " + "Use OrderPicker.get_order(request=OrderRequest(...)) instead." + ) diff --git a/EventDriven/tests/test_order_picker_deprecations.py b/EventDriven/tests/test_order_picker_deprecations.py new file mode 100644 index 0000000..cc1688b --- /dev/null +++ b/EventDriven/tests/test_order_picker_deprecations.py @@ -0,0 +1,55 @@ +"""Tests for hard-deprecated legacy order-picker entry points. + +These tests lock in the expected failure mode for legacy APIs while the old +source remains in place for short-term reference. +""" + +import importlib +import sys + +import pytest + +from EventDriven.dataclasses.orders import OrderRequest +from EventDriven.riskmanager.picker.order_picker import OrderPicker + + +def _make_request() -> OrderRequest: + return OrderRequest( + date="2026-01-02", + symbol="AAPL", + option_type="p", + max_close=1.0, + tick_cash=100.0, + direction="LONG", + signal_id="sig-1", + spot=100.0, + ) + + +def test_get_order_schema_raises_hard_error() -> None: + picker = OrderPicker(start_date="2026-01-01", end_date="2026-12-31") + with pytest.raises(AttributeError, match="get_order_schema"): + picker.get_order_schema(ticker="AAPL", option_type="P", max_total_price=1.0) + + +def test_construct_inputs_raises_hard_error() -> None: + picker = OrderPicker(start_date="2026-01-01", end_date="2026-12-31") + request = _make_request() + with pytest.raises(AttributeError, match="construct_inputs"): + picker.construct_inputs(request=request, schema=None) + + +def test_importing_orders_module_raises_import_error() -> None: + module_name = "EventDriven.riskmanager._orders" + sys.modules.pop(module_name, None) + + with pytest.raises(ImportError): + importlib.import_module(module_name) + + +def test_importing_order_validator_module_raises_import_error() -> None: + module_name = "EventDriven.riskmanager._order_validator" + sys.modules.pop(module_name, None) + + with pytest.raises(ImportError): + importlib.import_module(module_name) From bcf5cc72494e7cb13fc46312efa93b872088b110 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:15:39 -0400 Subject: [PATCH 19/81] QuantTools: align config schema and validation with scoring-only OrderPicker --- EventDriven/configs/core.py | 24 +- EventDriven/configs/export_configs.py | 310 +++++++++++++------------- EventDriven/configs/vars.py | 10 - 3 files changed, 158 insertions(+), 186 deletions(-) diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index ed78ece..73d7d68 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -96,20 +96,6 @@ class ZscoreSizerConfigs(BaseSizerConfigs): delta_lmt_type: Literal["default", "zscore"] = "zscore" -@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) -class OrderResolutionConfig(BaseConfigs): - """ - Configuration class for Order Resolution settings. - """ - - resolve_enabled: bool = True - otm_moneyness_width: float = 0.45 - itm_moneyness_width: float = 0.45 - max_close: float = 10.0 - max_tries: int = 20 - max_dte_tolerance: int = 90 - - @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) class UndlTimeseriesConfig(BaseConfigs): """ @@ -305,7 +291,7 @@ class ScoringConfigs(BaseConfigs): mid_max: numbers.Number = 3.0 mid_upper_limit: numbers.Number = 5 mid_lower_limit: numbers.Number = 0.25 - mid_sigma: numbers.Number = 1.0 + mid_sigma: numbers.Number = 0.25 # Spread pct_spread_max: numbers.Number = 1.0 @@ -318,3 +304,11 @@ class ScoringConfigs(BaseConfigs): # Theta burden theta_burden_max: numbers.Number = 0.03 theta_burden_sigma: numbers.Number = 0.02 + + +@pydantic_dataclass +class ExecutionHandlerConfig(BaseConfigs): + slippage_model: Literal["randomized", "fixed", "spread_pct"] = ( + "randomized" # Whether to use randomized slippage, fixed slippage, or slippage as a percentage of the spread. Default is randomized slippage. + ) + pct_alpha: float = 0.25 # If using spread_pct slippage model, this is the percentage of the spread to use as slippage. For example, if pct_alpha is 0.25 and the spread is $0.20, then the slippage will be $0.05. diff --git a/EventDriven/configs/export_configs.py b/EventDriven/configs/export_configs.py index 0f75719..3bdb79c 100644 --- a/EventDriven/configs/export_configs.py +++ b/EventDriven/configs/export_configs.py @@ -1,4 +1,3 @@ - from EventDriven.configs.base import BaseConfigs from collections.abc import Mapping from typing import Any, Iterable, Tuple, Dict, TypeVar, TypedDict, TYPE_CHECKING @@ -13,10 +12,10 @@ ChainConfig, OrderSchemaConfigs, OrderPickerConfig, + ScoringConfigs, BaseSizerConfigs, DefaultSizerConfigs, ZscoreSizerConfigs, - OrderResolutionConfig, UndlTimeseriesConfig, OptionPriceConfig, SkipCalcConfig, @@ -33,7 +32,7 @@ logger = logging.getLogger(__name__) # Type variable for generic config types -T = TypeVar('T', bound=BaseConfigs) +T = TypeVar("T", bound=BaseConfigs) # TypedDict for all config types from core.py @@ -42,37 +41,39 @@ class ConfigsDict(TypedDict, total=False): """ Type hints for all available config classes. - + Keys are config class names (without _1, _2 suffixes). Values are the actual config instances. - + Example: bundle.configs["ChainConfig"] # Type: ChainConfig bundle.get("PortfolioManagerConfig") # Type: PortfolioManagerConfig """ - ChainConfig: 'ChainConfig' - OrderSchemaConfigs: 'OrderSchemaConfigs' - OrderPickerConfig: 'OrderPickerConfig' - BaseSizerConfigs: 'BaseSizerConfigs' - DefaultSizerConfigs: 'DefaultSizerConfigs' - ZscoreSizerConfigs: 'ZscoreSizerConfigs' - OrderResolutionConfig: 'OrderResolutionConfig' - UndlTimeseriesConfig: 'UndlTimeseriesConfig' - OptionPriceConfig: 'OptionPriceConfig' - SkipCalcConfig: 'SkipCalcConfig' - BaseCogConfig: 'BaseCogConfig' - StrategyLimitsEnabled: 'StrategyLimitsEnabled' - LimitsEnabledConfig: 'LimitsEnabledConfig' - PositionAnalyzerConfig: 'PositionAnalyzerConfig' - PortfolioManagerConfig: 'PortfolioManagerConfig' - BacktesterConfig: 'BacktesterConfig' - CashAllocatorConfig: 'CashAllocatorConfig' - RiskManagerConfig: 'RiskManagerConfig' + + ChainConfig: "ChainConfig" + OrderSchemaConfigs: "OrderSchemaConfigs" + OrderPickerConfig: "OrderPickerConfig" + ScoringConfigs: "ScoringConfigs" + BaseSizerConfigs: "BaseSizerConfigs" + DefaultSizerConfigs: "DefaultSizerConfigs" + ZscoreSizerConfigs: "ZscoreSizerConfigs" + UndlTimeseriesConfig: "UndlTimeseriesConfig" + OptionPriceConfig: "OptionPriceConfig" + SkipCalcConfig: "SkipCalcConfig" + BaseCogConfig: "BaseCogConfig" + StrategyLimitsEnabled: "StrategyLimitsEnabled" + LimitsEnabledConfig: "LimitsEnabledConfig" + PositionAnalyzerConfig: "PositionAnalyzerConfig" + PortfolioManagerConfig: "PortfolioManagerConfig" + BacktesterConfig: "BacktesterConfig" + CashAllocatorConfig: "CashAllocatorConfig" + RiskManagerConfig: "RiskManagerConfig" @dataclass class ConfigLocation: """Metadata about where a config lives in the object graph.""" + label: str config_class: str path: str @@ -88,32 +89,33 @@ class RunConfigBundle: configs: ConfigsDict # Using TypedDict for specific type hints metadata: dict[str, dict] = field(default_factory=dict) # Stores path info per config - def save_to_yaml(self, filename: str): """ Save the configs and metadata to a YAML file. - + Args: filename (str): The path to the file where configs will be saved. """ data = { - 'run_name': self.run_name, - 'created_at': self.created_at.isoformat() if isinstance(self.created_at, datetime) else str(self.created_at), - 'configs': self.configs, - 'metadata': self.metadata + "run_name": self.run_name, + "created_at": self.created_at.isoformat() + if isinstance(self.created_at, datetime) + else str(self.created_at), + "configs": self.configs, + "metadata": self.metadata, } # Use safe_dump to avoid Python object tags with open(filename, "w") as f: yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False) - + @classmethod - def load_from_yaml(cls, filename: str = None, data = None) -> 'RunConfigBundle': + def load_from_yaml(cls, filename: str = None, data=None) -> "RunConfigBundle": """ Load a config bundle from a YAML file. - + Args: filename (str): The path to the YAML file. - + Returns: RunConfigBundle: The loaded config bundle. """ @@ -122,8 +124,8 @@ def load_from_yaml(cls, filename: str = None, data = None) -> 'RunConfigBundle': with open(filename, "r") as f: data = yaml.safe_load(f) - confs = data.get('configs', {}) - + confs = data.get("configs", {}) + conf_bund_cls: ConfigsDict = {} # type: ignore for label in confs.keys(): # Extract class name without _1, _2 suffixes @@ -132,24 +134,26 @@ def load_from_yaml(cls, filename: str = None, data = None) -> 'RunConfigBundle': conf_cls = getattr(cls_module, conf_name) # Store with just the class name (no _1 suffix) conf_bund_cls[conf_name] = conf_cls(**confs[label]) # type: ignore - - ret = cls( - run_name=data.get('run_name', ''), - created_at=datetime.fromisoformat(data['created_at']) if isinstance(data.get('created_at'), str) else data.get('created_at', datetime.now()), + + ret = cls( + run_name=data.get("run_name", ""), + created_at=datetime.fromisoformat(data["created_at"]) + if isinstance(data.get("created_at"), str) + else data.get("created_at", datetime.now()), configs=conf_bund_cls, - metadata=data.get('metadata', {}) + metadata=data.get("metadata", {}), ) return ret - + def apply_to(self, root: Any, strict: bool = True, verify_paths: bool = True) -> Dict[str, str]: """ Apply this bundle's configs to an object. - + Args: root: The root object to apply configs to strict: If True, raise errors on mismatches verify_paths: If True, verify config paths match metadata - + Returns: Dict mapping labels to their applied paths """ @@ -158,30 +162,32 @@ def apply_to(self, root: Any, strict: bool = True, verify_paths: bool = True) -> def get(self, label: str) -> BaseConfigs: """ Get a config by label with proper type hints. - + Since configs are stored by class name (without _1 suffix), - access them directly: bundle.get("ChainConfig") - + access them directly: bundle.get("ChainConfig") + Args: label: The config class name (e.g., "ChainConfig", not "ChainConfig_1") - + Returns: The config instance with proper type hint from ConfigsDict - + Example: >>> chain_config = bundle.get("ChainConfig") # Returns ChainConfig instance >>> pm_config = bundle.get("PortfolioManagerConfig") """ if label not in self.configs: raise KeyError(f"Config with label '{label}' not found. Available: {list(self.configs.keys())}") - + return self.configs[label] # type: ignore def __repr__(self): return f"RunConfigBundle(run_name={self.run_name!r}, created_at={self.created_at!r}, configs={list(self.configs.keys())})" -def collect_run_configs(root: Any, debug: bool = False, include_metadata: bool = False) -> Dict[str, BaseConfigs] | Tuple[Dict[str, BaseConfigs], Dict[str, ConfigLocation]]: +def collect_run_configs( + root: Any, debug: bool = False, include_metadata: bool = False +) -> Dict[str, BaseConfigs] | Tuple[Dict[str, BaseConfigs], Dict[str, ConfigLocation]]: """ Return a dict of {label: config_instance} for all *unique* BaseConfigs instances under `root`. @@ -189,12 +195,12 @@ def collect_run_configs(root: Any, debug: bool = False, include_metadata: bool = - Dedupes by object id globally in this call - Keeps *all* instances for a given class - Label format: "_" where i is per-class index - + Args: root: The root object to collect configs from debug: If True, print debug info include_metadata: If True, return (configs, metadata) tuple - + Returns: Dict of {label: config} or tuple of (configs, metadata) """ @@ -214,7 +220,7 @@ def collect_run_configs(root: Any, debug: bool = False, include_metadata: bool = label = f"{cls_name}_{counts[cls_name]}" # Parse path to extract parent and attribute - parts = path.rsplit('.', 1) + parts = path.rsplit(".", 1) parent_path = parts[0] if len(parts) > 1 else "root" attr_name = parts[1] if len(parts) > 1 else path @@ -225,7 +231,7 @@ def collect_run_configs(root: Any, debug: bool = False, include_metadata: bool = path=path, parent_path=parent_path, attribute_name=attr_name, - object_id=cfg_id + object_id=cfg_id, ) metadata[label] = location @@ -247,27 +253,27 @@ def _sanitize_for_yaml(obj: Any) -> Any: """ if obj is None or isinstance(obj, (str, int, float, bool)): return obj - - if isinstance(obj, (datetime, )): + + if isinstance(obj, (datetime,)): return obj.isoformat() - + if isinstance(obj, dict): return {k: _sanitize_for_yaml(v) for k, v in obj.items()} - + if isinstance(obj, (list, tuple)): return [_sanitize_for_yaml(item) for item in obj] - + if isinstance(obj, BaseConfigs): # Recursively sanitize config objects - return {k: _sanitize_for_yaml(v) for k, v in obj.__dict__.items() if not k.startswith('_')} - + return {k: _sanitize_for_yaml(v) for k, v in obj.__dict__.items() if not k.startswith("_")} + # For other objects, try to convert to dict or return string representation - if hasattr(obj, '__dict__'): + if hasattr(obj, "__dict__"): try: - return {k: _sanitize_for_yaml(v) for k, v in obj.__dict__.items() if not k.startswith('_')} + return {k: _sanitize_for_yaml(v) for k, v in obj.__dict__.items() if not k.startswith("_")} except Exception: return str(obj) - + return str(obj) @@ -291,23 +297,18 @@ def export_run_configs(root: Any, debug: bool = False, remove_underscore: bool = # exported[label] = _sanitize_for_yaml(dict(cfg.__dict__)) exported[new_label] = cfg if run_name is None and hasattr(exported[new_label], "run_name"): - run_name = getattr(exported[new_label], "run_name") # noqa - + run_name = getattr(exported[new_label], "run_name") # noqa + # Store metadata as dict (for YAML serialization) loc = metadata_objs[label] metadata_dict[new_label] = { - 'config_class': loc.config_class, - 'path': loc.path, - 'parent_path': loc.parent_path, - 'attribute_name': loc.attribute_name + "config_class": loc.config_class, + "path": loc.path, + "parent_path": loc.parent_path, + "attribute_name": loc.attribute_name, } - - return RunConfigBundle( - run_name=run_name, - created_at=datetime.now(), - configs=exported, - metadata=metadata_dict - ) + + return RunConfigBundle(run_name=run_name, created_at=datetime.now(), configs=exported, metadata=metadata_dict) # def walk_configs(root: Any, _seen=None): @@ -351,6 +352,7 @@ def export_run_configs(root: Any, debug: bool = False, remove_underscore: bool = # for v in attrs.values(): # yield from walk_configs(v, _seen) + def walk_configs(root: Any, _seen=None, _path: str = "root") -> Iterable[Tuple[BaseConfigs, str]]: """ Recursively walk an object graph starting at `root` and yield @@ -443,42 +445,42 @@ def apply_run_configs( configs: Dict[str, dict], metadata: Dict[str, dict] = None, strict: bool = True, - verify_paths: bool = True + verify_paths: bool = True, ) -> Dict[str, str]: """ Apply configs (as dicts from YAML) back to the object graph with validation. - + This is Strategy #1: Path-based verification. - + Args: root: The root object (e.g., OptionSignalBacktest instance) configs: Dict of {label: config_dict} from YAML/export metadata: Dict of {label: metadata_dict} with path info strict: If True, raise error if config not found verify_paths: If True, verify paths match metadata - + Returns: Dict mapping labels to their paths in the object graph - + Raises: ValueError: If config not found or type mismatch TypeError: If config type doesn't match expected type - + Example: >>> # Export configs >>> bundle = export_run_configs(backtest) >>> bundle.save_to_yaml('my_configs.yaml') - >>> + >>> >>> # Later, load and apply >>> bundle = RunConfigBundle.load_from_yaml('my_configs.yaml') >>> bundle.apply_to(new_backtest) """ # Collect current config locations current_configs, current_metadata = collect_run_configs(root, debug=False, include_metadata=True) - + applied = {} errors = [] - + for label, config_dict in configs.items(): # Check if this config exists in current object if label not in current_configs: @@ -487,15 +489,15 @@ def apply_run_configs( raise ValueError(msg) errors.append(msg) continue - + current_config = current_configs[label] current_loc = current_metadata[label] - + # Verify config class matches (using metadata if available) if metadata and label in metadata: - expected_class = metadata[label].get('config_class') + expected_class = metadata[label].get("config_class") actual_class = current_loc.config_class - + if expected_class != actual_class: msg = ( f"Config class mismatch for {label}:\n" @@ -507,12 +509,12 @@ def apply_run_configs( raise TypeError(msg) errors.append(msg) continue - + # Verify path matches (if requested) if verify_paths: - expected_path = metadata[label].get('path') + expected_path = metadata[label].get("path") actual_path = current_loc.path - + if expected_path != actual_path: msg = ( f"Config path mismatch for {label}:\n" @@ -524,10 +526,10 @@ def apply_run_configs( if strict: raise ValueError(msg) errors.append(msg) - + # Apply config by updating attributes for field_name, field_value in config_dict.items(): - if not field_name.startswith('_'): # Skip private fields + if not field_name.startswith("_"): # Skip private fields try: setattr(current_config, field_name, field_value) except Exception as e: @@ -535,77 +537,76 @@ def apply_run_configs( if strict: raise ValueError(msg) from e errors.append(msg) - + applied[label] = current_loc.path - + if errors and not strict: logger.warning(f"Applied configs with {len(errors)} warnings:\n" + "\n".join(f" - {e}" for e in errors)) - + return applied def validate_config_placement(root: Any, raise_on_error: bool = False) -> list[str]: """ Validate that all configs are correctly placed and typed. - + This is Strategy #4: Validation function. - + Args: root: The root object to validate raise_on_error: If True, raise ValueError on first error - + Returns: List of validation errors (empty if all valid) - + Example: >>> errors = validate_config_placement(backtest) >>> if errors: ... print("Validation errors:", errors) """ errors = [] - + # Define expected config types for known attributes EXPECTED_CONFIGS = { - 'OptionSignalBacktest': { - 'config': 'BacktesterConfig', + "OptionSignalBacktest": { + "config": "BacktesterConfig", }, - 'OptionSignalPortfolio': { - 'config': 'PortfolioManagerConfig', - 'cash_allocator_config': 'CashAllocatorConfig', + "OptionSignalPortfolio": { + "config": "PortfolioManagerConfig", + "cash_allocator_config": "CashAllocatorConfig", }, - 'RiskManager': { - 'config': 'RiskManagerConfig', + "RiskManager": { + "config": "RiskManagerConfig", }, - 'OrderPicker': { - 'config': 'OrderPickerConfig', - 'order_schema_configs': 'OrderSchemaConfigs', - 'chain_config': 'ChainConfig', + "OrderPicker": { + "_order_picker_config": "OrderPickerConfig", + "_scoring_config": "ScoringConfigs", }, - 'PositionAnalyzer': { - 'config': 'PositionAnalyzerConfig', + "PositionAnalyzer": { + "config": "PositionAnalyzerConfig", }, - 'LimitsAndSizingCog': { - 'config': 'LimitsAndSizingConfig', - 'sizer_configs': ('DefaultSizerConfigs', 'ZscoreSizerConfigs'), # Can be either - 'limits_enabled_config': 'LimitsEnabledConfig', + "LimitsAndSizingCog": { + "config": "LimitsAndSizingConfig", + "sizer_configs": ("DefaultSizerConfigs", "ZscoreSizerConfigs"), # Can be either + "limits_enabled_config": "LimitsEnabledConfig", }, } - + def check_object(obj, path="root", _seen=None): if _seen is None: _seen = set() - + obj_id = id(obj) if obj_id in _seen: return _seen.add(obj_id) - + obj_type = type(obj).__name__ - + # Check if this object type has expected configs if obj_type in EXPECTED_CONFIGS: expected = EXPECTED_CONFIGS[obj_type] - + for attr_name, expected_type in expected.items(): if not hasattr(obj, attr_name): error_msg = f"{path}: Missing required config attribute '{attr_name}'" @@ -613,51 +614,45 @@ def check_object(obj, path="root", _seen=None): if raise_on_error: raise ValueError(error_msg) continue - + attr_value = getattr(obj, attr_name) if attr_value is None: continue # Allow None values - + actual_type_name = type(attr_value).__name__ - + # Handle multiple allowed types if isinstance(expected_type, tuple): if actual_type_name not in expected_type: - error_msg = ( - f"{path}.{attr_name}: Expected one of {expected_type}, " - f"got {actual_type_name}" - ) + error_msg = f"{path}.{attr_name}: Expected one of {expected_type}, got {actual_type_name}" errors.append(error_msg) if raise_on_error: raise TypeError(error_msg) else: if actual_type_name != expected_type: - error_msg = ( - f"{path}.{attr_name}: Expected {expected_type}, " - f"got {actual_type_name}" - ) + error_msg = f"{path}.{attr_name}: Expected {expected_type}, got {actual_type_name}" errors.append(error_msg) if raise_on_error: raise TypeError(error_msg) - + # Recurse into nested objects (avoid infinite loops) try: attrs = vars(obj) except TypeError: return - + for attr_name, attr_value in attrs.items(): - if attr_name.startswith('_') or attr_name == 'logger': + if attr_name.startswith("_") or attr_name == "logger": continue - + child_path = f"{path}.{attr_name}" - + if isinstance(attr_value, BaseConfigs): # Configs themselves might contain configs check_object(attr_value, child_path, _seen) - elif hasattr(attr_value, '__dict__') and not isinstance(attr_value, (str, int, float, list, dict, tuple)): + elif hasattr(attr_value, "__dict__") and not isinstance(attr_value, (str, int, float, list, dict, tuple)): check_object(attr_value, child_path, _seen) - + check_object(root) return errors @@ -668,11 +663,11 @@ def apply_and_validate_configs( metadata: Dict[str, dict] = None, strict: bool = True, verify_paths: bool = True, - validate_after: bool = True + validate_after: bool = True, ) -> Dict[str, str]: """ Apply configs with full safety checks (Strategies 1 + 4 combined). - + Args: root: The root object to apply configs to configs: Dict of {label: config_dict} @@ -680,34 +675,28 @@ def apply_and_validate_configs( strict: If True, raise errors on mismatches verify_paths: If True, verify paths match metadata validate_after: If True, run validation after applying - + Returns: Dict mapping labels to their paths - + Raises: ValueError: If validation fails - + Example: >>> # Load from YAML >>> bundle = RunConfigBundle.load_from_yaml('configs.yaml') - >>> + >>> >>> # Apply with full validation >>> results = apply_and_validate_configs( - ... backtest, - ... bundle.configs, + ... backtest, + ... bundle.configs, ... bundle.metadata ... ) >>> print(f"Applied {len(results)} configs successfully") """ # Apply configs with path verification - results = apply_run_configs( - root, - configs, - metadata=metadata, - strict=strict, - verify_paths=verify_paths - ) - + results = apply_run_configs(root, configs, metadata=metadata, strict=strict, verify_paths=verify_paths) + # Validate placement if validate_after: errors = validate_config_placement(root, raise_on_error=strict) @@ -717,6 +706,5 @@ def apply_and_validate_configs( raise ValueError(error_msg) else: logger.warning(error_msg) - - return results + return results diff --git a/EventDriven/configs/vars.py b/EventDriven/configs/vars.py index d9589ce..e6432c6 100644 --- a/EventDriven/configs/vars.py +++ b/EventDriven/configs/vars.py @@ -8,7 +8,6 @@ "BaseSizerConfigs": "Base configuration for position sizing modules, defining the type of delta limit calculation.", "DefaultSizerConfigs": "Standard position sizing configuration using fixed leverage without volatility adjustments.", "ZscoreSizerConfigs": "Advanced position sizing configuration using volatility-adjusted z-score based limits for dynamic risk management.", - "OrderResolutionConfig": "Configuration for automatic order resolution when initial order schemas fail to find suitable options.", "UndlTimeseriesConfig": "Configuration for underlying asset timeseries data retrieval and interval settings.", "OptionPriceConfig": "Configuration specifying which price type (bid, ask, midpoint, close) to use for option valuation.", "SkipCalcConfig": "Configuration for anomaly detection in option data, determining when to skip calculations due to data quality issues.", @@ -70,15 +69,6 @@ "norm_const": "Normalization constant for z-score calculation to scale the volatility adjustment.", "delta_lmt_type": "Type of delta limit calculation to use: 'default' uses fixed limits, 'zscore' uses volatility-adjusted limits.", }, - "OrderResolutionConfig": { - "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", - "resolve_enabled": "Flag to enable or disable automatic order resolution when initial order schema fails.", - "otm_moneyness_width": "Maximum OTM moneyness width for ATM vs OTM option selection (default 0.45).", - "itm_moneyness_width": "Maximum ITM moneyness width for ATM vs ITM option selection (default 0.45).", - "max_close": "Maximum close price allowed for the order structure (default 10.0).", - "max_tries": "Maximum number of attempts to resolve an order schema before giving up (default 20).", - "max_dte_tolerance": "Maximum days to expiration tolerance allowed for the order (default 90).", - }, "UndlTimeseriesConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", "interval": "Time interval for underlying price data (e.g., '1d' for daily, '1h' for hourly).", From 6cad8cea28da7e8bec2b3aa8d6edc7eca765336d Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:15:47 -0400 Subject: [PATCH 20/81] QuantTools: improve position lifecycle handling, spread-aware pricing, and sizing safeguards --- EventDriven/attribution.py | 29 +++--- EventDriven/dataclasses/timeseries.py | 8 ++ EventDriven/execution.py | 80 +++++++++++++++- EventDriven/helpers.py | 8 +- EventDriven/new_portfolio.py | 93 +++++++++++++------ EventDriven/riskmanager/market_timeseries.py | 6 ++ EventDriven/riskmanager/new_base.py | 5 +- .../riskmanager/position/cogs/limits.py | 61 +++++++++++- .../position/cogs/mean_reversion.py | 3 + EventDriven/types.py | 13 ++- 10 files changed, 256 insertions(+), 50 deletions(-) diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index e37847d..788d9cf 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -380,6 +380,7 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: def compute_backtest_position_attribution( portfolio: OptionSignalPortfolio, trade_id: TradeID, + signal_id: SignalID, ) -> BacktestPositionAttribution: """Compute position attribution for a given TradeID within a backtest portfolio. @@ -398,7 +399,7 @@ def compute_backtest_position_attribution( ValueError: If trade_id is not found in portfolio.trades_map. """ # Retrieve the trade object from the portfolio using the trade_id - trade_obj: Trade = portfolio.trades_map.get(trade_id) + trade_obj: Trade = portfolio._get_trade_object(trade_id, signal_id) if not trade_obj: raise ValueError(f"TradeID {trade_id} not found in portfolio trades_map") @@ -432,35 +433,37 @@ class PositionAttributionAnalyzer: def __init__(self, portfolio: OptionSignalPortfolio): self.portfolio = portfolio - self.attribution_cache: Dict[TradeID, BacktestPositionAttribution] = {} + self.attribution_cache: Dict[Tuple[TradeID, SignalID], BacktestPositionAttribution] = {} - def analyze_trade(self, trade_id: TradeID, force: bool = False) -> BacktestPositionAttribution: + def analyze_trade(self, trade_id: TradeID, signal_id: SignalID, force: bool = False) -> BacktestPositionAttribution: """Analyze a specific trade by computing its position attribution. Args: trade_id: The TradeID of the trade to analyze. + signal_id: The SignalID associated with the trade. force: If True, forces re-computation even if the result is cached. Returns: A BacktestPositionAttribution containing the attribution analysis for the specified trade. """ - if trade_id not in self.attribution_cache or force: - self.attribution_cache[trade_id] = compute_backtest_position_attribution(self.portfolio, trade_id) - return self.attribution_cache[trade_id] + trade_key = self.portfolio._get_trade_key(trade_id, signal_id) + if trade_key not in self.attribution_cache or force: + self.attribution_cache[trade_key] = compute_backtest_position_attribution(self.portfolio, trade_id, signal_id) + return self.attribution_cache[trade_key] - def analyze_all_trades(self, force: bool = False) -> Dict[TradeID, BacktestPositionAttribution]: + def analyze_all_trades(self, force: bool = False) -> Dict[Tuple[TradeID, SignalID], BacktestPositionAttribution]: """Analyze all trades in the portfolio by computing their position attributions. Args: force: If True, forces re-computation even if results are cached. Returns: - A dictionary mapping each TradeID to its BacktestPositionAttribution. + A dictionary mapping each (TradeID, SignalID) tuple to its BacktestPositionAttribution. """ - for trade_id in tqdm(self.portfolio.trades_map.keys(), desc="Analyzing trades"): - if trade_id not in self.attribution_cache or force: - self.attribution_cache[trade_id] = compute_backtest_position_attribution(self.portfolio, trade_id) + for trade_key, trade_obj in tqdm(self.portfolio.trades_map.items(), desc="Analyzing trades"): + if trade_key not in self.attribution_cache or force: + self.attribution_cache[trade_key] = compute_backtest_position_attribution(self.portfolio, trade_obj.trade_id, trade_obj.signal_id) return self.attribution_cache def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: bool = False) -> pd.DataFrame: @@ -486,10 +489,10 @@ def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: raise ValueError("No attributions computed yet. Please run analyze_all_trades first.") if not ignore_missing: missing_trades = [ - trade_id for trade_id in self.portfolio.trades_map.keys() if trade_id not in self.attribution_cache + trade_key for trade_key in self.portfolio.trades_map.keys() if trade_key not in self.attribution_cache ] if missing_trades: - raise ValueError(f"Missing attributions for TradeIDs: {missing_trades}") + raise ValueError(f"Missing attributions for TradeKeys: {missing_trades}") records = [] for attr in self.attribution_cache.values(): df = attr.attribution.copy() diff --git a/EventDriven/dataclasses/timeseries.py b/EventDriven/dataclasses/timeseries.py index 774522b..e280edb 100644 --- a/EventDriven/dataclasses/timeseries.py +++ b/EventDriven/dataclasses/timeseries.py @@ -40,6 +40,14 @@ def get_price(self) -> numbers.Number: return self.midpoint else: raise ValueError(f"Invalid use_price value: {self.use_price}") + + def get_spread(self) -> numbers.Number: + """ + Get the spread based on the bid and ask prices. + """ + if self.bid <= 0 or self.ask <= 0 or self.ask < self.bid: + return 0.0 # Invalid spread + return self.ask - self.bid @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True), kw_only=True, frozen=True) diff --git a/EventDriven/execution.py b/EventDriven/execution.py index 7f7572e..d268ba4 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -1,12 +1,15 @@ import math +from typing import Optional import numpy as np from abc import ABCMeta, abstractmethod - from EventDriven.event import ExerciseEvent, FillEvent, OrderEvent from trade.helpers.helper import parse_option_tick from trade.helpers.Logging import setup_logger +from .configs.core import ExecutionHandlerConfig from copy import deepcopy + + logger = setup_logger('EventDriven.execution') exec_cache = { 'order': {}, @@ -54,7 +57,7 @@ class SimulatedExecutionHandler(ExecutionHandler): handler. """ - def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : float = 0.0065): + def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : float = 0.0065, config: Optional[ExecutionHandlerConfig] = None): """ Initialises the handler, setting the event queues up internally. @@ -70,6 +73,7 @@ def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : f self.max_slippage_pct = max_slippage_pct self.min_slippage_pct = max_slippage_pct / 2 # Minimum slippage is half of max slippage self.commission_rate = commission_rate + self.config: ExecutionHandlerConfig = config or ExecutionHandlerConfig() def execute_order_naively(self, order_event: OrderEvent): """ @@ -85,6 +89,72 @@ def execute_order_naively(self, order_event: OrderEvent): fill_event = FillEvent(order_event.datetime, order_event.symbol, 'ARCA', order_event.quantity, order_event.direction, fill_cost=0, commission=None, option=order_event.option, parent_event=order_event) self.events.put(fill_event) + + + + def calculate_slippage_value_randomized(self, order_event: OrderEvent) -> float: + """ + Calculate slippage value based on a random percentage within the specified slippage range. + The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + """ + if order_event.direction == 'BUY': + slippage_pct = np.random.uniform(self.min_slippage_pct, self.max_slippage_pct) ## Ensure that slippage is always positive, and never 0 or more than max_slippage_pct + elif order_event.direction == 'SELL': + slippage_pct = np.random.uniform(-self.max_slippage_pct, -self.min_slippage_pct) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct + else: + raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") + + slippage_value = order_event.position['close'] * slippage_pct * order_event.quantity + + return slippage_value + + def calculate_slippage_value_fixed(self, order_event: OrderEvent) -> float: + """ + Calculate slippage value based on a fixed percentage defined by max_slippage_pct. + The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + """ + if order_event.direction == 'BUY': + slippage_pct = self.max_slippage_pct + elif order_event.direction == 'SELL': + slippage_pct = -self.max_slippage_pct + else: + raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") + + slippage_value = order_event.position['close'] * slippage_pct * order_event.quantity + return slippage_value + + def calculate_slippage_pct_of_spread(self, order_event: OrderEvent) -> float: + """ + Calculate slippage value as a percentage of the bid-ask spread. + The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + """ + spread = order_event.position.get("spread", None) + if spread is None: + raise ValueError("Spread information is required in the order_event position data to calculate slippage as a percentage of the spread.") + spread_slippage = spread * self.config.pct_alpha + if order_event.direction == 'BUY': + slippage = spread_slippage * order_event.quantity + elif order_event.direction == 'SELL': + slippage = -spread_slippage * order_event.quantity + else: + raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") + close = order_event.position['close'] + pct_spread_slippage = (spread_slippage / close) if close != 0 else 0 + print(f"Spread: {spread}, Spread Slippage: {spread_slippage}, Total Slippage: {slippage}, Pct Spread Slippage: {pct_spread_slippage}, Signal ID: {order_event.signal_id}, Direction: {order_event.direction}, Date: {order_event.datetime}") + return slippage + def calculate_slippage_value(self, order_event: OrderEvent) -> float: + """ + Calculate slippage value based on the specified slippage model in the config. + """ + if self.config.slippage_model == 'randomized': + return self.calculate_slippage_value_randomized(order_event) + elif self.config.slippage_model == 'fixed': + return self.calculate_slippage_value_fixed(order_event) + elif self.config.slippage_model == 'spread_pct': + return self.calculate_slippage_pct_of_spread(order_event) + else: + raise ValueError(f"Invalid slippage model: {self.config.slippage_model}. Must be 'randomized', 'fixed', or 'spread_pct'.") + def execute_order_randomized_slippage(self, order_event: OrderEvent): """ @@ -103,7 +173,11 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): elif order_event.direction == 'SELL': ## We want to decrease the price for sells by slippage slippage_pct = np.random.uniform(-self.max_slippage_pct, -self.min_slippage_pct) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct - + + slippage_pct_value = self.calculate_slippage_value(order_event) + slippage_per_contract = slippage_pct_value / order_event.quantity if order_event.quantity != 0 else 0 + slippage_pct = slippage_per_contract / order_event.position['close'] if order_event.position['close'] != 0 else 0 + #slippage may increase or decrease intended price price = order_event.position['close'] * (1 + slippage_pct) diff --git a/EventDriven/helpers.py b/EventDriven/helpers.py index 217d082..1ea9fc0 100644 --- a/EventDriven/helpers.py +++ b/EventDriven/helpers.py @@ -27,10 +27,14 @@ def normalize_dollar_amount(price: float | int) -> float | int: def parse_signal_id(id): + if "::" in id: + strategy_slug, id = id.split("::", 1) # Remove strategy slug if present + else: + strategy_slug = None if "SHORT" in id: - return dict(direction=id[-5:], date=pd.to_datetime(id[-13:-5]), ticker=id[:-13]) + return dict(direction=id[-5:], date=pd.to_datetime(id[-13:-5]), ticker=id[:-13], strategy_slug=strategy_slug) elif "LONG" in id: - return dict(direction=id[-4:], date=pd.to_datetime(id[-12:-4]), ticker=id[:-12]) + return dict(direction=id[-4:], date=pd.to_datetime(id[-12:-4]), ticker=id[:-12], strategy_slug=strategy_slug) else: raise ValueError(f"Invalid signal id `{id}`, neither LONG nor SHORT was found in the id") diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index 627b49d..fd4152c 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -12,7 +12,7 @@ from EventDriven.dataclasses.orders import OrderRequest from EventDriven.eventScheduler import EventScheduler from EventDriven.trade import Trade -from EventDriven.types import EventTypes, FillDirection, ResultsEnum, SignalTypes, OrderData, Order +from EventDriven.types import EventTypes, FillDirection, ResultsEnum, SignalTypes, OrderData, Order, TradeID from EventDriven.riskmanager.new_base import RiskManager, order_failed from EventDriven.riskmanager.utils import parse_position_id from trade.helpers.Logging import setup_logger @@ -34,7 +34,7 @@ from trade.backtester_.utils.utils import plot_portfolio from typing import Optional import plotly -from EventDriven.dataclasses.states import PositionState, PortfolioMetaInfo, PortfolioState, PositionAnalysisContext +from EventDriven.dataclasses.states import AtTimePositionData, PositionState, PortfolioMetaInfo, PortfolioState, PositionAnalysisContext from EventDriven.dataclasses.states import StrategyChangeMeta from EventDriven.configs.core import PortfolioManagerConfig, CashAllocatorConfig from EventDriven.portfolio_utils import extract_events @@ -311,7 +311,8 @@ def trades(self): return self.trades_df def aggregate_trades(self): - trades_data = [self.trades_map[trade_id].stats for trade_id in self.trades_map.keys()] + # trades_data = [self.trades_map[trade_id].stats for trade_id in self.trades_map.keys()] + trades_data = [trade.stats for trade in self.trades_map.values()] return pd.concat(trades_data, ignore_index=True) if trades_data else None @property @@ -353,11 +354,13 @@ def generate_order(self, signal_event: SignalEvent): symbol = signal_event.symbol signal_type = signal_event.signal_type order_type = "MKT" + order = None + if signal_type != "CLOSE": # generate order for LONG or SHORT order = self.create_order(signal_event, order_type) self.order_cache["OPEN"].setdefault(signal_event.datetime, {})[signal_event.symbol] = order - return order + elif signal_type == "CLOSE": ## Check if we have signal_id in current positions. If not, log warning and skip. if signal_event.signal_id not in self.current_positions[symbol]: @@ -382,10 +385,10 @@ def generate_order(self, signal_event: SignalEvent): ) return None - ## Prepare order details + ## Prepare order details. Will leave close price and spread to be handled at the end of this if statment position = current_position["position"] self.logger.info(f"Selling contract for {symbol} at {signal_event.datetime} Position: {current_position}") - position["close"] = self.calculate_close_on_position(position) + # position["close"] = self.calculate_close_on_position(position) ## Access skip from risk_manager market data skip = self.risk_manager.market_data.skip(position_id=position["trade_id"], date=signal_event.datetime) @@ -426,9 +429,20 @@ def generate_order(self, signal_event: SignalEvent): parent_event=signal_event, ) self.order_cache["CLOSE"].setdefault(signal_event.datetime, {})[signal_event.symbol] = order - return order - - return None + + ## Update order with at-time data for execution handler to use for slippage calculations and order generation logic. + if order is not None: + at_time_data = self.get_at_time_position_data(position_id=order.position["trade_id"], date=signal_event.datetime) + spread = at_time_data.get_spread() + close = at_time_data.get_price() + self.logger.info( + f"Generated order for {symbol} at {signal_event.datetime} with spread {spread} and position {order.position if order is not None else None}" + ) + order.position["spread"] = spread + order.position["close"] = close + + + return order def resolve_order_result(self, position_result, signal): """ @@ -454,11 +468,6 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type cash_at_hand = self.__normalize_dollar_amount_to_decimal(self.allocated_cash_map[signal_event.symbol] * 1) ## TODO: Decommision self.__max_contract_price and rely solely on cash allocator config for max contract price. - # max_contract_price = ( - # self.__max_contract_price[signal_event.symbol] - # if signal_event.max_contract_price is None - # else signal_event.max_contract_price - # ) ## Determine max contract price based on cash allocator config or default to 50% of allocated cash max_contract_dict = ( @@ -839,6 +848,15 @@ def __normalize_dollar_amount(self, price: float) -> float: """ return price * 100 + def _get_trade_object(self, trade_id: str, signal_id: str) -> Trade: + return self.trades_map.get(self._get_trade_key(trade_id, signal_id)) + + def _get_trade_key(self, trade_id: str, signal_id: str) -> tuple: + return (trade_id, signal_id) + + def _set_trade_object(self, trade_id: str, signal_id: str, trade: Trade): + self.trades_map[self._get_trade_key(trade_id, signal_id)] = trade + def update_positions_on_fill(self, fill_event: FillEvent): """ Takes a FilltEvent object and updates the current positions in the portfolio @@ -849,14 +867,15 @@ def update_positions_on_fill(self, fill_event: FillEvent): """ # Check whether the fill is a buy or sell new_position_data = {} - - if fill_event.position["trade_id"] not in self.trades_map: - self.trades_map[fill_event.position["trade_id"]] = Trade( - fill_event.position["trade_id"], fill_event.symbol, fill_event.signal_id - ) - self.trades_map[fill_event.position["trade_id"]].update(fill_event) + trade_map_key = self._get_trade_key(fill_event.position["trade_id"], fill_event.signal_id) # noqa + trade_id = fill_event.position["trade_id"] + symbol = fill_event.symbol + if trade_map_key not in self.trades_map: + trade_obj = Trade(trade_id, symbol, fill_event.signal_id) + self._set_trade_object(trade_id, fill_event.signal_id, trade_obj) + self._get_trade_object(trade_id, fill_event.signal_id).update(fill_event) else: - self.trades_map[fill_event.position["trade_id"]].update(fill_event) + self._get_trade_object(trade_id, fill_event.signal_id).update(fill_event) if fill_event.direction == "BUY": if fill_event.position is not None: @@ -980,14 +999,20 @@ def update_timeindex(self): current_cash[sym] = self.allocated_cash_map[sym] # update current cash for the symbol remove_signals = [] for signal_id in self.current_positions[sym]: - current_close = self.calculate_close_on_position(self.current_positions[sym][signal_id]["position"]) + trade_id = self.current_positions[sym][signal_id]["position"]["trade_id"] + at_time_position_data = self.get_at_time_position_data(position_id=trade_id, date=current_date) + spread = at_time_position_data.get_spread() + # current_close = self.calculate_close_on_position(self.current_positions[sym][signal_id]["position"]) + current_close = at_time_position_data.get_price() market_value = self.__normalize_dollar_amount( self.current_positions[sym][signal_id]["quantity"] * current_close ) + current_position = self.current_positions[sym][signal_id] + current_position["position"]["spread"] = spread + current_trade_id = current_position["position"]["trade_id"] + current_trade = self._get_trade_object(current_trade_id, signal_id) + current_trade.update_current_price(self.__normalize_dollar_amount(current_close)) ## Update current price on trade object for PnL calculations in risk manager analysis. This is important to have accurate and up to date price information on the trade object for the position analysis and risk management decisions. - self.trades_map[self.current_positions[sym][signal_id]["position"]["trade_id"]].update_current_price( - self.__normalize_dollar_amount(current_close) - ) # update current price on trade self.current_positions[sym][signal_id]["position"]["close"] = ( current_close ##Update close price for every iteration @@ -1034,15 +1059,29 @@ def update_fill(self, fill_event: FillEvent): if roll_event is not None: self.execute_roll_buy(roll_event) - def calculate_close_on_position(self, position) -> float: + def calculate_close_on_position(self, position, date=None) -> float: """ Calculate the close price on a position the close price is the difference between the long and short legs of the position """ return self.risk_manager.market_data.get_at_time_position_data( - position["trade_id"], self.eventScheduler.current_date + position["trade_id"], date or self.eventScheduler.current_date ).get_price() + + def calculate_spread_on_position(self, position_id:TradeID, date=None) -> float: + """ + Calculate the spread on a position + the spread is the difference between the bid and ask price of the position + """ + return self.risk_manager.market_data.get_at_time_position_data( + position_id=position_id, date=date or self.eventScheduler.current_date + ).get_spread() + def get_at_time_position_data(self, position_id:TradeID, date=None) -> AtTimePositionData: + """ + Get the position data at a given time + """ + return self.risk_manager.market_data.get_at_time_position_data(position_id=position_id, date=date or self.eventScheduler.current_date) # Getters def get_weighted_holdings(self) -> pd.DataFrame: """ diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 2d0c9d0..f8c6317 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -336,6 +336,12 @@ def get_timeseries(_id, direction): if direction == "L": long.append(data) elif direction == "S": + ask = data["Closeask"] + bid = data["Closebid"] + + ## Swap bid and ask for short positions to reflect the perspective of the position holder + data["Closeask"] = bid + data["Closebid"] = ask short.append(data) else: raise ValueError(f"Position Type {_set[0]} not recognized") diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index 34c85c1..ec4dd4b 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -194,6 +194,7 @@ ) from EventDriven._vars import get_use_temp_cache from trade.helpers.helper import CustomCache, is_USholiday +import numpy as np import pandas as pd from typing import List, Optional from datetime import datetime @@ -375,7 +376,9 @@ def _liquity_multiplier(self, pct_ratio: float, good: Optional[float] = None, ba return 0.25 else: x = (pct_ratio - good) / (bad - good) # Normalize to [0, 1] - return 1.0 - x * 0.75 # Scale to [1.0, 0.25] + decay = np.exp(-5 * x) # Exponential decay factor + multiplier = round(0.25 + 0.75 * decay, 4) # Scale to [0.25, 1.0] + return multiplier def _quantiy_liquidity_adjustment(self, quantity: int, pct_ratio: float) -> int: """ diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index 79a78f8..a554e72 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -221,6 +221,8 @@ - Config changes trigger sizer reload """ +import math + import numpy as np import pandas as pd from dataclasses import dataclass @@ -350,8 +352,61 @@ def on_new_position(self, new_pos_state: NewPositionState) -> NewPositionState: self._calculate_limits(new_pos_state) self._update_position_quantity(new_pos_state) self._create_position_metadata(new_pos_state) + self._on_new_position_failsafe(new_pos_state) # Ensure limits and quantity are set to reasonable values even if there are issues in the main logic return new_pos_state + def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPositionState: + """ + Failsafe method ensures the quantity size is never 0 as long as cash can buy at least 1 contract, and that limits are set to some default value, even + if there are errors in the main on_new_position logic. + """ + if new_pos_state.limits is not None and new_pos_state.order["data"]["quantity"] != 0: + return new_pos_state + request = new_pos_state.request + option_price = new_pos_state.at_time_data.get_price() + delta = new_pos_state.at_time_data.delta + tick_cash = request.tick_cash + chain_spot = new_pos_state.undl_at_time_data.chain_spot["close"] + order = new_pos_state.order + default_delta_lmt = default_delta_limit( + cash_available=tick_cash, + underlier_price_at_time=chain_spot, + sizing_lev=self.sizer_configs.sizing_lev, + ) + if new_pos_state.limits is None: + logger.warning(f"Limits were not set for trade_id {new_pos_state.order['data']['trade_id']}. Setting to default delta limit of {default_delta_lmt}.") + new_pos_state.limits = PositionLimits(delta=default_delta_lmt, dte=self.config.default_dte, moneyness=self.config.default_moneyness) + if new_pos_state.order["data"]["quantity"] == 0: + max_size_cash_can_buy = abs(math.floor(tick_cash / (option_price * 100))) + if max_size_cash_can_buy >= 1: + logger.warning(f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}. However, based on the available cash of {tick_cash} and option price of {option_price}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Setting quantity to 1.") + order_dict = new_pos_state.order.to_dict() + order_dict["data"]["quantity"] = 1 + new_pos_state.order = Order.from_dict(order_dict) + else: + logger.warning(f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}, and even a single contract cannot be afforded based on the available cash of {tick_cash} and option price of {option_price}. Quantity will remain at 0.") + + ## Update limits to set to delta + buffer to avoid repeated sizing issues if the issue was with limit calculation + logger.warning(f"Delta limit for trade_id {new_pos_state.order['data']['trade_id']} was calculated as {new_pos_state.limits.delta}, which is below the default delta per contract of {delta}. Setting delta limit to {delta * 1.075} to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation.") + new_pos_state.limits.delta = delta * 1.075 # Set delta limit to 7.5% above the delta of a single contract to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation + pos_lmts = PositionLimits( + delta=new_pos_state.limits.delta, + dte=self.config.default_dte, + moneyness=self.config.default_moneyness, + creation_date=request.date, + ) + self._save_position_limits(order["data"]["trade_id"], order["signal_id"], pos_lmts) + + ## Update metadata as well to reflect the new limits and quantity + metadata = self.position_metadata.get(new_pos_state.order["data"]["trade_id"]) + if metadata is not None: + metadata.delta_lmt = new_pos_state.limits.delta + metadata.new_quantity = new_pos_state.order["data"]["quantity"] + logger.warning(f"Updated metadata for trade_id {new_pos_state.order['data']['trade_id']} to reflect new delta limit of {metadata.delta_lmt} and new quantity of {metadata.new_quantity}.") + + + + return new_pos_state def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: """ Create and store metadata for the new position. @@ -502,7 +557,11 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction ## Update analysis_date action.analysis_date = last_updated - action.effective_date = last_updated + t_plus_n_timedelta + + ## Update effective date to be the next trading day after last_updated + t_plus_n + ## This is because analysis is based on EOD data at last_updated. Execution therefore has to start from the next trading day. + ## If t_plus_n is 0, then effective date will be the next trading day after last_updated, which is the expected behavior. + action.effective_date = last_updated + t_plus_n_timedelta + pd.Timedelta(days=1) # Add 1 day to ensure we are on the next trading day after last_updated + t_plus_n ## Only generate verbose_info for non-HOLD actions (Task #4 optimization) if action.action != "HOLD": diff --git a/EventDriven/riskmanager/position/cogs/mean_reversion.py b/EventDriven/riskmanager/position/cogs/mean_reversion.py index e634d46..8289706 100644 --- a/EventDriven/riskmanager/position/cogs/mean_reversion.py +++ b/EventDriven/riskmanager/position/cogs/mean_reversion.py @@ -65,6 +65,9 @@ def on_new_position(self, state: NewPositionState): delta=delta, delta_limit=limit, ) + logger.info( + f"Calculated delta limit: {limit}, resulting quantity: {q} lev: {self.config.sizing_lev}. Raw scaler: {scaler_raw}, applied scaler: {scaler}. Trade ID: {order['data']['trade_id']} with z-score {z_raw} and z-excess {z_excess}" + ) ## Update order quantity and log metadata order["data"]["quantity"] = q diff --git a/EventDriven/types.py b/EventDriven/types.py index 04a11b6..fa428a0 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -44,15 +44,20 @@ class SignalID(str): """Unique identifier for a trading signal. Format: - {TICKER}{YYYYMMDD}{SIGNAL_TYPE} + {TICKER}{YYYYMMDD}{SIGNAL_TYPE}(::_{STRATEGY_SLUG}) (optional strategy slug prefix) """ - __slots__ = ("ticker", "date", "direction") + __slots__ = ("ticker", "date", "direction", "strategy_slug") def __new__(cls, signal_id: str) -> "SignalID": return super().__new__(cls, signal_id) def __init__(self, signal_id: str) -> None: + if "::" in signal_id: + strategy_slug, signal_id = signal_id.split("::", 1) # Remove strategy slug if present + self.strategy_slug = strategy_slug + else: + self.strategy_slug = None parsed = parse_signal_id(signal_id) self.ticker = parsed["ticker"] self.date = parsed["date"] @@ -62,8 +67,10 @@ def parse(self) -> Dict[str, Any]: return parse_signal_id(self) @staticmethod - def generate(underlier: str, date: pd.Timestamp, signal_type: str) -> "SignalID": + def generate(underlier: str, date: pd.Timestamp, signal_type: str, strategy_slug: str = None) -> "SignalID": signal_id = generate_signal_id(underlier, date, signal_type) + if strategy_slug: + signal_id = strategy_slug + "::" + signal_id return SignalID(signal_id) def __repr__(self) -> str: From c6e0d2c31c5fbae249e42506463ffa19fddc583f Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:15:55 -0400 Subject: [PATCH 21/81] QuantTools: cache/perf and framework consistency updates --- EventDriven/riskmanager/utils.py | 55 ++++++++++++---------- trade/__init__.py | 7 ++- trade/backtester_/_multi_asset_strategy.py | 2 +- trade/backtester_/_strategy.py | 16 ++++++- trade/datamanager/market_data.py | 2 +- 5 files changed, 54 insertions(+), 28 deletions(-) diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 7a71105..e81f411 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -423,7 +423,7 @@ def get_cache(name: str) -> CustomCache: raise ValueError(f"Invalid cache name: {name}") -@dynamic_memoize +# @dynamic_memoize def populate_cache_with_chain( tick, date, @@ -434,30 +434,37 @@ def populate_cache_with_chain( """ Populate the cache with chain data. """ - chain = retrieve_chain_bulk(tick, "", date, date, "16:00", "C", print_url=False) - logger.info(f"Retrieved chain for {tick} on {date}") - - ## Retrieve OI - ## Info: We use the previous business day to get OI - ## This is because thetadata updates OI at the end of the day - ## Therefore to avoid lookahead bias, we use the previous business day - prev = change_to_last_busday((pd.to_datetime(date) - BDay(1))).strftime("%Y-%m-%d") - oi = retrieve_bulk_open_interest(symbol=tick, exp=0, start_date=prev, end_date=prev, print_url=False) - - ## Clip Chain - chain_clipped = chain.reset_index() # [['datetime', 'Root', 'Strike', 'Right', 'Expiration', 'Midpoint']] - chain_clipped = chain_clipped.merge( - oi[["Root", "Expiration", "Strike", "Right", "Open_interest"]], - on=["Root", "Expiration", "Strike", "Right"], - how="left", - ) - if PATCH_TICKERS: - chain_clipped["Root"] = chain_clipped["Root"].apply(swap_ticker) + key = (tick, pd.to_datetime(date, format="%Y-%m-%d").strftime("%Y-%m-%d")) + if key in get_persistent_cache(): + chain_clipped = get_persistent_cache()[key] + + else: + chain = retrieve_chain_bulk(tick, "", date, date, "16:00", "C", print_url=False) + logger.info(f"Retrieved chain for {tick} on {date}") + + ## Retrieve OI + ## Info: We use the previous business day to get OI + ## This is because thetadata updates OI at the end of the day + ## Therefore to avoid lookahead bias, we use the previous business day + prev = change_to_last_busday((pd.to_datetime(date) - BDay(1))).strftime("%Y-%m-%d") + oi = retrieve_bulk_open_interest(symbol=tick, exp=0, start_date=prev, end_date=prev, print_url=False) + + ## Clip Chain + chain_clipped = chain.reset_index() # [['datetime', 'Root', 'Strike', 'Right', 'Expiration', 'Midpoint']] + chain_clipped = chain_clipped.merge( + oi[["Root", "Expiration", "Strike", "Right", "Open_interest"]], + on=["Root", "Expiration", "Strike", "Right"], + how="left", + ) + if PATCH_TICKERS: + chain_clipped["Root"] = chain_clipped["Root"].apply(swap_ticker) + + ## Create ID + id_params = chain_clipped[["Root", "Right", "Expiration", "Strike"]].T.to_numpy() + ids = runThreads(generate_option_tick_new, id_params) + chain_clipped["opttick"] = ids + _PERSISTENT_CACHE[key] = chain_clipped ## Cache the chain data to avoid redundant API calls in the future - ## Create ID - id_params = chain_clipped[["Root", "Right", "Expiration", "Strike"]].T.to_numpy() - ids = runThreads(generate_option_tick_new, id_params) - chain_clipped["opttick"] = ids filter_opt = get_avoid_opticks(tick) chain_clipped = chain_clipped[~chain_clipped["opttick"].isin(filter_opt)] ## Optticks to avoid chain_clipped["chain_id"] = chain_clipped["opttick"] + "_" + chain_clipped["datetime"].astype(str) diff --git a/trade/__init__.py b/trade/__init__.py index fea4064..fd41121 100644 --- a/trade/__init__.py +++ b/trade/__init__.py @@ -41,7 +41,12 @@ 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_new_years = pd.date_range(start="2000-01-01", end="2040-01-01", freq="AS").strftime("%Y-%m-%d").to_list() -all_christmas = pd.date_range(start="2000-01-01", end="2040-01-01", freq="A-DEC").strftime("%Y-%m-%d").to_list() +all_christmas = list( + map( + lambda x: x.replace(day=25).strftime("%Y-%m-%d"), + pd.date_range(start="2000-01-01", end="2040-01-01", freq="A-DEC"), + ) +) holidays.update(all_new_years) holidays.update(all_christmas) HOLIDAY_SET = set(holidays) diff --git a/trade/backtester_/_multi_asset_strategy.py b/trade/backtester_/_multi_asset_strategy.py index 1833a27..1ed080c 100644 --- a/trade/backtester_/_multi_asset_strategy.py +++ b/trade/backtester_/_multi_asset_strategy.py @@ -52,7 +52,7 @@ class SimulationResults: def __repr__(self) -> str: """String representation showing summary statistics.""" tickers = list(self.trades.keys()) - total_trades = sum(len(t) for t in self.trades.values()) + total_trades = sum(len(t) for t in self.trades.values()) / 2 # Assuming each trade has an open and close action return f"SimulationResults(" f"tickers={tickers}, " f"total_trades={total_trades})" @dataclass diff --git a/trade/backtester_/_strategy.py b/trade/backtester_/_strategy.py index 2c43907..64d734c 100644 --- a/trade/backtester_/_strategy.py +++ b/trade/backtester_/_strategy.py @@ -60,7 +60,12 @@ def __post_init__(self): raise TypeError("TradeDecision.side must be an integer.") if self.side not in (1, -1, 0): raise ValueError("TradeDecision.side must be 1 (long), -1 (short), or 0 (no position).") - if (self.signal_id is not None and not isinstance(self.signal_id, SignalID)) and self.signal_id != "N/A": + if (self.signal_id is not None and not isinstance(self.signal_id, (SignalID, str))) and self.signal_id != "N/A": + if isinstance(self.signal_id, str) and self.signal_id != "N/A": + try: + self.signal_id = SignalID.parse(self.signal_id) + except Exception as e: + raise ValueError(f"Invalid signal_id string: {self.signal_id}") from e raise TypeError("TradeDecision.signal_id must be of type SignalID, 'N/A', or None. Received type: {}".format(type(self.signal_id))) if self.pos_effect is not None and not isinstance(self.pos_effect, PositionEffect): raise TypeError("TradeDecision.pos_effect must be of type PositionEffect or None.") @@ -234,6 +239,15 @@ def __init_subclass__(cls, **kwargs): if req not in params: raise TypeError(f"{cls.__name__}.__init__ must accept parameter '{req}'.") + # Enfore params are in bt_params + for p in params.values(): + if p.name in must_have_in_init + ["self", "kwargs", "args"]: + continue + if p.name not in cls.bt_params: + raise TypeError( + f"{cls.__name__}.__init__ has parameter '{p.name}' which is not listed in {cls.__name__}.bt_params." + ) + # If __init__ has **kwargs, accept anything; still enforce bt_params existence/type. # has_kwargs = any(p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()) # if has_kwargs: diff --git a/trade/datamanager/market_data.py b/trade/datamanager/market_data.py index 8ee745b..853a4bc 100644 --- a/trade/datamanager/market_data.py +++ b/trade/datamanager/market_data.py @@ -716,7 +716,7 @@ def _get_spot_timeseries(self, sym: str, start: str = None, end: str = None, *ar ) cached_data = pd.concat([cached_data, data]).sort_index() - return self._clip_to_date_range(cached_data, start, end) + return self._clip_to_date_range(cached_data, start, end).copy() 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. From 5255edde40dce56783bfcc57d6adc0e43f2173d7 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:16:00 -0400 Subject: [PATCH 22/81] QuantTools: remove obsolete EventDriven testing artifacts --- EventDriven/Testing/.DS_Store | Bin 6148 -> 0 bytes EventDriven/Testing/concurency_test.py | 191 ------------------------ EventDriven/Testing/cron_test.py | 4 - EventDriven/Testing/riskmanager_test.py | 87 ----------- EventDriven/Testing/test.py | 35 ----- 5 files changed, 317 deletions(-) delete mode 100644 EventDriven/Testing/.DS_Store delete mode 100644 EventDriven/Testing/concurency_test.py delete mode 100644 EventDriven/Testing/cron_test.py delete mode 100644 EventDriven/Testing/riskmanager_test.py delete mode 100644 EventDriven/Testing/test.py diff --git a/EventDriven/Testing/.DS_Store b/EventDriven/Testing/.DS_Store deleted file mode 100644 index 1a71cb0776ca1e4c312bf3287e5b925ef4942c2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~F$w}f3`G;&La^D=avBfd4F=H@>;+s9Y(zoSdXDZ-CJ2t!BJu;tpJXO1`-+{7 zi0JyZUy1Z0GJ~7S(n4d3ypz3*a+UEuTu#UH>42KmCvn!+@Lrnz*rt#G36KB@kN^q% z5COZlVY7KvMiL+a5_l4@??Zx{=Fn2rKOG1@0zf;I-LUpq0-CG<&7q|#Dlm=dL8DcD z46(YmLsOi~p`~hV7meXV4M3`}dgXhH(h?7~0-B+w9;*1Wg-e+&OK|2Hj6Nq_|Y zjDU8VVY9|d#ohY$dRE^>)z$?L_2URHKLJSWDqg_du%B!J&7q|#Dlq;CI0gn1_$q-1 DiFXpM diff --git a/EventDriven/Testing/concurency_test.py b/EventDriven/Testing/concurency_test.py deleted file mode 100644 index 5a5e195..0000000 --- a/EventDriven/Testing/concurency_test.py +++ /dev/null @@ -1,191 +0,0 @@ -""" -The purpose of this module is to test the performance of using async vs multiprocessing in the context of the riskmanager.py module by testing -a known bottleneck in the code, the bottleneck is running data retireval from an api using list of different arguments. - -To run the module appropriately, the following steps should be taken: -run the run_Processes() function, make sure the run_async() function is commented out. The data will be logged to data/ directory . -run the run_async() function, make sure the run_Processes() function is commented out. The data will be logged to data/ directory . - -at the end of both runs, you will be able to compare performance by looking at runProcesses.txt and run_async.txt respectively. -""" - - -import os -from trade.helpers.pools import runProcesses -from trade.helpers.threads import runThreads -import asyncio -import cProfile -import pstats -import io -from dbase.DataAPI.ThetaData import (retrieve_eod_ohlc, retrieve_eod_ohlc_async, retrieve_openInterest, retrieve_openInterest_async) -from trade.helpers.helper import (generate_option_tick_new) - -orderedList = [['GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL'], - ['2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24'], - ['2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21'], - ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], - ['2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26'], - [132.5, 130.0, 127.5, 125.0, 122.5, 120.0, 117.5, 115.0, 112.5, 110.0, 115.0, 112.5, 110.0, 107.5, 105.0, 102.5, 100.0, 98.0, 97.5, 96.0, 95.0, 94.0, 92.0]] - -tickOrderedList = [['GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL'], - ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], - ['2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21'], - [132.5, 130.0, 127.5, 125.0, 122.5, 120.0, 117.5, 115.0, 112.5, 110.0, 115.0, 112.5, 110.0, 107.5, 105.0, 102.5, 100.0, 98.0, 97.5, 96.0, 95.0, 94.0, 92.0]] - -def test_runProcesses(): - #simulate the retrieve_eod_ohlc action in riskmanager.py - eod_results = list(runProcesses(retrieve_eod_ohlc, orderedList, 'imap')) - oi_results = list(runProcesses(retrieve_openInterest, orderedList, 'imap')) - tick_results = list(runProcesses(generate_option_tick_new, tickOrderedList, 'imap')) - - if (tick_results and oi_results and eod_results): - return (tick_results, oi_results, eod_results) - - -def test_runThreads(): - #simulate the retrieve_eod_ohlc action in riskmanager.py - eod_results = runThreads(retrieve_eod_ohlc, orderedList, 'map') - oi_results = runThreads(retrieve_openInterest, orderedList, 'map') - tick_results = runThreads(generate_option_tick_new, tickOrderedList, 'map') - - if (tick_results and oi_results and eod_results): - return (tick_results, oi_results, eod_results) - -async def test_async(): - # Transpose orderedList to generate tuples of arguments - transposed_ordered_list = list(zip(*orderedList)) - transposed_tick_ordered_list = list(zip(*tickOrderedList)) - # Create tasks for each tuple - eod_tasks = [asyncio.create_task(retrieve_eod_ohlc_async(*args)) for args in transposed_ordered_list] - oi_tasks = [asyncio.create_task(retrieve_openInterest_async(*args)) for args in transposed_ordered_list] - tick_results = [generate_option_tick_new(*args) for args in transposed_tick_ordered_list] - - - # Run all tasks concurrently - eod_results, oi_results = await asyncio.gather( - asyncio.gather(*eod_tasks), - asyncio.gather(*oi_tasks) - ) - - print('async complete') - return (tick_results, oi_results, eod_results) - - - - -if __name__ == '__main__' : - profiler = cProfile.Profile() - - # test the runProcesses function, log the results to a file - def run_processes(): - try: - profiler.enable() - (tick_result, oi_result, eod_result) = test_runProcesses() - profiler.disable() - # Save profiling data to a file - ioStringStream = io.StringIO() - fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') - fstats.print_stats() - ioStringStream.seek(0) - data = ioStringStream.read() - with open(os.path.join(os.path.dirname(__file__), 'data', 'runProcesses.txt'), 'w') as stream: - print('writing to file') - stream.write(data) - stream.flush() - - - with open(os.path.join(os.path.dirname(__file__), 'data','runProcesses_eod_results.csv'), 'w') as f: - for df in eod_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','runProcesses_oi_results.csv'), 'w') as f: - for df in oi_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','tick_results.txt'), 'w') as f: - for tick in tick_result: - f.write(str(tick)) - f.write('\n\n') - - except Exception as e: - print('error occured: ', e) - - # run_processes() - # run_processes() - - # test the run_async function, log the results to a file - def run_async(): - try: - profiler.enable() - (tick_result, oi_result, eod_result) = asyncio.run(test_async()) - profiler.disable() - # Save profiling data to a file - ioStringStream = io.StringIO() - fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') - fstats.print_stats() - ioStringStream.seek(0) - data = ioStringStream.read() - with open(os.path.join(os.path.dirname(__file__),'data','run_async.txt'), 'w') as stream: - print('writing to file') - stream.write(data) - stream.flush() - - with open(os.path.join(os.path.dirname(__file__), 'data','run_async_eod_results.csv'), 'w') as f: - for df in eod_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','run_async_oi_results.csv'), 'w') as f: - for df in oi_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','run_async_tick_results.txt'), 'w') as f: - for tick in tick_result: - f.write(str(tick)) - f.write('\n\n') - - except Exception as e: - print('error occured: ', e) - - - # run_async() - - def run_threads(): - try: - profiler.enable() - (tick_result, oi_result, eod_result) = test_runThreads() - profiler.disable() - # Save profiling data to a file - ioStringStream = io.StringIO() - fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') - fstats.print_stats() - ioStringStream.seek(0) - data = ioStringStream.read() - with open(os.path.join(os.path.dirname(__file__), 'data', 'runThreads.txt'), 'w') as stream: - print('writing to file') - stream.write(data) - stream.flush() - - with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_eod_results.csv'), 'w') as f: - for df in eod_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_oi_results.csv'), 'w') as f: - for df in oi_result: - df.to_csv(f, index=False) - f.write('\n\n') # Add spaces between each dataframe - - with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_tick_results.txt'), 'w') as f: - for tick in tick_result: - f.write(str(tick)) - f.write('\n\n') - - except Exception as e: - print('error occured: ', e) - - run_threads() - run_async() \ No newline at end of file diff --git a/EventDriven/Testing/cron_test.py b/EventDriven/Testing/cron_test.py deleted file mode 100644 index 92cdeec..0000000 --- a/EventDriven/Testing/cron_test.py +++ /dev/null @@ -1,4 +0,0 @@ -from datetime import datetime - -with open('/Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/text.txt', 'a') as f: - f.write(f"Current time is {datetime.now()}\n") \ No newline at end of file diff --git a/EventDriven/Testing/riskmanager_test.py b/EventDriven/Testing/riskmanager_test.py deleted file mode 100644 index be3f042..0000000 --- a/EventDriven/Testing/riskmanager_test.py +++ /dev/null @@ -1,87 +0,0 @@ -from EventDriven.riskmanager import RiskManager -from dbase.DataAPI.ThetaData import list_contracts, retrieve_option_ohlc, is_theta_data_retrieval_successful #type: ignore -import datetime -import pandas as pd -import pandas_market_calendars as mcal -import unittest -import numpy as np - -tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'NFLX', 'AMD'] -date_range = pd.date_range() - -#generate date range -nyse = mcal.get_calendar('NYSE') -year_ago_date = datetime.datetime.now() - datetime.timedelta(days=365) -schedule = nyse.schedule(start_date=year_ago_date, end_date=datetime.datetime.now()) -date_range = mcal.date_range(schedule, frequency='1D') -dates = [date.strftime('%Y-%m-%d') for date in date_range] - - - - -class RiskManagerOperations(unittest.TestCase): - def set_up(self): - self.risk_manager = RiskManager() - - def test_order_picker(self): - ticker = np.random.choice(tickers) - contract_date = np.random.choice(dates) - contracts = list_contracts(ticker, pd.to_datetime(contract_date).strftime('%Y%m%d')) - self.assertTrue(is_theta_data_retrieval_successful(contracts)) - - contract = contracts.sample() - contract_right = contract['right'] - contract_expiration = pd.to_datetime(contract['expiration']).strftime('%Y%m%d') - contract_strike = float(contract['strike']) - max_close = np.random.randint(1, 10) - - #order settings - moneyness_width = np.random.uniform(0.01, 0.05) - rel_strike_long = np.random.uniform(1.05, 1.3) - rel_strike_short = np.random.uniform(0.7, 0.95) - dte = np.random.randint(30, 365) - - order_settings = { - 'type': 'spread', - 'specifics': [ - {'direction': 'long', 'rel_strike': rel_strike_long, 'dte': dte, 'moneyness_width': moneyness_width}, - {'direction': 'short', 'rel_strike': rel_strike_short, 'dte': dte, 'moneyness_width': moneyness_width} - ], - 'name': 'vertical_spread' - } - - try: - self.order = self.risk_manager.OrderPicker.get_order(ticker, contract_expiration, contract_right, max_close, order_settings) - self.assertIsInstance(self.order, dict) - self.assertIsInstance(self.order['long'], list) - self.assertIsInstance(self.order['short'], list) - self.assertGreater(len(self.order['long']), 0) - self.assertGreater(len(self.order['short']), 0) - self.assertIsInstance(self.order['close'], float) - except AssertionError as e: - print(f"AssertionError: {e}") - print(f"Ticker: {ticker}") - print(f"Contract Date: {contract_date}") - print(f"Contracts: {contracts}") - print(f"Contract: {contract}") - print(f"Contract Right: {contract_right}") - print(f"Contract Expiration: {contract_expiration}") - print(f"Contract Strike: {contract_strike}") - print(f"Max Close: {max_close}") - print(f"Order Settings: {order_settings}") - raise - except Exception as e: - print(f"Exception: {e}") - print(f"Ticker: {ticker}") - print(f"Contract Date: {contract_date}") - print(f"Contracts: {contracts}") - print(f"Contract: {contract}") - print(f"Contract Right: {contract_right}") - print(f"Contract Expiration: {contract_expiration}") - print(f"Contract Strike: {contract_strike}") - print(f"Max Close: {max_close}") - print(f"Order Settings: {order_settings}") - raise - -if __name__ == "__main__": - unittest.main() \ No newline at end of file diff --git a/EventDriven/Testing/test.py b/EventDriven/Testing/test.py deleted file mode 100644 index fb7f7e1..0000000 --- a/EventDriven/Testing/test.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -This is used to test the proxy server works to fetch thetadata -""" - -import http.client -import json -import os - -# print(os.environ['PROXY_URL']) -# {'end_date': 20250619, 'root': 'AAPL', 'use_csv': 'true', 'exp': 20241220, 'right': 'C', 'start_date': 20170101, 'strike': 220000, 'url': 'http://127.0.0.1:25510/v2/hist/option/eod?end_date=20250619&root=AAPL&use_csv=true&exp=20241220&right=C&start_date=20170101&strike=220000'} -conn = http.client.HTTPConnection("54.205.248.219", 5500) -payload = json.dumps({ - "method": "GET", - "url": 'http://127.0.0.1:25510/v2/hist/option/eod?end_date=20250619&root=AAPL&use_csv=true&exp=20241220&right=C&start_date=20240101&strike=220000' -}) -url_old = "http://127.0.0.1:25510/v2/hist/option/eod?exp=20231103&right=C&strike=170000&start_date=20231103&end_date=20231103&root=AAPL" -headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' -} -conn.request("POST", "/thetadata", payload, headers) -res = conn.getresponse() -data = res.read() -print(data.decode("utf-8")) - -print("\n\n\n") -retrieve_quote_payload = json.dumps({ - "method": "GET", - "url": "http://127.0.0.1:25510/v2/hist/option/quote?end_date=20230706&root=MSFT&use_csv=true&exp=20240621&ivl=1800000&right=C&start_date=20230706&strike=355000&start_time=34200000&rth=False&end_time=57600000" -}) -conn.request("POST", "/thetadata", retrieve_quote_payload, headers) -res = conn.getresponse() -data = res.read() -print(data.decode("utf-8")) -conn.close() \ No newline at end of file From 5e69a29f3c060f63ebf971c1c455678753a649b7 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:16:53 -0400 Subject: [PATCH 23/81] Misc changes --- EventDriven/tests/concurency_test.py | 191 +++ EventDriven/tests/cron_test.py | 4 + EventDriven/tests/data/runProcesses.txt | 284 ++++ .../tests/data/runProcesses_eod_results.csv | 989 ++++++++++++ .../tests/data/runProcesses_oi_results.csv | 529 +++++++ EventDriven/tests/data/runThreads.txt | 77 + .../tests/data/runThreads_eod_results.csv | 989 ++++++++++++ .../tests/data/runThreads_oi_results.csv | 529 +++++++ .../tests/data/runThreads_tick_results.txt | 46 + EventDriven/tests/data/run_async.txt | 1395 +++++++++++++++++ .../tests/data/run_async_eod_results.csv | 989 ++++++++++++ .../tests/data/run_async_oi_results.csv | 529 +++++++ .../tests/data/run_async_tick_results.txt | 46 + EventDriven/tests/data/tick_results.txt | 46 + EventDriven/tests/riskmanager_test.py | 87 + EventDriven/tests/test.py | 35 + EventDriven/tests/text.txt | 72 + 17 files changed, 6837 insertions(+) create mode 100644 EventDriven/tests/concurency_test.py create mode 100644 EventDriven/tests/cron_test.py create mode 100644 EventDriven/tests/data/runProcesses.txt create mode 100644 EventDriven/tests/data/runProcesses_eod_results.csv create mode 100644 EventDriven/tests/data/runProcesses_oi_results.csv create mode 100644 EventDriven/tests/data/runThreads.txt create mode 100644 EventDriven/tests/data/runThreads_eod_results.csv create mode 100644 EventDriven/tests/data/runThreads_oi_results.csv create mode 100644 EventDriven/tests/data/runThreads_tick_results.txt create mode 100644 EventDriven/tests/data/run_async.txt create mode 100644 EventDriven/tests/data/run_async_eod_results.csv create mode 100644 EventDriven/tests/data/run_async_oi_results.csv create mode 100644 EventDriven/tests/data/run_async_tick_results.txt create mode 100644 EventDriven/tests/data/tick_results.txt create mode 100644 EventDriven/tests/riskmanager_test.py create mode 100644 EventDriven/tests/test.py create mode 100644 EventDriven/tests/text.txt diff --git a/EventDriven/tests/concurency_test.py b/EventDriven/tests/concurency_test.py new file mode 100644 index 0000000..5a5e195 --- /dev/null +++ b/EventDriven/tests/concurency_test.py @@ -0,0 +1,191 @@ +""" +The purpose of this module is to test the performance of using async vs multiprocessing in the context of the riskmanager.py module by testing +a known bottleneck in the code, the bottleneck is running data retireval from an api using list of different arguments. + +To run the module appropriately, the following steps should be taken: +run the run_Processes() function, make sure the run_async() function is commented out. The data will be logged to data/ directory . +run the run_async() function, make sure the run_Processes() function is commented out. The data will be logged to data/ directory . + +at the end of both runs, you will be able to compare performance by looking at runProcesses.txt and run_async.txt respectively. +""" + + +import os +from trade.helpers.pools import runProcesses +from trade.helpers.threads import runThreads +import asyncio +import cProfile +import pstats +import io +from dbase.DataAPI.ThetaData import (retrieve_eod_ohlc, retrieve_eod_ohlc_async, retrieve_openInterest, retrieve_openInterest_async) +from trade.helpers.helper import (generate_option_tick_new) + +orderedList = [['GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL'], + ['2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24', '2023-07-24'], + ['2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21'], + ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], + ['2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26', '2023-06-26'], + [132.5, 130.0, 127.5, 125.0, 122.5, 120.0, 117.5, 115.0, 112.5, 110.0, 115.0, 112.5, 110.0, 107.5, 105.0, 102.5, 100.0, 98.0, 97.5, 96.0, 95.0, 94.0, 92.0]] + +tickOrderedList = [['GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL', 'GOOGL'], + ['P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P', 'P'], + ['2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21', '2024-06-21'], + [132.5, 130.0, 127.5, 125.0, 122.5, 120.0, 117.5, 115.0, 112.5, 110.0, 115.0, 112.5, 110.0, 107.5, 105.0, 102.5, 100.0, 98.0, 97.5, 96.0, 95.0, 94.0, 92.0]] + +def test_runProcesses(): + #simulate the retrieve_eod_ohlc action in riskmanager.py + eod_results = list(runProcesses(retrieve_eod_ohlc, orderedList, 'imap')) + oi_results = list(runProcesses(retrieve_openInterest, orderedList, 'imap')) + tick_results = list(runProcesses(generate_option_tick_new, tickOrderedList, 'imap')) + + if (tick_results and oi_results and eod_results): + return (tick_results, oi_results, eod_results) + + +def test_runThreads(): + #simulate the retrieve_eod_ohlc action in riskmanager.py + eod_results = runThreads(retrieve_eod_ohlc, orderedList, 'map') + oi_results = runThreads(retrieve_openInterest, orderedList, 'map') + tick_results = runThreads(generate_option_tick_new, tickOrderedList, 'map') + + if (tick_results and oi_results and eod_results): + return (tick_results, oi_results, eod_results) + +async def test_async(): + # Transpose orderedList to generate tuples of arguments + transposed_ordered_list = list(zip(*orderedList)) + transposed_tick_ordered_list = list(zip(*tickOrderedList)) + # Create tasks for each tuple + eod_tasks = [asyncio.create_task(retrieve_eod_ohlc_async(*args)) for args in transposed_ordered_list] + oi_tasks = [asyncio.create_task(retrieve_openInterest_async(*args)) for args in transposed_ordered_list] + tick_results = [generate_option_tick_new(*args) for args in transposed_tick_ordered_list] + + + # Run all tasks concurrently + eod_results, oi_results = await asyncio.gather( + asyncio.gather(*eod_tasks), + asyncio.gather(*oi_tasks) + ) + + print('async complete') + return (tick_results, oi_results, eod_results) + + + + +if __name__ == '__main__' : + profiler = cProfile.Profile() + + # test the runProcesses function, log the results to a file + def run_processes(): + try: + profiler.enable() + (tick_result, oi_result, eod_result) = test_runProcesses() + profiler.disable() + # Save profiling data to a file + ioStringStream = io.StringIO() + fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') + fstats.print_stats() + ioStringStream.seek(0) + data = ioStringStream.read() + with open(os.path.join(os.path.dirname(__file__), 'data', 'runProcesses.txt'), 'w') as stream: + print('writing to file') + stream.write(data) + stream.flush() + + + with open(os.path.join(os.path.dirname(__file__), 'data','runProcesses_eod_results.csv'), 'w') as f: + for df in eod_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','runProcesses_oi_results.csv'), 'w') as f: + for df in oi_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','tick_results.txt'), 'w') as f: + for tick in tick_result: + f.write(str(tick)) + f.write('\n\n') + + except Exception as e: + print('error occured: ', e) + + # run_processes() + # run_processes() + + # test the run_async function, log the results to a file + def run_async(): + try: + profiler.enable() + (tick_result, oi_result, eod_result) = asyncio.run(test_async()) + profiler.disable() + # Save profiling data to a file + ioStringStream = io.StringIO() + fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') + fstats.print_stats() + ioStringStream.seek(0) + data = ioStringStream.read() + with open(os.path.join(os.path.dirname(__file__),'data','run_async.txt'), 'w') as stream: + print('writing to file') + stream.write(data) + stream.flush() + + with open(os.path.join(os.path.dirname(__file__), 'data','run_async_eod_results.csv'), 'w') as f: + for df in eod_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','run_async_oi_results.csv'), 'w') as f: + for df in oi_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','run_async_tick_results.txt'), 'w') as f: + for tick in tick_result: + f.write(str(tick)) + f.write('\n\n') + + except Exception as e: + print('error occured: ', e) + + + # run_async() + + def run_threads(): + try: + profiler.enable() + (tick_result, oi_result, eod_result) = test_runThreads() + profiler.disable() + # Save profiling data to a file + ioStringStream = io.StringIO() + fstats = pstats.Stats(profiler, stream=ioStringStream).sort_stats('cumulative') + fstats.print_stats() + ioStringStream.seek(0) + data = ioStringStream.read() + with open(os.path.join(os.path.dirname(__file__), 'data', 'runThreads.txt'), 'w') as stream: + print('writing to file') + stream.write(data) + stream.flush() + + with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_eod_results.csv'), 'w') as f: + for df in eod_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_oi_results.csv'), 'w') as f: + for df in oi_result: + df.to_csv(f, index=False) + f.write('\n\n') # Add spaces between each dataframe + + with open(os.path.join(os.path.dirname(__file__), 'data','runThreads_tick_results.txt'), 'w') as f: + for tick in tick_result: + f.write(str(tick)) + f.write('\n\n') + + except Exception as e: + print('error occured: ', e) + + run_threads() + run_async() \ No newline at end of file diff --git a/EventDriven/tests/cron_test.py b/EventDriven/tests/cron_test.py new file mode 100644 index 0000000..92cdeec --- /dev/null +++ b/EventDriven/tests/cron_test.py @@ -0,0 +1,4 @@ +from datetime import datetime + +with open('/Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/text.txt', 'a') as f: + f.write(f"Current time is {datetime.now()}\n") \ No newline at end of file diff --git a/EventDriven/tests/data/runProcesses.txt b/EventDriven/tests/data/runProcesses.txt new file mode 100644 index 0000000..75c9d26 --- /dev/null +++ b/EventDriven/tests/data/runProcesses.txt @@ -0,0 +1,284 @@ + 6223 function calls in 10.050 seconds + + Ordered by: cumulative time + + ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.001 0.001 10.050 10.050 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:35(test_runProcesses) + 3 0.002 0.001 10.047 3.349 /Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/pools.py:13(runProcesses) + 6 0.000 0.000 9.666 1.611 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:659(join) + 3 0.000 0.000 9.651 3.217 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:237(join) + 60 9.596 0.160 9.596 0.160 {method 'acquire' of '_thread.lock' objects} + 27 0.000 0.000 9.591 0.355 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1087(join) + 27 0.000 0.000 9.591 0.355 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1125(_wait_for_tstate_lock) + 4 0.003 0.001 0.326 0.082 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:183(__init__) + 4 0.000 0.000 0.293 0.073 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:305(_repopulate_pool) + 4 0.008 0.002 0.292 0.073 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:314(_repopulate_pool_static) + 32 0.005 0.000 0.268 0.008 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:110(start) + 32 0.005 0.000 0.261 0.008 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:278(_Popen) + 3 0.003 0.001 0.259 0.086 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:211(restart) + 32 0.002 0.000 0.253 0.008 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:15(__init__) + 32 0.007 0.000 0.248 0.008 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:62(_launch) + 32 0.232 0.007 0.235 0.007 {built-in method posix.fork} + 3 0.001 0.000 0.095 0.032 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:88(__init__) + 6 0.000 0.000 0.094 0.016 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:128(_serve) + 192 0.003 0.000 0.074 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:24(poll) + 48 0.000 0.000 0.074 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:142(join) + 48 0.000 0.000 0.074 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:36(wait) + 144 0.071 0.000 0.071 0.000 {built-in method posix.waitpid} + 13 0.000 0.000 0.044 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:372(put) + 6 0.000 0.000 0.039 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:647(close) + 3 0.000 0.000 0.037 0.012 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:225(close) + 13 0.000 0.000 0.034 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:185(send_bytes) + 13 0.000 0.000 0.034 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:409(_send_bytes) + 13 0.000 0.000 0.033 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:384(_send) + 13 0.033 0.003 0.033 0.003 {built-in method posix.write} + 12 0.001 0.000 0.018 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:110(SimpleQueue) + 4 0.000 0.000 0.018 0.004 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:345(_setup_queues) + 32 0.001 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:179(Process) + 4 0.000 0.000 0.016 0.004 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:139(_clear) + 3 0.000 0.000 0.015 0.005 :1165(_find_and_load) + 3 0.000 0.000 0.015 0.005 :1120(_find_and_load_unlocked) + 32 0.011 0.000 0.015 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:80(__init__) + 3 0.000 0.000 0.014 0.005 :666(_load_unlocked) + 3 0.000 0.000 0.014 0.005 :934(exec_module) + 3 0.000 0.000 0.013 0.004 :1007(get_code) + 3 0.000 0.000 0.012 0.004 :1127(get_data) + 12 0.000 0.000 0.012 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:342(__init__) + 3 0.012 0.004 0.012 0.004 {built-in method io.open_code} + 24 0.000 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:65(Lock) + 13 0.001 0.000 0.009 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/reduction.py:51(dumps) + 12 0.000 0.000 0.008 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:945(start) + 27 0.000 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:205(__call__) + 3 0.000 0.000 0.007 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:680(_terminate_pool) + 12 0.000 0.000 0.006 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:611(wait) + 12 0.000 0.000 0.006 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:295(wait) + 13 0.000 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/_dill.py:418(dump) + 13 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:476(dump) + 36 0.005 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:186(__init__) + 13 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/_dill.py:367(save) + 12 0.002 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:856(__init__) + 13 0.000 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:535(save) + 24 0.000 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:174(__init__) + 24 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:56(__init__) + 55 0.003 0.000 0.003 0.000 {built-in method builtins.getattr} + 13 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/reduction.py:41(__init__) + 3 0.001 0.000 0.003 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/multiprocessing.py:156(imap) + 32 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:237(_releaseLock) + 32 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:433(_flush_std_streams) + 80 0.000 0.000 0.002 0.000 {method 'join' of 'str' objects} + 32 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:61(_cleanup) + 24 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:127(_make_name) + 119 0.000 0.000 0.002 0.000 {built-in method builtins.next} + 24 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/tempfile.py:154(__next__) + 64 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:94() + 45 0.001 0.000 0.001 0.000 {method 'copy' of 'dict' objects} + 12 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:562(__init__) + 12 0.001 0.000 0.001 0.000 {built-in method _thread.start_new_thread} + 44 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:85(add) + 76 0.001 0.000 0.001 0.000 {built-in method posix.pipe} + 3 0.000 0.000 0.001 0.000 :1054(_find_spec) + 13 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/_dill.py:351(__init__) + 3 0.000 0.000 0.001 0.000 :1499(find_spec) + 3 0.000 0.000 0.001 0.000 :1467(_get_spec) + 3 0.000 0.000 0.001 0.000 :1607(find_spec) + 15 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:243(__init__) + 32 0.001 0.000 0.001 0.000 {method 'release' of '_thread.RLock' objects} + 64 0.001 0.000 0.001 0.000 {method 'flush' of '_io.TextIOWrapper' objects} + 72 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:853(next) + 78 0.001 0.000 0.001 0.000 :405(parent) + 9 0.000 0.000 0.001 0.000 :140(_path_stat) + 9 0.001 0.000 0.001 0.000 {built-in method posix.stat} + 3 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:467(__enter__) + 130 0.001 0.000 0.001 0.000 {built-in method posix.close} + 3 0.000 0.000 0.001 0.000 :159(_path_isfile) + 3 0.000 0.000 0.001 0.000 :150(_path_is_mode_type) + 24 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:171(register_after_fork) + 3 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:396(imap) + 13 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:409(__init__) + 6 0.000 0.000 0.001 0.000 :233(_call_with_frames_removed) + 3 0.000 0.000 0.001 0.000 {built-in method builtins.exec} + 32 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:228(_acquireLock) + 64 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:189(name) + 3 0.000 0.000 0.000 0.000 :727(_compile_bytecode) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/random.py:480(choices) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:164(__setitem__) + 3 0.000 0.000 0.000 0.000 {built-in method marshal.loads} + 76 0.000 0.000 0.000 0.000 {method 'add' of 'set' objects} + 33 0.000 0.000 0.000 0.000 {built-in method _thread.allocate_lock} + 13 0.000 0.000 0.000 0.000 {method 'update' of 'dict' objects} + 12 0.000 0.000 0.000 0.000 {built-in method builtins.__build_class__} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:839(__init__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:1() + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:103(__exit__) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:811(_newname) + 26 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:217(commit_frame) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:535(Pipe) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:212(end_framing) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1324(_make_invoke_excepthook) + 13 0.000 0.000 0.000 0.000 {method '__exit__' of '_multiprocessing.SemLock' objects} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:463(close_fds) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/logger.py:127(trace_setup) + 32 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.RLock' objects} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/random.py:493() + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:347(__new__) + 48 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1453(current_thread) + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:105(remove) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:279(_get_sentinels) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:100(__enter__) + 3 0.000 0.000 0.000 0.000 :566(module_from_spec) + 84 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:271(__enter__) + 223 0.000 0.000 0.000 0.000 {built-in method posix.getpid} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/tempfile.py:143(rng) + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:134(__del__) + 3 0.000 0.000 0.000 0.000 :493(_init_module_attrs) + 32 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:193(name) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:153(is_alive) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:1() + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:41(cpu_count) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/helpers/mp_helper.py:13(starargs) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1206(daemon) + 3 0.000 0.000 0.000 0.000 {method 'read' of '_io.BufferedReader' objects} + 33 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:39(_remove) + 3 0.000 0.000 0.000 0.000 {method '__exit__' of '_io._IOBase' objects} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:671(_help_stuff_finish) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1051(_stop) + 6 0.000 0.000 0.000 0.000 :437(cache_from_source) + 32 0.000 0.000 0.000 0.000 {method 'replace' of 'str' objects} + 32 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:205(daemon) + 6 0.000 0.000 0.000 0.000 :392(cached) + 13 0.000 0.000 0.000 0.000 {method '__enter__' of '_multiprocessing.SemLock' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:48(debug) + 84 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:274(__exit__) + 3 0.000 0.000 0.000 0.000 :567(_get_cached) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:224(exitcode) + 26 0.000 0.000 0.000 0.000 {built-in method _struct.pack} + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1221(daemon) + 105 0.000 0.000 0.000 0.000 {method 'discard' of 'set' objects} + 35 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} + 39 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:241(write) + 93 0.000 0.000 0.000 0.000 {method 'rpartition' of 'str' objects} + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:379(_close) + 3 0.000 0.000 0.000 0.000 :169(__enter__) + 18 0.000 0.000 0.000 0.000 :126(_path_join) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:352(__init__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/random.py:119(__init__) + 128 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:99(_check_closed) + 3 0.000 0.000 0.000 0.000 {method 'put' of '_queue.SimpleQueue' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/random.py:128(seed) + 6 0.000 0.000 0.000 0.000 {built-in method posix.cpu_count} + 88 0.000 0.000 0.000 0.000 {built-in method builtins.len} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:1() + 3 0.000 0.000 0.000 0.000 :1602(_get_spec) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:205(__init__) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:121(__init__) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:740(save_none) + 18 0.000 0.000 0.000 0.000 {built-in method _weakref._remove_dead_weakref} + 55 0.000 0.000 0.000 0.000 {built-in method builtins.isinstance} + 1 0.000 0.000 0.000 0.000 {function Random.seed at 0x10b9e1800} + 73 0.000 0.000 0.000 0.000 {method 'get' of 'dict' objects} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:441(__init__) + 3 0.000 0.000 0.000 0.000 :179(_get_module_lock) + 6 0.000 0.000 0.000 0.000 :132(_path_split) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/_dill.py:2180(is_dill) + 3 0.000 0.000 0.000 0.000 :1146(path_stats) + 84 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.lock' objects} + 192 0.000 0.000 0.000 0.000 {built-in method math.floor} + 3 0.000 0.000 0.000 0.000 :642(_classify_pyc) + 18 0.000 0.000 0.000 0.000 :128() + 36 0.000 0.000 0.000 0.000 {built-in method builtins.hasattr} + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:286(_is_owned) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:96(_make_methods) + 3 0.000 0.000 0.000 0.000 :778(spec_from_file_location) + 54 0.000 0.000 0.000 0.000 {built-in method _thread.get_ident} + 52 0.000 0.000 0.000 0.000 {method 'write' of '_io.BytesIO' objects} + 192 0.000 0.000 0.000 0.000 {method 'random' of '_random.Random' objects} + 3 0.000 0.000 0.000 0.000 :173(__exit__) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:283(_acquire_restore) + 26 0.000 0.000 0.000 0.000 {method 'getbuffer' of '_io.BytesIO' objects} + 12 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.RLock' objects} + 51 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:575(is_set) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:146(_check_writable) + 7 0.000 0.000 0.000 0.000 :1207(_handle_fromlist) + 3 0.000 0.000 0.000 0.000 :125(release) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:209(start_framing) + 12 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:280(_release_save) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pathos/abstract_launcher.py:144(__imap) + 90 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.lock' objects} + 6 0.000 0.000 0.000 0.000 {built-in method builtins.max} + 3 0.000 0.000 0.000 0.000 {method 'acquire' of '_multiprocessing.SemLock' objects} + 32 0.000 0.000 0.000 0.000 {built-in method posix.waitstatus_to_exitcode} + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dill/_dill.py:316(get) + 37 0.000 0.000 0.000 0.000 {built-in method builtins.id} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:488(__exit__) + 9 0.000 0.000 0.000 0.000 :84(_unpack_uint32) + 24 0.000 0.000 0.000 0.000 {built-in method __new__ of type object at 0x10b4b9898} + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:157(__init__) + 21 0.000 0.000 0.000 0.000 :244(_verbose_message) + 51 0.000 0.000 0.000 0.000 {method 'pop' of 'dict' objects} + 72 0.000 0.000 0.000 0.000 {method 'popleft' of 'collections.deque' objects} + 3 0.000 0.000 0.000 0.000 :71(__init__) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:237(get_context) + 3 0.000 0.000 0.000 0.000 :100(acquire) + 3 0.000 0.000 0.000 0.000 :675(_validate_timestamp_pyc) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:38(Queue) + 27 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/util.py:44(sub_debug) + 12 0.000 0.000 0.000 0.000 :134() + 24 0.000 0.000 0.000 0.000 {method 'release' of '_thread.lock' objects} + 3 0.000 0.000 0.000 0.000 :48(_new_module) + 36 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:187(get_context) + 3 0.000 0.000 0.000 0.000 :198(cb) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/popen_fork.py:12(Popen) + 3 0.000 0.000 0.000 0.000 :920(find_spec) + 39 0.000 0.000 0.000 0.000 {method 'tell' of '_io.BytesIO' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:376(Barrier) + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/pickle.py:605(persistent_id) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/process.py:37(current_process) + 12 0.000 0.000 0.000 0.000 :1030(__exit__) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/context.py:197(get_start_method) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:334(Event) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:340(SimpleQueue) + 42 0.000 0.000 0.000 0.000 {method 'rstrip' of 'str' objects} + 13 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/connection.py:138(_check_closed) + 3 0.000 0.000 0.000 0.000 :748(find_spec) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:52(SemLock) + 3 0.000 0.000 0.000 0.000 {built-in method builtins.setattr} + 12 0.000 0.000 0.000 0.000 :1026(__enter__) + 3 0.000 0.000 0.000 0.000 :599(_check_name_wrapper) + 3 0.000 0.000 0.000 0.000 :67(_relax_case) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/_distutils_hack/__init__.py:102(find_spec) + 3 0.000 0.000 0.000 0.000 {method 'pop' of 'list' objects} + 18 0.000 0.000 0.000 0.000 {built-in method _imp.acquire_lock} + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1192(is_alive) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:351(_check_running) + 12 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects} + 6 0.000 0.000 0.000 0.000 {method 'startswith' of 'str' objects} + 3 0.000 0.000 0.000 0.000 :180(_path_isabs) + 3 0.000 0.000 0.000 0.000 :357(__init__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:266(__del__) + 3 0.000 0.000 0.000 0.000 :1424(_path_importer_cache) + 18 0.000 0.000 0.000 0.000 {built-in method _imp.release_lock} + 6 0.000 0.000 0.000 0.000 {method 'rfind' of 'str' objects} + 6 0.000 0.000 0.000 0.000 {built-in method _warnings._filters_mutated} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:136(Semaphore) + 3 0.000 0.000 0.000 0.000 :1097(__init__) + 3 0.000 0.000 0.000 0.000 {built-in method _imp.is_builtin} + 9 0.000 0.000 0.000 0.000 {built-in method from_bytes} + 12 0.000 0.000 0.000 0.000 {method 'locked' of '_thread.lock' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:223(Condition) + 9 0.000 0.000 0.000 0.000 {built-in method posix.fspath} + 3 0.000 0.000 0.000 0.000 {built-in method _imp.find_frozen} + 3 0.000 0.000 0.000 0.000 :165(__init__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/pool.py:850(__iter__) + 3 0.000 0.000 0.000 0.000 {method 'endswith' of 'str' objects} + 3 0.000 0.000 0.000 0.000 {built-in method _imp._fix_co_filename} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/queues.py:297(JoinableQueue) + 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} + 2 0.000 0.000 0.000 0.000 {method 'setter' of 'property' objects} + 3 0.000 0.000 0.000 0.000 :931(create_module) + 3 0.000 0.000 0.000 0.000 :413(has_location) + 3 0.000 0.000 0.000 0.000 :1122(get_filename) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:155(BoundedSemaphore) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:172(Lock) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/multiprocess/synchronize.py:197(RLock) + + diff --git a/EventDriven/tests/data/runProcesses_eod_results.csv b/EventDriven/tests/data/runProcesses_eod_results.csv new file mode 100644 index 0000000..7003273 --- /dev/null +++ b/EventDriven/tests/data/runProcesses_eod_results.csv @@ -0,0 +1,989 @@ +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 + + diff --git a/EventDriven/tests/data/runProcesses_oi_results.csv b/EventDriven/tests/data/runProcesses_oi_results.csv new file mode 100644 index 0000000..3f423d0 --- /dev/null +++ b/EventDriven/tests/data/runProcesses_oi_results.csv @@ -0,0 +1,529 @@ +Open_interest,Date,time,Datetime +1399,20230626,06:30:00,2023-06-26 +1399,20230627,06:30:00,2023-06-27 +1399,20230628,06:30:01,2023-06-28 +1399,20230629,06:30:00,2023-06-29 +1399,20230630,06:30:00,2023-06-30 +1399,20230703,06:30:01,2023-07-03 +1399,20230705,06:30:00,2023-07-05 +1399,20230706,06:30:01,2023-07-06 +1399,20230707,06:30:00,2023-07-07 +1399,20230710,06:30:01,2023-07-10 +1399,20230711,06:30:01,2023-07-11 +1399,20230712,06:30:01,2023-07-12 +1399,20230713,06:30:01,2023-07-13 +1404,20230714,06:30:00,2023-07-14 +1406,20230717,06:30:00,2023-07-17 +1412,20230718,06:30:00,2023-07-18 +1356,20230719,06:30:00,2023-07-19 +1351,20230720,06:30:00,2023-07-20 +1352,20230721,06:30:00,2023-07-21 +1352,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1912,20230626,06:30:00,2023-06-26 +1912,20230627,06:30:00,2023-06-27 +1910,20230628,06:30:01,2023-06-28 +1910,20230629,06:30:00,2023-06-29 +1910,20230630,06:30:01,2023-06-30 +1910,20230703,06:30:00,2023-07-03 +1909,20230705,06:30:01,2023-07-05 +1909,20230706,06:30:00,2023-07-06 +1909,20230707,06:30:01,2023-07-07 +1909,20230710,06:30:00,2023-07-10 +1909,20230711,06:30:00,2023-07-11 +1909,20230712,06:30:00,2023-07-12 +1909,20230713,06:30:01,2023-07-13 +1908,20230714,06:30:00,2023-07-14 +1908,20230717,06:30:01,2023-07-17 +1908,20230718,06:30:00,2023-07-18 +1900,20230719,06:30:01,2023-07-19 +1900,20230720,06:30:01,2023-07-20 +1900,20230721,06:30:00,2023-07-21 +1901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +818,20230626,06:30:00,2023-06-26 +818,20230627,06:30:00,2023-06-27 +818,20230628,06:30:01,2023-06-28 +818,20230629,06:30:00,2023-06-29 +818,20230630,06:30:01,2023-06-30 +820,20230703,06:30:00,2023-07-03 +826,20230705,06:30:01,2023-07-05 +826,20230706,06:30:00,2023-07-06 +826,20230707,06:30:01,2023-07-07 +826,20230710,06:30:00,2023-07-10 +826,20230711,06:30:00,2023-07-11 +826,20230712,06:30:00,2023-07-12 +826,20230713,06:30:01,2023-07-13 +825,20230714,06:30:00,2023-07-14 +826,20230717,06:30:01,2023-07-17 +823,20230718,06:30:00,2023-07-18 +814,20230719,06:30:01,2023-07-19 +813,20230720,06:30:01,2023-07-20 +1004,20230721,06:30:00,2023-07-21 +1006,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1622,20230626,06:30:00,2023-06-26 +1622,20230627,06:30:00,2023-06-27 +1622,20230628,06:30:01,2023-06-28 +1622,20230629,06:30:00,2023-06-29 +1616,20230630,06:30:01,2023-06-30 +1616,20230703,06:30:00,2023-07-03 +1616,20230705,06:30:01,2023-07-05 +1616,20230706,06:30:00,2023-07-06 +1616,20230707,06:30:01,2023-07-07 +1624,20230710,06:30:00,2023-07-10 +1625,20230711,06:30:00,2023-07-11 +1625,20230712,06:30:00,2023-07-12 +1625,20230713,06:30:01,2023-07-13 +1625,20230714,06:30:00,2023-07-14 +1650,20230717,06:30:01,2023-07-17 +1876,20230718,06:30:00,2023-07-18 +1863,20230719,06:30:01,2023-07-19 +1864,20230720,06:30:01,2023-07-20 +1859,20230721,06:30:00,2023-07-21 +1861,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1709,20230626,06:30:00,2023-06-26 +1709,20230627,06:30:00,2023-06-27 +1709,20230628,06:30:01,2023-06-28 +1709,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1709,20230705,06:30:01,2023-07-05 +1709,20230706,06:30:00,2023-07-06 +1710,20230707,06:30:01,2023-07-07 +1710,20230710,06:30:00,2023-07-10 +2025,20230711,06:30:00,2023-07-11 +2027,20230712,06:30:00,2023-07-12 +2027,20230713,06:30:01,2023-07-13 +2027,20230714,06:30:00,2023-07-14 +2020,20230717,06:30:01,2023-07-17 +2030,20230718,06:30:00,2023-07-18 +1993,20230719,06:30:01,2023-07-19 +1993,20230720,06:30:01,2023-07-20 +2000,20230721,06:30:00,2023-07-21 +2000,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +4405,20230626,06:30:00,2023-06-26 +4462,20230627,06:30:00,2023-06-27 +4462,20230628,06:30:01,2023-06-28 +4463,20230629,06:30:00,2023-06-29 +4464,20230630,06:30:01,2023-06-30 +4464,20230703,06:30:00,2023-07-03 +4464,20230705,06:30:01,2023-07-05 +5124,20230706,06:30:00,2023-07-06 +7080,20230707,06:30:01,2023-07-07 +7080,20230710,06:30:00,2023-07-10 +7081,20230711,06:30:00,2023-07-11 +7081,20230712,06:30:00,2023-07-12 +7081,20230713,06:30:01,2023-07-13 +7082,20230714,06:30:00,2023-07-14 +7102,20230717,06:30:01,2023-07-17 +7107,20230718,06:30:00,2023-07-18 +7133,20230719,06:30:01,2023-07-19 +7140,20230720,06:30:01,2023-07-20 +7141,20230721,06:30:00,2023-07-21 +7152,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +998,20230626,06:30:00,2023-06-26 +1007,20230627,06:30:00,2023-06-27 +1007,20230628,06:30:01,2023-06-28 +1007,20230629,06:30:00,2023-06-29 +1007,20230630,06:30:01,2023-06-30 +1009,20230703,06:30:00,2023-07-03 +1009,20230705,06:30:01,2023-07-05 +1009,20230706,06:30:00,2023-07-06 +1009,20230707,06:30:01,2023-07-07 +1009,20230710,06:30:00,2023-07-10 +1009,20230711,06:30:00,2023-07-11 +1009,20230712,06:30:00,2023-07-12 +1009,20230713,06:30:01,2023-07-13 +1009,20230714,06:30:00,2023-07-14 +1002,20230717,06:30:01,2023-07-17 +1002,20230718,06:30:00,2023-07-18 +966,20230719,06:30:01,2023-07-19 +966,20230720,06:30:01,2023-07-20 +968,20230721,06:30:00,2023-07-21 +968,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1460,20230626,06:30:00,2023-06-26 +1524,20230627,06:30:00,2023-06-27 +1724,20230628,06:30:01,2023-06-28 +1724,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1714,20230705,06:30:01,2023-07-05 +1716,20230706,06:30:00,2023-07-06 +1718,20230707,06:30:01,2023-07-07 +1702,20230710,06:30:00,2023-07-10 +1720,20230711,06:30:00,2023-07-11 +1720,20230712,06:30:00,2023-07-12 +1721,20230713,06:30:01,2023-07-13 +1721,20230714,06:30:00,2023-07-14 +1731,20230717,06:30:01,2023-07-17 +1738,20230718,06:30:00,2023-07-18 +1942,20230719,06:30:01,2023-07-19 +1956,20230720,06:30:01,2023-07-20 +1961,20230721,06:30:00,2023-07-21 +1961,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +857,20230626,06:30:00,2023-06-26 +949,20230627,06:30:00,2023-06-27 +1151,20230628,06:30:01,2023-06-28 +1155,20230629,06:30:00,2023-06-29 +1160,20230630,06:30:00,2023-06-30 +1162,20230703,06:30:01,2023-07-03 +1162,20230705,06:30:00,2023-07-05 +1439,20230706,06:30:01,2023-07-06 +1439,20230707,06:30:00,2023-07-07 +1440,20230710,06:30:01,2023-07-10 +1455,20230711,06:30:01,2023-07-11 +1459,20230712,06:30:01,2023-07-12 +1463,20230713,06:30:01,2023-07-13 +1466,20230714,06:30:00,2023-07-14 +1466,20230717,06:30:00,2023-07-17 +1466,20230718,06:30:00,2023-07-18 +1501,20230719,06:30:00,2023-07-19 +1524,20230720,06:30:00,2023-07-20 +1605,20230721,06:30:01,2023-07-21 +1636,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +976,20230626,06:30:01,2023-06-26 +1052,20230627,06:30:01,2023-06-27 +1054,20230628,06:30:01,2023-06-28 +1054,20230629,06:30:01,2023-06-29 +1143,20230630,06:30:00,2023-06-30 +1149,20230703,06:30:01,2023-07-03 +1149,20230705,06:30:01,2023-07-05 +1149,20230706,06:30:00,2023-07-06 +1149,20230707,06:30:01,2023-07-07 +1149,20230710,06:30:00,2023-07-10 +1220,20230711,06:30:01,2023-07-11 +1265,20230712,06:30:01,2023-07-12 +1264,20230713,06:30:01,2023-07-13 +1264,20230714,06:30:01,2023-07-14 +1293,20230717,06:30:01,2023-07-17 +1296,20230718,06:30:01,2023-07-18 +1374,20230719,06:30:01,2023-07-19 +1452,20230720,06:30:01,2023-07-20 +1455,20230721,06:30:01,2023-07-21 +1455,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +8245,20230626,06:30:00,2023-06-26 +8245,20230627,06:30:00,2023-06-27 +8941,20230628,06:30:01,2023-06-28 +10141,20230629,06:30:00,2023-06-29 +10141,20230630,06:30:00,2023-06-30 +10146,20230703,06:30:01,2023-07-03 +10148,20230705,06:30:00,2023-07-05 +10148,20230706,06:30:01,2023-07-06 +13459,20230707,06:30:00,2023-07-07 +13459,20230710,06:30:01,2023-07-10 +13477,20230711,06:30:01,2023-07-11 +14711,20230712,06:30:01,2023-07-12 +14727,20230713,06:30:01,2023-07-13 +14726,20230714,06:30:00,2023-07-14 +14726,20230717,06:30:00,2023-07-17 +14723,20230718,06:30:00,2023-07-18 +14738,20230719,06:30:00,2023-07-19 +14751,20230720,06:30:00,2023-07-20 +15159,20230721,06:30:00,2023-07-21 +15160,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +826,20230626,06:30:00,2023-06-26 +811,20230627,06:30:00,2023-06-27 +811,20230628,06:30:01,2023-06-28 +811,20230629,06:30:00,2023-06-29 +811,20230630,06:30:01,2023-06-30 +811,20230703,06:30:00,2023-07-03 +811,20230705,06:30:01,2023-07-05 +811,20230706,06:30:00,2023-07-06 +811,20230707,06:30:01,2023-07-07 +811,20230710,06:30:00,2023-07-10 +812,20230711,06:30:00,2023-07-11 +812,20230712,06:30:00,2023-07-12 +810,20230713,06:30:01,2023-07-13 +810,20230714,06:30:00,2023-07-14 +807,20230717,06:30:01,2023-07-17 +807,20230718,06:30:00,2023-07-18 +831,20230719,06:30:01,2023-07-19 +831,20230720,06:30:01,2023-07-20 +830,20230721,06:30:00,2023-07-21 +830,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1394,20230626,06:30:01,2023-06-26 +1404,20230627,06:30:00,2023-06-27 +1404,20230628,06:30:00,2023-06-28 +1404,20230629,06:30:01,2023-06-29 +1404,20230630,06:30:01,2023-06-30 +1404,20230703,06:30:01,2023-07-03 +1404,20230705,06:30:01,2023-07-05 +1404,20230706,06:30:00,2023-07-06 +1404,20230707,06:30:01,2023-07-07 +1404,20230710,06:30:01,2023-07-10 +1409,20230711,06:30:00,2023-07-11 +1417,20230712,06:30:00,2023-07-12 +1417,20230713,06:30:00,2023-07-13 +1416,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1415,20230719,06:30:00,2023-07-19 +1415,20230720,06:30:00,2023-07-20 +1417,20230721,06:30:00,2023-07-21 +1417,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1413,20230626,06:30:01,2023-06-26 +1413,20230627,06:30:00,2023-06-27 +1413,20230628,06:30:00,2023-06-28 +1413,20230629,06:30:01,2023-06-29 +1413,20230630,06:30:01,2023-06-30 +1413,20230703,06:30:01,2023-07-03 +1413,20230705,06:30:01,2023-07-05 +1424,20230706,06:30:00,2023-07-06 +1424,20230707,06:30:01,2023-07-07 +1426,20230710,06:30:01,2023-07-10 +1429,20230711,06:30:00,2023-07-11 +1426,20230712,06:30:00,2023-07-12 +1426,20230713,06:30:00,2023-07-13 +1423,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1395,20230719,06:30:00,2023-07-19 +1395,20230720,06:30:00,2023-07-20 +1400,20230721,06:30:00,2023-07-21 +1400,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +7477,20230626,06:30:01,2023-06-26 +7482,20230627,06:30:01,2023-06-27 +7482,20230628,06:30:01,2023-06-28 +7503,20230629,06:30:01,2023-06-29 +7503,20230630,06:30:00,2023-06-30 +8562,20230703,06:30:01,2023-07-03 +8562,20230705,06:30:01,2023-07-05 +8562,20230706,06:30:00,2023-07-06 +8562,20230707,06:30:01,2023-07-07 +8561,20230710,06:30:00,2023-07-10 +8947,20230711,06:30:01,2023-07-11 +8947,20230712,06:30:01,2023-07-12 +8947,20230713,06:30:01,2023-07-13 +8947,20230714,06:30:01,2023-07-14 +8914,20230717,06:30:01,2023-07-17 +8913,20230718,06:30:01,2023-07-18 +8890,20230719,06:30:01,2023-07-19 +8891,20230720,06:30:01,2023-07-20 +8904,20230721,06:30:01,2023-07-21 +8901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1361,20230626,06:30:01,2023-06-26 +1361,20230627,06:30:01,2023-06-27 +1361,20230628,06:30:01,2023-06-28 +1361,20230629,06:30:01,2023-06-29 +1361,20230630,06:30:00,2023-06-30 +1374,20230703,06:30:01,2023-07-03 +1374,20230705,06:30:01,2023-07-05 +1374,20230706,06:30:00,2023-07-06 +1374,20230707,06:30:01,2023-07-07 +1374,20230710,06:30:00,2023-07-10 +1361,20230711,06:30:01,2023-07-11 +1374,20230712,06:30:01,2023-07-12 +1375,20230713,06:30:01,2023-07-13 +1375,20230714,06:30:01,2023-07-14 +1368,20230717,06:30:01,2023-07-17 +1368,20230718,06:30:01,2023-07-18 +1361,20230719,06:30:01,2023-07-19 +1361,20230720,06:30:01,2023-07-20 +1361,20230721,06:30:01,2023-07-21 +1361,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1056,20230626,06:30:00,2023-06-26 +1056,20230627,06:30:01,2023-06-27 +1056,20230628,06:30:00,2023-06-28 +1056,20230629,06:30:00,2023-06-29 +1056,20230630,06:30:00,2023-06-30 +1054,20230703,06:30:00,2023-07-03 +1054,20230705,06:30:00,2023-07-05 +1055,20230706,06:30:01,2023-07-06 +1055,20230707,06:30:00,2023-07-07 +1055,20230710,06:30:00,2023-07-10 +1056,20230711,06:30:00,2023-07-11 +1062,20230712,06:30:00,2023-07-12 +1063,20230713,06:30:00,2023-07-13 +1061,20230714,06:30:00,2023-07-14 +1060,20230717,06:30:01,2023-07-17 +1060,20230718,06:30:00,2023-07-18 +1056,20230719,06:30:00,2023-07-19 +1057,20230720,06:30:01,2023-07-20 +1057,20230721,06:30:01,2023-07-21 +1057,20230724,06:30:01,2023-07-24 + + diff --git a/EventDriven/tests/data/runThreads.txt b/EventDriven/tests/data/runThreads.txt new file mode 100644 index 0000000..e676034 --- /dev/null +++ b/EventDriven/tests/data/runThreads.txt @@ -0,0 +1,77 @@ + 4526 function calls in 6.617 seconds + + Ordered by: cumulative time + + ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.000 0.000 6.617 6.617 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:45(test_runThreads) + 3 0.000 0.000 6.617 2.206 /Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/threads.py:4(runThreads) + 132 0.001 0.000 6.606 0.050 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:295(wait) + 462 6.605 0.014 6.605 0.014 {method 'acquire' of '_thread.lock' objects} + 72 0.001 0.000 6.475 0.090 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:612(result_iterator) + 69 0.000 0.000 6.474 0.094 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:314(_result_or_cancel) + 69 0.001 0.000 6.473 0.094 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:428(result) + 3 0.000 0.000 0.140 0.047 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:583(map) + 3 0.000 0.000 0.140 0.047 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:608() + 69 0.001 0.000 0.140 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/thread.py:161(submit) + 69 0.001 0.000 0.138 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/thread.py:180(_adjust_thread_count) + 24 0.000 0.000 0.135 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:945(start) + 24 0.000 0.000 0.133 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:611(wait) + 3 0.000 0.000 0.002 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:646(__exit__) + 3 0.000 0.000 0.002 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/thread.py:216(shutdown) + 24 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1087(join) + 24 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1125(_wait_for_tstate_lock) + 24 0.001 0.000 0.001 0.000 {built-in method _thread.start_new_thread} + 69 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:440(acquire) + 24 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:856(__init__) + 96 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:243(__init__) + 69 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:328(__init__) + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:364(cancel) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:562(__init__) + 231 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:271(__enter__) + 231 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:274(__exit__) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1051(_stop) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1324(_make_invoke_excepthook) + 162 0.000 0.000 0.000 0.000 {built-in method _thread.allocate_lock} + 87 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:283(_acquire_restore) + 45 0.000 0.000 0.000 0.000 {method '_acquire_restore' of '_thread.RLock' objects} + 87 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:286(_is_owned) + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:90(RLock) + 45 0.000 0.000 0.000 0.000 {method '_release_save' of '_thread.RLock' objects} + 87 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:280(_release_save) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:829(_maintain_shutdown_locks) + 48 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1453(current_thread) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:85(add) + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:427(__setitem__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/thread.py:123(__init__) + 258 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.lock' objects} + 138 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.RLock' objects} + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/thread.py:47(__init__) + 72 0.000 0.000 0.000 0.000 {method 'put' of '_queue.SimpleQueue' objects} + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:398(__get_result) + 48 0.000 0.000 0.000 0.000 {method 'add' of 'set' objects} + 162 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.RLock' objects} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:429(__init__) + 126 0.000 0.000 0.000 0.000 {built-in method time.monotonic} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:839() + 132 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects} + 111 0.000 0.000 0.000 0.000 {method 'release' of '_thread.lock' objects} + 93 0.000 0.000 0.000 0.000 {method '__enter__' of '_thread.lock' objects} + 3 0.000 0.000 0.000 0.000 {built-in method posix.cpu_count} + 69 0.000 0.000 0.000 0.000 {method 'pop' of 'list' objects} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:39(_remove) + 66 0.000 0.000 0.000 0.000 {built-in method builtins.len} + 24 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:369(remove) + 48 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1206(daemon) + 63 0.000 0.000 0.000 0.000 {method 'remove' of 'collections.deque' objects} + 48 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:575(is_set) + 24 0.000 0.000 0.000 0.000 {method 'difference_update' of 'set' objects} + 48 0.000 0.000 0.000 0.000 {built-in method _thread.get_ident} + 45 0.000 0.000 0.000 0.000 {method '_is_owned' of '_thread.RLock' objects} + 81 0.000 0.000 0.000 0.000 {method 'locked' of '_thread.lock' objects} + 24 0.000 0.000 0.000 0.000 {method 'discard' of 'set' objects} + 3 0.000 0.000 0.000 0.000 {built-in method builtins.min} + 3 0.000 0.000 0.000 0.000 {method 'reverse' of 'list' objects} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/concurrent/futures/_base.py:643(__enter__) + 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} + + diff --git a/EventDriven/tests/data/runThreads_eod_results.csv b/EventDriven/tests/data/runThreads_eod_results.csv new file mode 100644 index 0000000..7003273 --- /dev/null +++ b/EventDriven/tests/data/runThreads_eod_results.csv @@ -0,0 +1,989 @@ +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 + + diff --git a/EventDriven/tests/data/runThreads_oi_results.csv b/EventDriven/tests/data/runThreads_oi_results.csv new file mode 100644 index 0000000..3f423d0 --- /dev/null +++ b/EventDriven/tests/data/runThreads_oi_results.csv @@ -0,0 +1,529 @@ +Open_interest,Date,time,Datetime +1399,20230626,06:30:00,2023-06-26 +1399,20230627,06:30:00,2023-06-27 +1399,20230628,06:30:01,2023-06-28 +1399,20230629,06:30:00,2023-06-29 +1399,20230630,06:30:00,2023-06-30 +1399,20230703,06:30:01,2023-07-03 +1399,20230705,06:30:00,2023-07-05 +1399,20230706,06:30:01,2023-07-06 +1399,20230707,06:30:00,2023-07-07 +1399,20230710,06:30:01,2023-07-10 +1399,20230711,06:30:01,2023-07-11 +1399,20230712,06:30:01,2023-07-12 +1399,20230713,06:30:01,2023-07-13 +1404,20230714,06:30:00,2023-07-14 +1406,20230717,06:30:00,2023-07-17 +1412,20230718,06:30:00,2023-07-18 +1356,20230719,06:30:00,2023-07-19 +1351,20230720,06:30:00,2023-07-20 +1352,20230721,06:30:00,2023-07-21 +1352,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1912,20230626,06:30:00,2023-06-26 +1912,20230627,06:30:00,2023-06-27 +1910,20230628,06:30:01,2023-06-28 +1910,20230629,06:30:00,2023-06-29 +1910,20230630,06:30:01,2023-06-30 +1910,20230703,06:30:00,2023-07-03 +1909,20230705,06:30:01,2023-07-05 +1909,20230706,06:30:00,2023-07-06 +1909,20230707,06:30:01,2023-07-07 +1909,20230710,06:30:00,2023-07-10 +1909,20230711,06:30:00,2023-07-11 +1909,20230712,06:30:00,2023-07-12 +1909,20230713,06:30:01,2023-07-13 +1908,20230714,06:30:00,2023-07-14 +1908,20230717,06:30:01,2023-07-17 +1908,20230718,06:30:00,2023-07-18 +1900,20230719,06:30:01,2023-07-19 +1900,20230720,06:30:01,2023-07-20 +1900,20230721,06:30:00,2023-07-21 +1901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +818,20230626,06:30:00,2023-06-26 +818,20230627,06:30:00,2023-06-27 +818,20230628,06:30:01,2023-06-28 +818,20230629,06:30:00,2023-06-29 +818,20230630,06:30:01,2023-06-30 +820,20230703,06:30:00,2023-07-03 +826,20230705,06:30:01,2023-07-05 +826,20230706,06:30:00,2023-07-06 +826,20230707,06:30:01,2023-07-07 +826,20230710,06:30:00,2023-07-10 +826,20230711,06:30:00,2023-07-11 +826,20230712,06:30:00,2023-07-12 +826,20230713,06:30:01,2023-07-13 +825,20230714,06:30:00,2023-07-14 +826,20230717,06:30:01,2023-07-17 +823,20230718,06:30:00,2023-07-18 +814,20230719,06:30:01,2023-07-19 +813,20230720,06:30:01,2023-07-20 +1004,20230721,06:30:00,2023-07-21 +1006,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1622,20230626,06:30:00,2023-06-26 +1622,20230627,06:30:00,2023-06-27 +1622,20230628,06:30:01,2023-06-28 +1622,20230629,06:30:00,2023-06-29 +1616,20230630,06:30:01,2023-06-30 +1616,20230703,06:30:00,2023-07-03 +1616,20230705,06:30:01,2023-07-05 +1616,20230706,06:30:00,2023-07-06 +1616,20230707,06:30:01,2023-07-07 +1624,20230710,06:30:00,2023-07-10 +1625,20230711,06:30:00,2023-07-11 +1625,20230712,06:30:00,2023-07-12 +1625,20230713,06:30:01,2023-07-13 +1625,20230714,06:30:00,2023-07-14 +1650,20230717,06:30:01,2023-07-17 +1876,20230718,06:30:00,2023-07-18 +1863,20230719,06:30:01,2023-07-19 +1864,20230720,06:30:01,2023-07-20 +1859,20230721,06:30:00,2023-07-21 +1861,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1709,20230626,06:30:00,2023-06-26 +1709,20230627,06:30:00,2023-06-27 +1709,20230628,06:30:01,2023-06-28 +1709,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1709,20230705,06:30:01,2023-07-05 +1709,20230706,06:30:00,2023-07-06 +1710,20230707,06:30:01,2023-07-07 +1710,20230710,06:30:00,2023-07-10 +2025,20230711,06:30:00,2023-07-11 +2027,20230712,06:30:00,2023-07-12 +2027,20230713,06:30:01,2023-07-13 +2027,20230714,06:30:00,2023-07-14 +2020,20230717,06:30:01,2023-07-17 +2030,20230718,06:30:00,2023-07-18 +1993,20230719,06:30:01,2023-07-19 +1993,20230720,06:30:01,2023-07-20 +2000,20230721,06:30:00,2023-07-21 +2000,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +4405,20230626,06:30:00,2023-06-26 +4462,20230627,06:30:00,2023-06-27 +4462,20230628,06:30:01,2023-06-28 +4463,20230629,06:30:00,2023-06-29 +4464,20230630,06:30:01,2023-06-30 +4464,20230703,06:30:00,2023-07-03 +4464,20230705,06:30:01,2023-07-05 +5124,20230706,06:30:00,2023-07-06 +7080,20230707,06:30:01,2023-07-07 +7080,20230710,06:30:00,2023-07-10 +7081,20230711,06:30:00,2023-07-11 +7081,20230712,06:30:00,2023-07-12 +7081,20230713,06:30:01,2023-07-13 +7082,20230714,06:30:00,2023-07-14 +7102,20230717,06:30:01,2023-07-17 +7107,20230718,06:30:00,2023-07-18 +7133,20230719,06:30:01,2023-07-19 +7140,20230720,06:30:01,2023-07-20 +7141,20230721,06:30:00,2023-07-21 +7152,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +998,20230626,06:30:00,2023-06-26 +1007,20230627,06:30:00,2023-06-27 +1007,20230628,06:30:01,2023-06-28 +1007,20230629,06:30:00,2023-06-29 +1007,20230630,06:30:01,2023-06-30 +1009,20230703,06:30:00,2023-07-03 +1009,20230705,06:30:01,2023-07-05 +1009,20230706,06:30:00,2023-07-06 +1009,20230707,06:30:01,2023-07-07 +1009,20230710,06:30:00,2023-07-10 +1009,20230711,06:30:00,2023-07-11 +1009,20230712,06:30:00,2023-07-12 +1009,20230713,06:30:01,2023-07-13 +1009,20230714,06:30:00,2023-07-14 +1002,20230717,06:30:01,2023-07-17 +1002,20230718,06:30:00,2023-07-18 +966,20230719,06:30:01,2023-07-19 +966,20230720,06:30:01,2023-07-20 +968,20230721,06:30:00,2023-07-21 +968,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1460,20230626,06:30:00,2023-06-26 +1524,20230627,06:30:00,2023-06-27 +1724,20230628,06:30:01,2023-06-28 +1724,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1714,20230705,06:30:01,2023-07-05 +1716,20230706,06:30:00,2023-07-06 +1718,20230707,06:30:01,2023-07-07 +1702,20230710,06:30:00,2023-07-10 +1720,20230711,06:30:00,2023-07-11 +1720,20230712,06:30:00,2023-07-12 +1721,20230713,06:30:01,2023-07-13 +1721,20230714,06:30:00,2023-07-14 +1731,20230717,06:30:01,2023-07-17 +1738,20230718,06:30:00,2023-07-18 +1942,20230719,06:30:01,2023-07-19 +1956,20230720,06:30:01,2023-07-20 +1961,20230721,06:30:00,2023-07-21 +1961,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +857,20230626,06:30:00,2023-06-26 +949,20230627,06:30:00,2023-06-27 +1151,20230628,06:30:01,2023-06-28 +1155,20230629,06:30:00,2023-06-29 +1160,20230630,06:30:00,2023-06-30 +1162,20230703,06:30:01,2023-07-03 +1162,20230705,06:30:00,2023-07-05 +1439,20230706,06:30:01,2023-07-06 +1439,20230707,06:30:00,2023-07-07 +1440,20230710,06:30:01,2023-07-10 +1455,20230711,06:30:01,2023-07-11 +1459,20230712,06:30:01,2023-07-12 +1463,20230713,06:30:01,2023-07-13 +1466,20230714,06:30:00,2023-07-14 +1466,20230717,06:30:00,2023-07-17 +1466,20230718,06:30:00,2023-07-18 +1501,20230719,06:30:00,2023-07-19 +1524,20230720,06:30:00,2023-07-20 +1605,20230721,06:30:01,2023-07-21 +1636,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +976,20230626,06:30:01,2023-06-26 +1052,20230627,06:30:01,2023-06-27 +1054,20230628,06:30:01,2023-06-28 +1054,20230629,06:30:01,2023-06-29 +1143,20230630,06:30:00,2023-06-30 +1149,20230703,06:30:01,2023-07-03 +1149,20230705,06:30:01,2023-07-05 +1149,20230706,06:30:00,2023-07-06 +1149,20230707,06:30:01,2023-07-07 +1149,20230710,06:30:00,2023-07-10 +1220,20230711,06:30:01,2023-07-11 +1265,20230712,06:30:01,2023-07-12 +1264,20230713,06:30:01,2023-07-13 +1264,20230714,06:30:01,2023-07-14 +1293,20230717,06:30:01,2023-07-17 +1296,20230718,06:30:01,2023-07-18 +1374,20230719,06:30:01,2023-07-19 +1452,20230720,06:30:01,2023-07-20 +1455,20230721,06:30:01,2023-07-21 +1455,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +8245,20230626,06:30:00,2023-06-26 +8245,20230627,06:30:00,2023-06-27 +8941,20230628,06:30:01,2023-06-28 +10141,20230629,06:30:00,2023-06-29 +10141,20230630,06:30:00,2023-06-30 +10146,20230703,06:30:01,2023-07-03 +10148,20230705,06:30:00,2023-07-05 +10148,20230706,06:30:01,2023-07-06 +13459,20230707,06:30:00,2023-07-07 +13459,20230710,06:30:01,2023-07-10 +13477,20230711,06:30:01,2023-07-11 +14711,20230712,06:30:01,2023-07-12 +14727,20230713,06:30:01,2023-07-13 +14726,20230714,06:30:00,2023-07-14 +14726,20230717,06:30:00,2023-07-17 +14723,20230718,06:30:00,2023-07-18 +14738,20230719,06:30:00,2023-07-19 +14751,20230720,06:30:00,2023-07-20 +15159,20230721,06:30:00,2023-07-21 +15160,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +826,20230626,06:30:00,2023-06-26 +811,20230627,06:30:00,2023-06-27 +811,20230628,06:30:01,2023-06-28 +811,20230629,06:30:00,2023-06-29 +811,20230630,06:30:01,2023-06-30 +811,20230703,06:30:00,2023-07-03 +811,20230705,06:30:01,2023-07-05 +811,20230706,06:30:00,2023-07-06 +811,20230707,06:30:01,2023-07-07 +811,20230710,06:30:00,2023-07-10 +812,20230711,06:30:00,2023-07-11 +812,20230712,06:30:00,2023-07-12 +810,20230713,06:30:01,2023-07-13 +810,20230714,06:30:00,2023-07-14 +807,20230717,06:30:01,2023-07-17 +807,20230718,06:30:00,2023-07-18 +831,20230719,06:30:01,2023-07-19 +831,20230720,06:30:01,2023-07-20 +830,20230721,06:30:00,2023-07-21 +830,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1394,20230626,06:30:01,2023-06-26 +1404,20230627,06:30:00,2023-06-27 +1404,20230628,06:30:00,2023-06-28 +1404,20230629,06:30:01,2023-06-29 +1404,20230630,06:30:01,2023-06-30 +1404,20230703,06:30:01,2023-07-03 +1404,20230705,06:30:01,2023-07-05 +1404,20230706,06:30:00,2023-07-06 +1404,20230707,06:30:01,2023-07-07 +1404,20230710,06:30:01,2023-07-10 +1409,20230711,06:30:00,2023-07-11 +1417,20230712,06:30:00,2023-07-12 +1417,20230713,06:30:00,2023-07-13 +1416,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1415,20230719,06:30:00,2023-07-19 +1415,20230720,06:30:00,2023-07-20 +1417,20230721,06:30:00,2023-07-21 +1417,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1413,20230626,06:30:01,2023-06-26 +1413,20230627,06:30:00,2023-06-27 +1413,20230628,06:30:00,2023-06-28 +1413,20230629,06:30:01,2023-06-29 +1413,20230630,06:30:01,2023-06-30 +1413,20230703,06:30:01,2023-07-03 +1413,20230705,06:30:01,2023-07-05 +1424,20230706,06:30:00,2023-07-06 +1424,20230707,06:30:01,2023-07-07 +1426,20230710,06:30:01,2023-07-10 +1429,20230711,06:30:00,2023-07-11 +1426,20230712,06:30:00,2023-07-12 +1426,20230713,06:30:00,2023-07-13 +1423,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1395,20230719,06:30:00,2023-07-19 +1395,20230720,06:30:00,2023-07-20 +1400,20230721,06:30:00,2023-07-21 +1400,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +7477,20230626,06:30:01,2023-06-26 +7482,20230627,06:30:01,2023-06-27 +7482,20230628,06:30:01,2023-06-28 +7503,20230629,06:30:01,2023-06-29 +7503,20230630,06:30:00,2023-06-30 +8562,20230703,06:30:01,2023-07-03 +8562,20230705,06:30:01,2023-07-05 +8562,20230706,06:30:00,2023-07-06 +8562,20230707,06:30:01,2023-07-07 +8561,20230710,06:30:00,2023-07-10 +8947,20230711,06:30:01,2023-07-11 +8947,20230712,06:30:01,2023-07-12 +8947,20230713,06:30:01,2023-07-13 +8947,20230714,06:30:01,2023-07-14 +8914,20230717,06:30:01,2023-07-17 +8913,20230718,06:30:01,2023-07-18 +8890,20230719,06:30:01,2023-07-19 +8891,20230720,06:30:01,2023-07-20 +8904,20230721,06:30:01,2023-07-21 +8901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1361,20230626,06:30:01,2023-06-26 +1361,20230627,06:30:01,2023-06-27 +1361,20230628,06:30:01,2023-06-28 +1361,20230629,06:30:01,2023-06-29 +1361,20230630,06:30:00,2023-06-30 +1374,20230703,06:30:01,2023-07-03 +1374,20230705,06:30:01,2023-07-05 +1374,20230706,06:30:00,2023-07-06 +1374,20230707,06:30:01,2023-07-07 +1374,20230710,06:30:00,2023-07-10 +1361,20230711,06:30:01,2023-07-11 +1374,20230712,06:30:01,2023-07-12 +1375,20230713,06:30:01,2023-07-13 +1375,20230714,06:30:01,2023-07-14 +1368,20230717,06:30:01,2023-07-17 +1368,20230718,06:30:01,2023-07-18 +1361,20230719,06:30:01,2023-07-19 +1361,20230720,06:30:01,2023-07-20 +1361,20230721,06:30:01,2023-07-21 +1361,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1056,20230626,06:30:00,2023-06-26 +1056,20230627,06:30:01,2023-06-27 +1056,20230628,06:30:00,2023-06-28 +1056,20230629,06:30:00,2023-06-29 +1056,20230630,06:30:00,2023-06-30 +1054,20230703,06:30:00,2023-07-03 +1054,20230705,06:30:00,2023-07-05 +1055,20230706,06:30:01,2023-07-06 +1055,20230707,06:30:00,2023-07-07 +1055,20230710,06:30:00,2023-07-10 +1056,20230711,06:30:00,2023-07-11 +1062,20230712,06:30:00,2023-07-12 +1063,20230713,06:30:00,2023-07-13 +1061,20230714,06:30:00,2023-07-14 +1060,20230717,06:30:01,2023-07-17 +1060,20230718,06:30:00,2023-07-18 +1056,20230719,06:30:00,2023-07-19 +1057,20230720,06:30:01,2023-07-20 +1057,20230721,06:30:01,2023-07-21 +1057,20230724,06:30:01,2023-07-24 + + diff --git a/EventDriven/tests/data/runThreads_tick_results.txt b/EventDriven/tests/data/runThreads_tick_results.txt new file mode 100644 index 0000000..b582be2 --- /dev/null +++ b/EventDriven/tests/data/runThreads_tick_results.txt @@ -0,0 +1,46 @@ +GOOGL20240621P132.5 + +GOOGL20240621P130 + +GOOGL20240621P127.5 + +GOOGL20240621P125 + +GOOGL20240621P122.5 + +GOOGL20240621P120 + +GOOGL20240621P117.5 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P107.5 + +GOOGL20240621P105 + +GOOGL20240621P102.5 + +GOOGL20240621P100 + +GOOGL20240621P98 + +GOOGL20240621P97.5 + +GOOGL20240621P96 + +GOOGL20240621P95 + +GOOGL20240621P94 + +GOOGL20240621P92 + diff --git a/EventDriven/tests/data/run_async.txt b/EventDriven/tests/data/run_async.txt new file mode 100644 index 0000000..0832156 --- /dev/null +++ b/EventDriven/tests/data/run_async.txt @@ -0,0 +1,1395 @@ + 858846 function calls (848142 primitive calls) in 9.918 seconds + + Ordered by: cumulative time + + ncalls tottime percall cumtime percall filename:lineno(function) + 1 0.000 0.000 9.919 9.919 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:160(run) + 3 0.000 0.000 9.918 3.306 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:618(run_until_complete) + 3 0.000 0.000 9.918 3.306 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:594(run_forever) + 10 0.001 0.000 9.918 0.992 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:1859(_run_once) + 1 0.000 0.000 9.918 9.918 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:86(run) + 101 0.000 0.000 9.917 0.098 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:82(_run) + 101 0.002 0.000 9.916 0.098 {method 'run' of '_contextvars.Context' objects} + 46 0.002 0.000 8.575 0.186 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:34(request_from_proxy) + 46 0.001 0.000 8.572 0.186 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/api.py:14(request) + 46 0.001 0.000 8.561 0.186 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:500(request) + 46 0.003 0.000 8.415 0.183 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:673(send) + 46 0.001 0.000 8.030 0.175 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:613(send) + 46 0.002 0.000 7.993 0.174 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:594(urlopen) + 46 0.002 0.000 7.982 0.174 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:379(_make_request) + 155 0.002 0.000 7.142 0.046 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:704(readinto) + 155 7.139 0.046 7.139 0.046 {method 'recv_into' of '_socket.socket' objects} + 46 0.003 0.000 6.884 0.150 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:481(getresponse) + 46 0.001 0.000 6.863 0.149 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1351(getresponse) + 46 0.003 0.000 6.858 0.149 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:318(begin) + 46 0.002 0.000 6.820 0.148 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:285(_read_status) + 322 0.001 0.000 6.818 0.021 {method 'readline' of '_io.BufferedReader' objects} + 23 0.008 0.000 5.050 0.220 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:274(retrieve_eod_ohlc_async) + 23 0.005 0.000 4.660 0.203 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:529(retrieve_openInterest_async) + 46 0.002 0.000 1.094 0.024 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:365(request) + 92 0.001 0.000 1.082 0.012 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:988(send) + 46 0.000 0.000 1.081 0.024 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1287(endheaders) + 46 0.000 0.000 1.081 0.023 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1049(_send_output) + 46 0.000 0.000 1.074 0.023 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:278(connect) + 46 0.002 0.000 1.074 0.023 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:193(_new_conn) + 46 0.001 0.000 1.072 0.023 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/connection.py:27(create_connection) + 46 1.040 0.023 1.040 0.023 {method 'connect' of '_socket.socket' objects} + 138 0.001 0.000 0.376 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:890(content) + 460 0.001 0.000 0.376 0.001 {method 'join' of 'bytes' objects} + 115 0.001 0.000 0.375 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:816(generate) + 115 0.001 0.000 0.374 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:1038(stream) + 69 0.002 0.000 0.373 0.005 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:910(read) + 115 0.003 0.000 0.367 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:858(_raw_read) + 115 0.001 0.000 0.354 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:801(_fp_read) + 115 0.002 0.000 0.354 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:457(read) + 92 0.004 0.000 0.330 0.004 {method 'read' of '_io.BufferedReader' objects} + 253 0.005 0.000 0.304 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:673(to_datetime) + 253 0.004 0.000 0.254 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:314(_convert_listlike_datetimes) + 46 0.002 0.000 0.221 0.005 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:868(read_csv) + 46 0.003 0.000 0.218 0.005 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:583(_read) + 1173 0.029 0.000 0.205 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:475(__new__) + 2 0.000 0.000 0.197 0.098 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:54(test_async) + 1 0.000 0.000 0.196 0.196 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:61() + 23 0.001 0.000 0.196 0.009 /Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/helper.py:649(generate_option_tick_new) + 414 0.006 0.000 0.179 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4062(__getitem__) + 46 0.002 0.000 0.175 0.004 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1907(read) + 23 0.000 0.000 0.165 0.007 /Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/helper.py:63(save_option_keys) + 23 0.009 0.000 0.165 0.007 /Users/chiemelienwanisobi/cloned_repos/QuantTools/trade/helpers/helper.py:55(import_option_keys) + 253 0.061 0.000 0.162 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:126(_guess_datetime_format_for_array) + 2254 0.029 0.000 0.154 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:517(sanitize_array) + 644/598 0.018 0.000 0.151 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:389(__init__) + 23 0.000 0.000 0.148 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/__init__.py:274(load) + 138 0.003 0.000 0.146 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:694(__init__) + 23 0.000 0.000 0.144 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/__init__.py:299(loads) + 23 0.000 0.000 0.141 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/decoder.py:332(decode) + 23 0.141 0.006 0.141 0.006 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/decoder.py:343(raw_decode) + 230 0.004 0.000 0.134 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4271(__setitem__) + 46 0.003 0.000 0.131 0.003 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:423(dict_to_mgr) + 230 0.001 0.000 0.129 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4514(_set_item) + 230 0.004 0.000 0.115 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4481(_set_item_mgr) + 46 0.001 0.000 0.113 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:750(merge_environment_settings) + 46 0.000 0.000 0.110 0.002 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:826(get_environ_proxies) + 230 0.006 0.000 0.102 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1347(insert) + 92 0.001 0.000 0.099 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:11661(sum) +154518/153874 0.055 0.000 0.098 0.000 {built-in method builtins.isinstance} + 92 0.000 0.000 0.097 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:12498(sum) + 92 0.001 0.000 0.097 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:12459(_min_count_stat_function) + 92 0.005 0.000 0.095 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:11435(_reduce) + 2116 0.006 0.000 0.081 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7593(ensure_index) + 92 0.011 0.000 0.080 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2499(getproxies_environment) + 253 0.005 0.000 0.080 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:456(_array_strptime_with_fallback) + 115 0.002 0.000 0.078 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6186(_get_indexer_strict) + 345 0.005 0.000 0.077 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:3820(get_indexer) + 13984 0.013 0.000 0.071 0.000 :859(__iter__) + 207 0.001 0.000 0.067 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6162(get_indexer_for) + 230 0.005 0.000 0.065 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6956(insert) + 46 0.001 0.000 0.060 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:765(should_bypass_proxies) + 46 0.000 0.000 0.057 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2672(proxy_bypass) + 69 0.001 0.000 0.052 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:4789(apply) + 69 0.000 0.000 0.051 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/apply.py:1409(apply) + 552 0.002 0.000 0.050 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:769(_try_cast) + 46 0.000 0.000 0.050 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2685(getproxies) + 69 0.001 0.000 0.050 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/apply.py:1482(apply_standard) + 69 0.001 0.000 0.048 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:5636(rename) + 253 0.003 0.000 0.047 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1200(maybe_cast_to_datetime) + 69 0.002 0.000 0.047 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:1070(_rename) + 115 0.001 0.000 0.047 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4142(_take_with_is_copy) + 161 0.001 0.000 0.046 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/common.py:62(new_method) + 17986 0.017 0.000 0.044 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:173(search) + 115 0.002 0.000 0.044 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4027(take) + 46 0.001 0.000 0.043 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:96(arrays_to_mgr) + 92 0.000 0.000 0.043 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:3951(T) + 161 0.010 0.000 0.043 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:708(_slice_take_blocks_ax0) + 92 0.002 0.000 0.043 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:3767(transpose) + 13226 0.017 0.000 0.042 0.000 :674(__getitem__) + 253 0.004 0.000 0.042 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:666(_parse) + 138 0.001 0.000 0.040 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6133(_arith_method) + 138 0.002 0.000 0.039 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:1371(_arith_method) + 115 0.001 0.000 0.039 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:869(take) + 230 0.003 0.000 0.038 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:674(_with_infer) + 46 0.002 0.000 0.038 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1575(__init__) + 138 0.001 0.000 0.037 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:623(reindex_indexer) + 46 0.017 0.000 0.035 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:222(read) + 46 0.001 0.000 0.033 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:607(_init_dict) + 17066 0.015 0.000 0.033 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/generic.py:42(_instancecheck) + 46 0.001 0.000 0.033 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:231(parse_headers) + 345 0.000 0.000 0.032 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6679(_maybe_cast_listlike_indexer) + 46 0.001 0.000 0.032 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:457(prepare_request) + 19488 0.019 0.000 0.032 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:272(_compile) + 253 0.001 0.000 0.031 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:325(_from_sequence) + 69 0.000 0.000 0.031 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:891(_map_values) + 253 0.005 0.000 0.030 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:329(_from_sequence_not_strict) + 46 0.002 0.000 0.030 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1848(_make_engine) + 368 0.003 0.000 0.029 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:3983(_ixs) + 46 0.001 0.000 0.029 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/parser.py:59(parsestr) + 92 0.001 0.000 0.028 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1483(reduce) + 115 0.001 0.000 0.028 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:4323(reindex) + 46 0.001 0.000 0.028 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/parser.py:41(parse) + 46 0.000 0.000 0.027 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:957(getaddrinfo) + 161 0.001 0.000 0.027 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6201(_construct_result) + 920 0.003 0.000 0.027 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6284(__getattr__) + 46 0.000 0.000 0.027 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/_mixins.py:78(method) + 46 0.001 0.000 0.027 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:736(map) + 46 0.026 0.001 0.026 0.001 {built-in method _socket.getaddrinfo} + 276 0.002 0.000 0.026 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4626(_get_item_cache) +39388/30068 0.018 0.000 0.026 0.000 {built-in method builtins.len} + 966 0.015 0.000 0.025 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1157(maybe_infer_to_datetimelike) + 46 0.000 0.000 0.024 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:419(_close_conn) + 46 0.001 0.000 0.024 0.001 {method 'close' of '_io.BufferedReader' objects} + 23 0.000 0.000 0.024 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:5433(drop) + 46 0.001 0.000 0.024 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2121(create_block_manager_from_column_arrays) + 23 0.000 0.000 0.024 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4757(drop) + 2599 0.009 0.000 0.023 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6301(__setattr__) + 46 0.001 0.000 0.023 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:781(close) + 92 0.001 0.000 0.022 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6432(dtypes) + 69 0.000 0.000 0.022 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arraylike.py:208(__truediv__) + 23 0.001 0.000 0.022 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4796(_drop_axis) + 69 0.003 0.000 0.022 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/algorithms.py:1667(map_array) + 46 0.000 0.000 0.022 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:489(_decref_socketios) + 94 0.000 0.000 0.022 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:499(close) + 48 0.000 0.000 0.021 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:495(_real_close) + 92 0.001 0.000 0.021 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:209(_maybe_cache) + 161 0.002 0.000 0.021 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1782(_consolidate_inplace) + 48 0.021 0.000 0.021 0.000 {function socket.close at 0x106f9f740} + 46 0.000 0.000 0.020 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:171(feed) + 92 0.001 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:176(_call_parse) + 345 0.002 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6394(_should_compare) + 1369 0.008 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_dtype.py:346(_name_get) + 92 0.003 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:216(_parsegen) + 46 0.010 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:60(__init__) + 667 0.008 0.000 0.019 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:231(asarray_tuplesafe) + 46 0.002 0.000 0.018 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:596(_homogenize) + 23 0.001 0.000 0.018 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:5993(set_index) + 253 0.002 0.000 0.018 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:199(split) + 230 0.007 0.000 0.018 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5368(insert) + 17066 0.014 0.000 0.018 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/generic.py:37(_check) + 46 0.000 0.000 0.018 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:351(prepare) + 25599 0.011 0.000 0.017 0.000 :760(decode) + 46 0.000 0.000 0.017 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:446(get_connection_with_tls_context) + 46 0.000 0.000 0.017 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2658(proxy_bypass_macosx_sysconf) + 12880 0.008 0.000 0.017 0.000 :697(__iter__) + 46 0.001 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2259(_consolidate) + 368 0.007 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:124(maybe_convert_platform) + 1633 0.011 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:274(full) + 736 0.002 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/array_algos/take.py:59(take_nd) + 13226 0.011 0.000 0.016 0.000 :756(encode) + 1288 0.006 0.000 0.016 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1596(pandas_dtype) + 394 0.001 0.000 0.015 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:101(isna) + 69 0.001 0.000 0.015 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6662(copy) + 46 0.001 0.000 0.015 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:359(build_response) + 394 0.002 0.000 0.015 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:184(_isna) + 598 0.004 0.000 0.014 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1863(from_array) + 1334 0.002 0.000 0.014 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:189(__next__) + 253 0.003 0.000 0.014 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1240(_ensure_nanosecond_dtype) + 1081 0.010 0.000 0.014 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6236(__finalize__) + 23 0.000 0.000 0.014 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4130(_getitem_bool_array) + 46 0.004 0.000 0.014 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:355(_concatenate_chunks) + 46 0.000 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:276(connection_from_host) + 46 0.013 0.000 0.013 0.000 {built-in method _scproxy._get_proxy_settings} + 92 0.001 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:398(reduce) + 414 0.006 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:842(_engine) + 46 0.000 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:5773(isna) + 46 0.000 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:305(connection_from_context) + 46 0.000 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:8693(isna) + 1495 0.010 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:649(_simple_new) + 736 0.003 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:145(_get_option) + 1886 0.004 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7688(maybe_extract_name) + 1334 0.008 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:77(get_token) + 414 0.002 0.000 0.013 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:1287(take_nd) +1771/1725 0.003 0.000 0.012 0.000 {built-in method builtins.all} + 506 0.002 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:2428(maybe_convert_dtype) + 46 0.000 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arraylike.py:200(__mul__) + 253 0.003 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:2184(_sequence_to_dt64) + 713 0.007 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/array_algos/take.py:120(_take_nd_ndarray) + 1518 0.012 0.000 0.012 0.000 {method 'reduce' of 'numpy.ufunc' objects} + 69 0.001 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:557(copy) + 529 0.001 0.000 0.012 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:511(_validate_dtype) + 92 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:11458(blk_func) + 69 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6520(_transform_index) + 253 0.003 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:875(_parse_numeric_token) + 667 0.006 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5323(__contains__) + 46 0.000 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:330(connection_from_pool_key) + 46 0.000 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:5136(reindex) + 276 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4608(_box_col_values) + 46 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:6463(astype) + 92 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:76(_f) + 115 0.002 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:317(apply) + 92 0.001 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:1176(__getitem__) + 46 0.000 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:5343(reindex) + 529 0.007 0.000 0.011 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:2744(inferred_type) + 1495 0.008 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:278(__init__) + 46 0.003 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2190(_form_blocks) + 23 0.000 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4477(__delitem__) + 736 0.003 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:807(_set_axis) + 621 0.003 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5552(equals) + 46 0.001 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:229(_new_pool) + 23 0.001 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7031(drop) +29257/29165 0.009 0.000 0.010 0.000 {built-in method builtins.getattr} + 368 0.003 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:2313(is_unique) + 23 0.001 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1434(idelete) + 92 0.001 0.000 0.010 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:1719(_getitem_axis) + 230 0.002 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1412(_insert_update_blklocs_and_blknos) + 4899 0.004 0.000 0.009 0.000 :117(__instancecheck__) + 46 0.000 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2662(getproxies_macosx_sysconf) + 828 0.003 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5373(__getitem__) + 46 0.009 0.000 0.009 0.000 {built-in method _scproxy._get_proxies} + 46 0.001 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:177(__init__) + 743 0.001 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:225(compile) + 230 0.003 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:1393(moveaxis) + 92 0.001 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:124(extract_cookies_to_jar) + 27140 0.009 0.000 0.009 0.000 {method 'decode' of 'bytes' objects} + 46 0.002 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:664(get_handle) + 299 0.005 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:3955(_get_indexer) + 46 0.001 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:947(json) + 230 0.001 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:5242(_sanitize_column) + 92 0.003 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2276(_merge_blocks) + 92 0.000 0.000 0.009 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:389(new_func) + 184 0.001 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5437(_can_hold_identifiers_and_holds_name) + 805 0.003 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:784(name) + 92 0.001 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:455(newfunc) + 18584 0.008 0.000 0.008 0.000 {method 'search' of 're.Pattern' objects} + 46 0.000 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:421(astype) + 46 0.001 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:409(prepare_url) + 276 0.001 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:40(__init__) + 92 0.002 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:369(parse_url) + 92 0.001 0.000 0.008 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:604(nansum) + 368 0.001 0.000 0.007 0.000 {built-in method builtins.sorted} + 46 0.000 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/simplejson/__init__.py:459(loads) + 46 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:242(__init__) + 368 0.003 0.000 0.007 0.000 :941(update) + 1369 0.002 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_dtype.py:330(_name_includes_bit_suffix) + 782 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6312(_index_as_unique) + 46 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/simplejson/decoder.py:379(decode) + 506 0.003 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:122(put) + 460 0.005 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5562(append) + 713 0.002 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:339(_from_mgr) + 2139 0.004 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:696(_sanitize_ndim) + 230 0.002 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:131(filterwarnings) + 184 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:287(get_dtypes) + 46 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:390(__init__) + 529 0.004 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:3777(get_loc) + 368 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:678(_constructor_sliced_from_mgr) + 92 0.002 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:237(ndarray_to_mgr) + 690 0.003 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5170(_get_engine_target) +3979/3956 0.005 0.000 0.007 0.000 {built-in method numpy.asarray} + 184 0.001 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:592(get_content_type) + 2668 0.004 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:416(extract_array) + 46 0.000 0.000 0.007 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:723(astype) + 92 0.007 0.000 0.007 0.000 {method 'sendall' of '_socket.socket' objects} + 483 0.001 0.000 0.006 0.000 {method 'any' of 'numpy.ndarray' objects} + 736 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:127(_get_single_key) + 368 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:490(get) + 460 0.005 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/concat.py:52(concat_compat) + 161 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/contextlib.py:141(__exit__) + 23 0.006 0.000 0.006 0.000 {built-in method io.open} + 46 0.006 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/simplejson/decoder.py:392(raw_decode) + 276 0.004 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1012(iget) + 644 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2264() + 322 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:61(merge_setting) + 46 0.002 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:337(extend) + 46 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:469(_parse_headers) + 230 0.002 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1146(take) + 46 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:204(get_netrc_auth) + 644 0.002 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:82(shape) + 46 0.000 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:585(__call__) + 506 0.002 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1163(is_float_dtype) + 1058 0.004 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2645(maybe_coerce_values) + 460 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:374(urlparse) + 46 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:674(astype) + 1587 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1434(_is_dtype_type) + 828 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2716(new_block) + 46 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:1174(_close_pool_connections) + 23 0.000 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arraylike.py:98(__add__) + 3335 0.006 0.000 0.006 0.000 {built-in method numpy.empty} + 322 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:225(_consolidate_key) + 423 0.001 0.000 0.006 0.000 {built-in method builtins.next} + 920 0.006 0.000 0.006 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:326() + 4899 0.005 0.000 0.006 0.000 {built-in method _abc._abc_instancecheck} + 736 0.001 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:236(set_axis) + 230 0.003 0.000 0.006 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1402(_insert_update_mgr_locs) + 345 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6415(_is_comparable_dtype) + 483 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_methods.py:55(_any) + 8163 0.005 0.000 0.005 0.000 {built-in method builtins.hasattr} + 552 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:154(get) + 1150 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1375(_is_dtype) + 138 0.003 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexers/utils.py:239(maybe_convert_indices) + 506 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_strptime.py:26(_getlang) + 805 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1571(validate_all_hashable) + 414 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2703(new_block_2d) + 46 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:573(__init__) + 9 0.000 0.000 0.005 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:740(compile) + 92 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:5598() + 46 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/astype.py:191(astype_array_safe) + 184 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:373(__getitem__) + 322 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:351(putheader) + 838 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numerictypes.py:357(issubdtype) + 798 0.002 0.000 0.005 0.000 {built-in method builtins.any} + 736 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:137(is_object_dtype) + 851 0.003 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:180(blknos) + 23 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arraylike.py:42(__ne__) + 230 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:735(_error_catcher) + 23 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6110(_cmp_method) + 1219 0.004 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:467(__enter__) + 92 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1680(extract_cookies) + 3427 0.003 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/inference.py:334(is_hashable) + 138 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:616(get_content_maintype) + 345 0.002 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1081(is_numeric_dtype) +15066/15020 0.004 0.000 0.005 0.000 {method 'encode' of 'str' objects} + 552 0.003 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:182(_add_filter) + 1081 0.005 0.000 0.005 0.000 {built-in method numpy.array} + 690 0.001 0.000 0.005 0.000 {method 'max' of 'numpy.ndarray' objects} + 253 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:208(findall) + 138 0.001 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/array_ops.py:240(arithmetic_op) + 46 0.000 0.000 0.005 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5651(identical) + 1656 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:734(name) + 414 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1580(construct_1d_object_array_from_listlike) + 1886 0.004 0.000 0.004 0.000 {method 'match' of 're.Pattern' objects} + 253 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2425(ensure_arraylike_for_datetimelike) + 4669 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:909(__len__) + 530 0.002 0.000 0.004 0.000 :771(get) + 506 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/locale.py:594(getlocale) + 230 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/_mixins.py:278(__getitem__) + 345 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6324(_maybe_downcast_for_indexing) + 46 0.000 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:182(close) + 759 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:428(append) + 138 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:227(_encode_invalid_chars) + 460 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:1330(normalize_axis_tuple) + 1426 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2674(get_block_type) + 736 0.002 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:86(_validate_set_axis) + 15297 0.004 0.000 0.004 0.000 {method 'lower' of 'str' objects} + 575 0.002 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1198(is_bool_dtype) + 253 0.001 0.000 0.004 0.000 {method 'sum' of 'numpy.ndarray' objects} + 1288 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:831(_values) + 391 0.002 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/inference.py:273(is_dict_like) + 966 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:568(require_length_match) + 3634 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/range.py:999(__len__) + 115 0.002 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6219(_raise_if_missing) + 46 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:483(prepare_headers) + 1173 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:609(_dtype_to_subclass) + 690 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_methods.py:39(_amax) + 10607 0.004 0.000 0.004 0.000 {built-in method builtins.issubclass} + 46 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:658(__init__) + 322 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:166(simplefilter) + 2093 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1331(is_ea_or_datetimelike_dtype) + 506 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:163(match) + 2369 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:481(ensure_wrapped_if_datetimelike) + 736 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:635(_get_root) + 46 0.003 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:115(__init__) + 506 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/_policybase.py:319(header_fetch_parse) + 460 0.004 0.000 0.004 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:576() + 92 0.000 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:635(release_conn) + 1932 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:84() + 46 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6459(any) + 46 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:304(_get_filepath_or_buffer) + 253 0.000 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_methods.py:47(_sum) + 92 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:838(select_proxy) + 46 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:255(__init__) + 138 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:35(__init__) + 253 0.001 0.000 0.004 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:97(is_bool_indexer) + 138 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:521(cookiejar_from_dict) + 46 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/request.py:2567(_proxy_bypass_macosx_sysconf) + 161 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:545(_box_func) + 207 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:12675(_reindex_for_setitem) + 46 0.003 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:447(astype) + 46 0.003 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1627(_get_options_with_defaults) + 9 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:979(parse) + 322 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1259(putheader) + 92 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1599(make_cookies) + 253 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:836(__iter__) + 138 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:261(_isna_array) + 92 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4883(_update_inplace) + 28/9 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:456(_parse_sub) + 46 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:454(__exit__) + 46 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:296(_put_conn) + 161 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/array_ops.py:189(_na_arithmetic_op) + 92 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:202(__init__) + 46/14 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:516(_parse) + 276 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_ufunc_config.py:33(seterr) + 253 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:659(_constructor_from_mgr) + 46 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:794(close) + 506 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/_policybase.py:289(_sanitize_header) + 253 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:221(__init__) + 46 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/range.py:137(__new__) + 299 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:536(is_string_dtype) + 1219 0.003 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:488(__exit__) + 506 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/locale.py:479(_parse_localename) + 46 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6418(_reduce) + 3105 0.001 0.000 0.003 0.000 {built-in method builtins.setattr} + 23 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:12590(values) + 1771 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:735(_sanitize_str_dtypes) + 414 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:164(hostname) + 23 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:2632(check_bool_indexer) + 322 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:345(to_key_val_list) + 230 0.001 0.000 0.003 0.000 {built-in method builtins.sum} + 23 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1633(as_array) + 46 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/astype.py:157(astype_array) + 46 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:303(makefile) + 230 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2311(_fast_count_smallints) + 92 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1165(_is_binary_mode) + 253 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:474(resolve_ymd) + 2737 0.003 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/__init__.py:34(using_copy_on_write) + 1676 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numerictypes.py:283(issubclass_) + 46 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:258(_get_conn) + 1288 0.002 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2004(internal_values) + 92 0.001 0.000 0.003 0.000 :229(expanduser) + 46 0.000 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:331(putrequest) + 92 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:1499(_maybe_null_out) + 23 0.001 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1707(_interleave) + 1173 0.003 0.000 0.003 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:591(_ensure_array) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:610(prepare_cookies) + 46 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:213(_read_headers) + 46 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1685(_clean_options) + 713 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/array_algos/take.py:564(_take_preprocess_indexer_and_fill_value) + 1012 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:366(notify) + 184 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2246(_stack_arrays) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:546(request_url) + 644 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:2776(_is_multi) + 161 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1764(is_consolidated) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1103(putrequest) + 1219 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/warnings.py:441(__init__) + 1610 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1590() + 253 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:2514(_validate_dt64_dtype) + 92 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:536(close) + 230 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/_policybase.py:301(header_source_parse) + 184 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_asarray.py:27(require) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:140(get_cookie_header) + 138 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_ufunc_config.py:430(__enter__) + 46 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/astype.py:56(_astype_nansafe) + 69 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:794(_set_axis_nocheck) + 92 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:253(_get_values) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/response.py:40(assert_header_parsing) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:487() + 253 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:304(_simple_new) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:315(__init__) + 5136 0.002 0.000 0.002 0.000 {method 'get' of 'dict' objects} + 345 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:914(__len__) + 368 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:127(__next__) + 2 0.000 0.000 0.002 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_strptime.py:261(compile) + 23 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexers/utils.py:419(check_array_indexer) + 253 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/algorithms.py:1131(take) + 161 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/common.py:81(get_op_result_name) + 3040 0.002 0.000 0.002 0.000 {built-in method __new__ of type object at 0x106802898} + 1495 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/flags.py:51(__init__) + 506 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/locale.py:396(normalize) + 2852 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:208(isnum) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:100(push) + 6886 0.002 0.000 0.002 0.000 {method 'append' of 'list' objects} + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:191(_validate_parse_dates_presence) + 736 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:676(_translate_key) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:137(__init__) + 1196 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:46(__setitem__) + 1311 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1392() + 92 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1204(is_potential_multi_index) + 460 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:1768(ravel) + 230 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2811(ensure_block_shape) + 506 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_dtype.py:178(_datetime_metadata_str) + 437 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:259(__getitem__) + 69 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:255(get) + 161 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1772(_consolidate_check) + 253 0.002 0.000 0.002 0.000 {method 'findall' of 're.Pattern' objects} + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:724() + 92 0.000 0.000 0.002 0.000 :16(exists) + 92 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/shape_base.py:219(vstack) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:482(nanany) + 23 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7553(ensure_index_from_sequences) + 115 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1433(find_common_type) + 92 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:466(array_equivalent) + 92 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:305(_isna_string_dtype) + 2139 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:758(_maybe_repeat) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:479(items) + 184 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:525(get_all) + 184 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1261(__init__) + 161 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/computation/expressions.py:226(evaluate) + 161 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/contextlib.py:299(helper) + 1725 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1399(_get_dtype) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:238(_new_conn) + 92 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:664(_constructor_from_mgr) + 506 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:2765(check_dict_or_set_indexers) + 9 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:573(_code) + 253 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:62(__init__) + 1472 0.002 0.000 0.002 0.000 {method 'split' of 'str' objects} + 102 0.002 0.000 0.002 0.000 {built-in method posix.stat} + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:159(resolve_redirects) + 276 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:1471(_set_as_cached) + 161 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2320(_preprocess_slice_or_indexer) + 92 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:267(clear) + 138 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:274(make_block) + 276 0.001 0.000 0.002 0.000 :778(__contains__) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/numpy_.py:492(to_numpy) + 92 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:958(fast_xs) + 299 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:551(maybe_promote) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:487() + 391 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:253(fill_value) + 785 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:193(__get__) + 554 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:119(_coerce_args) + 1081 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/flags.py:87(allows_duplicate_labels) + 1265 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/inference.py:300() + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:95(_default_key_normalizer) + 46 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:201(_set_noconvert_columns) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/ipaddress.py:1286(__init__) + 4140 0.002 0.000 0.002 0.000 {built-in method builtins.hash} + 552 0.002 0.000 0.002 0.000 {method 'remove' of 'list' objects} + 48 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:220(__init__) + 460 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:628(convert_milliseconds) + 46 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/numpy_.py:226(isna) + 322 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4402(_check_setitem_copy) + 3795 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5144(_values) + 23 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6916(delete) + 1633 0.002 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:831(_reset_identity) + 506 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2537(dtype_to_unit) + 161 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/computation/expressions.py:67(_evaluate_standard) + 460 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:205(_hostinfo) + 138 0.000 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_ufunc_config.py:435(__exit__) + 460 0.001 0.000 0.002 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:586(_get_axis) + 1058 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:271(__enter__) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/typing.py:1327(__instancecheck__) + 207 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:1006(_find_hms_idx) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:76(copy) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:396(build_connection_pool_key_attributes) + 230 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:1034(check_header_validity) + 4554 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/typing.py:2287(cast) + 552 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/utils.py:52(_has_surrogates) + 1817 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:3809() + 1058 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:51(__getitem__) + 232 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:728(is_valid_na_for_dtype) + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2152(unit) + 23 0.001 0.000 0.001 0.000 {method '__exit__' of '_io._IOBase' objects} + 138 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2353(copy) + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:679(_init_length) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:34(__init__) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:143(clear) + 1150 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:214(is_extension) + 1012 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:286(_is_owned) + 230 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:302(add) + 345 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:74(__len__) + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:1141(_to_decimal) + 345 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:3996(_check_indexing_method) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:240(init_poolmanager) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:90(_urllib3_request_context) + 64/9 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:37(_compile) + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1778() + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:316(close) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:351(_encode_target) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:390(_check_close) + 299 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:572(condition) + 1348 0.001 0.000 0.001 0.000 {method 'format' of 'str' objects} + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:900(default_headers) + 1242 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:123() + 1150 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/__init__.py:55(using_pyarrow_string_dtype) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:79() + 667 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:791(is_) + 828 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:219(_can_consolidate) + 345 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7723(_unpack_nested_dtype) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2177(_grouping_func) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/ipaddress.py:1187(_ip_int_from_string) + 184 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:29(_splitparam) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:243(get_payload) + 345 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1643(__len__) + 415 0.001 0.000 0.001 0.000 :1207(_handle_fromlist) + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1356(add_cookie_header) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:588(prepare_auth) + 368 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:77(readline) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:564(_array_equivalent_object) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:209(_maybe_get_mask) + 276 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_ufunc_config.py:132(geterr) + 1058 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1837(__init__) + 161 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/contextlib.py:104(__init__) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:248(stringify_path) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/__init__.py:183(dumps) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:134(__init__) + 368 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:1253(iget) + 138 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/range.py:201(_simple_new) + 1058 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:274(__exit__) + 230 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5295(_validate_fill_value) + 437 0.001 0.000 0.001 0.000 {method 'take' of 'numpy.ndarray' objects} + 1059 0.001 0.000 0.001 0.000 {method 'startswith' of 'str' objects} + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:395(__init__) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:1045(_validate_header_part) + 736 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:667(_info_axis) + 1058 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:248(_qsize) + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/array_ops.py:288(comparison_op) + 713 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/array_algos/take.py:325(_get_take_nd_function) + 276 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4623(_clear_item_cache) + 391 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:456(_engine_type) + 69 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6537() + 1312 0.001 0.000 0.001 0.000 {built-in method numpy.datetime_data} + 269/107 0.000 0.000 0.001 0.000 :121(__subclasscheck__) + 484 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/missing.py:673(na_value_for_dtype) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:1018(get_auth_from_url) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:362(_make_index) + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:380(validate) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/typing.py:1602(__subclasscheck__) + 966 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1270(is_1d_only_ea_dtype) + 368 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/base.py:549(find) + 736 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:617(_select_options) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:539(get_encoding_from_headers) + 1081 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:203(isword) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:1025(argsort) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:199(__init__) + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:948(from_blocks) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2095(create_block_manager_from_blocks) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5134(array) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:81(__init__) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:494(prepare_body) + 253 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:707(dtype) + 138 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:301(maybe_iterable_to_list) + 345 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2795(extend_blocks) + 161 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/dispatch.py:17(should_extension_dispatch) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:1380() + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1233(dedup_names) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1990(__exit__) + 207 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:790(copy) + 414 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1040(needs_i8_conversion) + 1369 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_dtype.py:24(_kind_name) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/retry.py:202(__init__) + 23 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5172(delete) + 276 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:350(getitem_block_columns) + 115 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1519(construct_1d_arraylike_from_scalar) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:122(_reset_cache) + 506 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:2570(_validate_tz_from_dtype) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/encoder.py:183(encode) + 414 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:292(make_block_same_class) + 101 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:752(call_soon) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:550(infer_compression) + 368 0.001 0.000 0.001 0.000 {method 'copy' of 'numpy.ndarray' objects} + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:2133(_refine_defaults_read) + 69 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:446(_init_decoder) + 184 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:303(_normalize_host) + 161 0.001 0.000 0.001 0.000 {method 'view' of 'numpy.generic' objects} + 736 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:372(apply_if_callable) + 269/107 0.001 0.000 0.001 0.000 {built-in method _abc._abc_subclasscheck} + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:370() + 115 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/common.py:103(_maybe_match_name) + 322 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:400() + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/api.py:386(default_index) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/typing.py:373(inner) + 138 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:243(__init__) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:660(requote_uri) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:367() + 2783 0.001 0.000 0.001 0.000 {method 'read' of '_io.StringIO' objects} + 1242 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:121(classes) + 138 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:84() + 161 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/contextlib.py:132(__enter__) + 1656 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/multiarray.py:1080(copyto) + 46 0.000 0.000 0.001 0.000 {built-in method from_bytes} + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:838(_cleanup) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1622(close) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:2322(any) + 1955 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:551(dtype) + 138 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/array_ops.py:507(maybe_prepare_scalar_for_op) + 2990 0.001 0.000 0.001 0.000 {built-in method _warnings._filters_mutated} + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:69(close) + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:263(_remove_path_dot_segments) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:106(_encode_params) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/tools/datetimes.py:149(should_cache) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:350(_maybe_make_multi_index_columns) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:424(close) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:53(_wrapfunc) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:509(urlunparse) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:3995(_maybe_update_cacher) + 299 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:291(arrays) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:849(__init__) + 4048 0.001 0.000 0.001 0.000 {method 'isdigit' of 'str' objects} + 2254 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:405(flags) + 1242 0.001 0.000 0.001 0.000 {built-in method numpy.asanyarray} + 92 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/shape_base.py:81(atleast_2d) + 230 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:4585(_ensure_valid_index) + 23 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:571() + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:675(empty) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:107(get_redirect_target) + 2209 0.001 0.000 0.001 0.000 {method 'pop' of 'dict' objects} + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:1667(_validate_integer) + 207 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:336(hms) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:774(get_proxy) + 1426 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:58() + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:909(text) + 69 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/apply.py:1377(__init__) + 69 0.001 0.000 0.001 0.000 {built-in method _operator.truediv} + 759 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:261(get) + 253 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1993(dtype) + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:266(index_labels_to_array) + 322 0.001 0.000 0.001 0.000 {method 'reshape' of 'numpy.ndarray' objects} + 92 0.001 0.000 0.001 0.000 {method 'readlines' of '_io._IOBase' objects} + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:542(merge_cookies) + 277 0.001 0.000 0.001 0.000 {method 'clear' of 'dict' objects} + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:99(_intenum_converter) + 138 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:534() + 805 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:152(cast_scalar_indexer) + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/encoder.py:205(iterencode) + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:378(interleaved_dtype) + 184 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:109(__init__) + 92 0.001 0.000 0.001 0.000 {method 'settimeout' of '_socket.socket' objects} + 276 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1227(deepvalues) + 506 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:251(_put) + 368 0.000 0.000 0.001 0.000 :790(items) + 1 0.000 0.000 0.001 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:62(__exit__) + 805 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:572(_get_axis_number) + 115 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:251(put) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:1135(_splitport) + 736 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/config.py:649(_get_deprecated_option) + 1 0.000 0.000 0.001 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:65(close) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:348(_get_timeout) + 414 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/construction.py:688(_sanitize_non_ordered) + 368 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:72(check) + 207 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4379(_set_is_copy) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:71(_wrapreduction) + 115 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:4436(_wrap_reindex_result) + 184 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/ipaddress.py:1213(_parse_octet) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/encodings/idna.py:145(encode) + 184 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:288() + 101 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:781(_call_soon) + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:586() + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:52(__init__) + 966 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1940(_block) + 1 0.000 0.000 0.001 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1186(_get_binary_io_classes) + 47 0.001 0.000 0.001 0.000 {built-in method builtins.locals} + 299 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/inference.py:195(is_array_like) + 138 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:6137(_align_for_op) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/url.py:100(__new__) + 1 0.000 0.000 0.001 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/compat/_optional.py:85(import_optional_dependency) + 770 0.001 0.000 0.001 0.000 {built-in method builtins.max} + 345 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:576(tz) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1674(_check_file_or_buffer) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/numpy_.py:95(__init__) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/inference.py:105(is_file_like) + 184 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:332(array) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:1149(_normalize_host) + 1104 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:718(dtype) + 138 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:164(host) + 1 0.000 0.000 0.001 0.001 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/importlib/__init__.py:108(import_module) + 92 0.000 0.000 0.001 0.000 {function SocketIO.close at 0x106ff8540} + 1 0.000 0.000 0.001 0.001 :1192(_gcd_import) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:769(is_redirect) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1850(from_blocks) + 1 0.000 0.000 0.001 0.001 :1165(_find_and_load) + 1058 0.001 0.000 0.001 0.000 {method '__enter__' of '_thread.lock' objects} + 253 0.001 0.000 0.001 0.000 {method 'nonzero' of 'numpy.ndarray' objects} + 46 0.000 0.000 0.001 0.000 {method 'all' of 'numpy.ndarray' objects} + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2285() + 506 0.001 0.000 0.001 0.000 {built-in method _locale.setlocale} + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:572(prepare_content_length) + 253 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/extension.py:67(fget) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:448(size) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:583(copy_func) + 23 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:321() + 230 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:507(set_raw) + 184 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_asarray.py:108() + 276 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:90(RLock) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:601(_set_noconvert_dtype_columns) + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:974(close) + 943 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:974(dtype) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:847(quote) + 1012 0.001 0.000 0.001 0.000 {method 'acquire' of '_thread.lock' objects} + 46 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:692(__init__) + 460 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:369() + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:630(prepare_hooks) + 110 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:688(__call__) + 967 0.001 0.000 0.001 0.000 {method 'pop' of 'list' objects} + 437 0.001 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:529(is_string_or_object_np_dtype) + 49 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:430(create_task) + 322 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/range.py:553(equals) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:871(_do_date_conversions) + 92 0.001 0.000 0.001 0.000 {method 'argsort' of 'numpy.ndarray' objects} + 1 0.000 0.000 0.001 0.001 :1120(_find_and_load_unlocked) + 1518 0.001 0.000 0.001 0.000 {method 'partition' of 'str' objects} + 92 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:405(_check_values_indices_shape_match) + 46 0.000 0.000 0.001 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:376(create_task) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:172(from_float) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_methods.py:61(_all) + 1 0.000 0.000 0.000 0.000 :1054(_find_spec) + 971 0.000 0.000 0.000 0.000 {method 'join' of 'str' objects} + 598 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/_config/__init__.py:42(warn_copy_on_write) + 506 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:254(_get) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1096(_can_fast_transpose) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:799(mount) + 1173 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/flags.py:55(allows_duplicate_labels) + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:4440(_maybe_preserve_names) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1010(view) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1277(is_extension_array_dtype) + 368 0.000 0.000 0.000 0.000 {method 'fullmatch' of 're.Pattern' objects} + 1081 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:363(attrs) + 713 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:246(items) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:188(clone) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/retry.py:387(is_retry) + 93 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:1467(debug) + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:57(__iter__) + 785 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:1257(value) + 184 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:884(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:195(_new_message) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/request.py:189(body_to_chunks) + 155 0.000 0.000 0.000 0.000 {method '_checkReadable' of '_io._IOBase' objects} + 414 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:540() + 644 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:353() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:194(close) + 783 0.000 0.000 0.000 0.000 {method 'insert' of 'list' objects} + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:505(get_rename_function) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_strptime.py:238(pattern) + 860 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:240(__next) + 1104 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/multiarray.py:153(concatenate) + 1092 0.000 0.000 0.000 0.000 {method 'items' of 'dict' objects} + 1 0.000 0.000 0.000 0.000 :1499(find_spec) + 207 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:329(month) + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:2310(_construct_from_dt64_naive) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:207(register_hook) + 667 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:196(blklocs) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:58(__enter__) + 506 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1194() + 1 0.000 0.000 0.000 0.000 :1467(_get_spec) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:131(_lazy_init) + 460 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/concat.py:73() + 345 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:131() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/connection.py:93(_set_socket_options) + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:482() + 414 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:213(isspace) + 560 0.000 0.000 0.000 0.000 {method 'rstrip' of 'str' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:84(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:781(get_adapter) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1392(np_find_common_type) + 253 0.000 0.000 0.000 0.000 {method 'transpose' of 'numpy.ndarray' objects} + 874 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1671(name) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1047(shape) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:808(new_event_loop) + 368 0.000 0.000 0.000 0.000 {built-in method time.time} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:324(_get_dtype_max) + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimes.py:103(tz_to_dtype) + 460 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/util.py:19(to_str) + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:511(_compile_info) + 47 0.000 0.000 0.000 0.000 :405(parent) + 161 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/missing.py:131(dispatch_fill_zeros) + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:291(__iter__) + 45 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:243(_optimize_charset) + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1111(_values) + 1196 0.000 0.000 0.000 0.000 {built-in method builtins.callable} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:773(_view) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:568(__init__) + 115 0.000 0.000 0.000 0.000 {method 'write' of '_io.BytesIO' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:358(update) + 10 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:558(select) + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:592(_get_block_manager_axis) + 460 0.000 0.000 0.000 0.000 {method 'ravel' of 'numpy.ndarray' objects} + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/response.py:9(is_fp_closed) + 10 0.000 0.000 0.000 0.000 :1607(find_spec) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:121(pushlines) + 1012 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:511(f) + 376 0.000 0.000 0.000 0.000 {method 'find' of 'str' objects} + 368 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1028(_output) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:742(_get_axes) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:102(__setitem__) + 201 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:743(readable) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:515(get_compression_method) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:175(port) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:245(read_timeout) + 184 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1229() + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:150(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/retry.py:379(_is_method_retryable) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:520(urlunsplit) + 368 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/retry.py:242() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:207(validate_header_arg) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:258(__init__) + 345 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:1122() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:923(quote_from_bytes) + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/apply.py:121(__init__) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/hooks.py:15(default_hooks) + 276 0.000 0.000 0.000 0.000 {built-in method numpy.seterrobj} + 322 0.000 0.000 0.000 0.000 {method 'copy' of 'dict' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:693(new_event_loop) + 115 0.000 0.000 0.000 0.000 {method 'astype' of 'numpy.ndarray' objects} + 207 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:319(jump) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/copy.py:66(copy) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/_mixins.py:157(take) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/unix_events.py:63(__init__) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/compat/numpy/function.py:413(validate_func) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:212() + 1636 0.000 0.000 0.000 0.000 {built-in method builtins.ord} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:330() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:188(all_none) + 12 0.000 0.000 0.000 0.000 {method 'control' of 'select.kqueue' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:49(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:514(_parse_content_type_header) + 46 0.000 0.000 0.000 0.000 {built-in method numpy.arange} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:4518(_check_inplace_and_allows_duplicate_labels) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:135(super_len) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1734(clear_expired_cookies) + 1083 0.000 0.000 0.000 0.000 {method 'isalpha' of 'str' objects} + 423 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:168(__getitem__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:35() + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/shape_base.py:215(_vhstack_dispatcher) + 231 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1763(np_can_hold_element) + 483 0.000 0.000 0.000 0.000 {method 'upper' of 'str' objects} + 368 0.000 0.000 0.000 0.000 {method 'setdefault' of 'dict' objects} + 324 0.000 0.000 0.000 0.000 {method 'strip' of 'str' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:799(iter_content) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:59() + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:5676() + 345 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:126(_classes_and_not_datetimelike) + 1061 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.lock' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:1079(closed) + 184 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/util/_validators.py:226(validate_bool_kwarg) + 299 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:303() + 506 0.000 0.000 0.000 0.000 {method 'capitalize' of 'str' objects} + 610 0.000 0.000 0.000 0.000 {method 'rpartition' of 'str' objects} + 207 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2148(_creso) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:136(ones) + 102 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:35(__init__) + 186 0.000 0.000 0.000 0.000 {method 'acquire' of '_thread.RLock' objects} + 73/20 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:178(getwidth) + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:1262(_slice) + 460 0.000 0.000 0.000 0.000 {built-in method numpy.core._multiarray_umath.normalize_axis_index} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:1123(_make_date_converter) + 138 0.000 0.000 0.000 0.000 {method 'subn' of 're.Pattern' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/astype.py:249(astype_is_view) + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1753(__iter__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:728(gather) + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:447(isclosed) + 552 0.000 0.000 0.000 0.000 {built-in method numpy.geterrobj} + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2063(_maybe_pin_freq) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:196(mgr_to_mgr) + 184 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:241(is_single_block) + 598 0.000 0.000 0.000 0.000 {method 'values' of 'dict' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/typing.py:1394(__hash__) + 46 0.000 0.000 0.000 0.000 {built-in method _operator.mul} + 46 0.000 0.000 0.000 0.000 {method 'setsockopt' of '_socket.socket' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:805() + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/cloned_repos/QuantTools/EventDriven/Testing/concurency_test.py:60() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:85(path_url) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:91(ensure_python_int) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:319(asi8) + 278 0.000 0.000 0.000 0.000 {method 'lstrip' of 'str' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:334(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:244(__init__) + 69 0.000 0.000 0.000 0.000 {method 'view' of 'numpy.ndarray' objects} + 345 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/compat/numpy/function.py:64(__call__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:1269(_process_date_conversion) + 506 0.000 0.000 0.000 0.000 {method 'pop' of 'set' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:1426(_validate_parse_dates_arg) + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/_internal_utils.py:25(to_native_string) + 48 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:764(_done_callback) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:211(is_multipart) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:126(resolve_default_timeout) + 492 0.000 0.000 0.000 0.000 {method 'popleft' of 'collections.deque' objects} + 368 0.000 0.000 0.000 0.000 :812(__init__) + 690 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:1480(_clear_item_cache) + 69 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:580(_get_axis_name) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_collections.py:95(__getitem__) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:343() + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:2794(_na_value) + 110 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:1095(__new__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:436(flush) + 184 0.000 0.000 0.000 0.000 {method 'count' of 'str' objects} + 414 0.000 0.000 0.000 0.000 {method 'values' of 'collections.OrderedDict' objects} + 93 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:1734(isEnabledFor) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:392(ensure_dtype_objs) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/json/__init__.py:244(detect_encoding) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:706() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:279(_extract_multi_indexer_columns) + 69 0.000 0.000 0.000 0.000 {method 'getvalue' of '_io.BytesIO' objects} + 51 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:662(_ensure_future) + 46 0.000 0.000 0.000 0.000 {built-in method _codecs.lookup} + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:1455() + 92 0.000 0.000 0.000 0.000 {method 'update' of 'dict' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:1117(_maybe_memory_map) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:213() + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:1525(__and__) + 138 0.000 0.000 0.000 0.000 {method 'endswith' of 'str' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/message.py:331(set_payload) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/hooks.py:22(dispatch_hook) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:208(_pop_message) + 155 0.000 0.000 0.000 0.000 {method '_checkClosed' of '_io._IOBase' objects} + 368 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:660(_constructor) + 230 0.000 0.000 0.000 0.000 {method 'item' of 'numpy.ndarray' objects} + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/array_algos/take.py:287(_get_take_nd_function_cached) + 391 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/missing.py:1073(clean_reindex_fill_method) + 414 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:913(__init__) + 23 0.000 0.000 0.000 0.000 {built-in method _operator.ne} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:636(unquote_unreserved) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/shape_base.py:207(_arrays_for_stack_dispatcher) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:156(username) + 554 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:108(_noop) + 460 0.000 0.000 0.000 0.000 {built-in method _operator.index} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:393(prepare_method) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1069(_is_homogeneous_type) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:304(cert_verify) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:943(urlencode) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:659(unquote) + 460 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5558(_append_dispatcher) + 552 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:130(_validate_timeout) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/poolmanager.py:374(_merge_pool_kwargs) + 552 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/base.py:363(ndim) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:161(iloc) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:893(_check_data_length) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:403(retries) + 529 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:6672(_maybe_cast_indexer) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:105(_make_self_pipe) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/ops/array_ops.py:594(_bool_arith_check) + 92 0.000 0.000 0.000 0.000 {method 'clear' of 'collections.OrderedDict' objects} + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:360(convertyear) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/_request_methods.py:51(__init__) + 460 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:1764(_ravel_dispatcher) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/c_parser_wrapper.py:326() + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:465(_decode) + 437 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:266(mgr_locs) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1297(_cookie_attrs) + 46 0.000 0.000 0.000 0.000 :815(__len__) + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/response.py:248(__len__) + 46 0.000 0.000 0.000 0.000 :2(__init__) + 193 0.000 0.000 0.000 0.000 {method 'extend' of 'list' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexing.py:1165(_check_deprecated_callable_usage) + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1176(_maybe_disallow_fill) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:255(_has_complex_date_col) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:131(close) + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:402(has_year) + 230 0.000 0.000 0.000 0.000 {built-in method sys.getrefcount} + 276 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:2043(freq) + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:7652(ensure_has_len) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/_internal_utils.py:38(unicode_is_ascii) + 207 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:256(match) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:606(_maybe_promote_cached) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:2056(_clean_na_values) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:978(__array__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:2365(_validate_skipfooter) + 207 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/generic.py:696(ndim) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:110(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1251(_validate_host) + 50 0.000 0.000 0.000 0.000 :126(_path_join) + 138 0.000 0.000 0.000 0.000 {method 'count' of 'bytes' objects} + 167 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:176(append) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:775(infer_dtype_from_scalar) + 10 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:98(closegroup) + 186 0.000 0.000 0.000 0.000 {method 'release' of '_thread.RLock' objects} + 184 0.000 0.000 0.000 0.000 {method '__exit__' of '_thread.RLock' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/unix_events.py:67(close) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/hooks.py:16() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/base.py:115(__eq__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/util.py:7(to_bytes) + 184 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:117(info) + 92 0.000 0.000 0.000 0.000 {method 'truncate' of '_io.StringIO' objects} + 46 0.000 0.000 0.000 0.000 {method 'write' of '_io.StringIO' objects} + 92 0.000 0.000 0.000 0.000 {method 'extend' of 'collections.deque' objects} + 184 0.000 0.000 0.000 0.000 {method 'groups' of 're.Match' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexers/utils.py:62(is_list_like_indexer) + 45 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:216(_compile_charset) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1686(name) + 138 0.000 0.000 0.000 0.000 {built-in method sys.audit} + 253 0.000 0.000 0.000 0.000 {method 'keys' of 'dict' objects} + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/datetimelike.py:745(_get_engine_target) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:1447(is_index_col) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:91(merge_hooks) + 414 0.000 0.000 0.000 0.000 {method 'isspace' of 'str' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:2294() + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:614(_maybe_promote) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:86(close) + 46 0.000 0.000 0.000 0.000 {built-in method sys.getfilesystemencoding} + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1463() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:188(_expand_user) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:202(start_connect) + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5364(_insert_dispatcher) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:140(__init__) + 216 0.000 0.000 0.000 0.000 {method 'append' of 'collections.deque' objects} + 184 0.000 0.000 0.000 0.000 {method 'seek' of '_io.StringIO' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:11522() + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/_ufunc_config.py:426(__init__) + 253 0.000 0.000 0.000 0.000 {method 'is_finite' of 'decimal.Decimal' objects} + 138 0.000 0.000 0.000 0.000 {method 'decode' of 'bytearray' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/dtypes.py:1454(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:918(_wrap_ipv6) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/structures.py:60(__len__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:193(_userinfo) + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/multiarray.py:892(bincount) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:44(_debug) + 10 0.000 0.000 0.000 0.000 :140(_path_stat) + 23 0.000 0.000 0.000 0.000 {built-in method _operator.add} + 46 0.000 0.000 0.000 0.000 {built-in method _codecs.lookup_error} + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:211() + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:654(ensure_future) + 92 0.000 0.000 0.000 0.000 {method 'pop' of 'collections.OrderedDict' objects} + 230 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/numeric.py:1389(_moveaxis_dispatcher) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/construction.py:585(_ensure_2d) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/common.py:292(is_fsspec_url) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/cookiejar.py:1290(_cookies_for_request) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/common.py:992(is_numeric_v_string_like) + 49 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:39(_remove) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:265(_add_reader) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:72() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/parser.py:17(__init__) + 138 0.000 0.000 0.000 0.000 {method 'end' of 're.Match' objects} + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/base.py:397(ensure_np_dtype) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/util/_validators.py:450(check_dtype_backend) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:2267(_extract_dialect) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/connection.py:103(allowed_gai_family) + 92 0.000 0.000 0.000 0.000 :41(_get_sep) + 46 0.000 0.000 0.000 0.000 :1() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:235() + 71 0.000 0.000 0.000 0.000 {built-in method fromkeys} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:97(_close_self_pipe) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/request.py:134(set_file_position) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:452(urlsplit) + 48 0.000 0.000 0.000 0.000 {built-in method _thread.allocate_lock} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1234(_validate_method) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1230(_encode_request) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:692(_constructor) + 253 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/dateutil/parser/_parser.py:186(__iter__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/inspect.py:292(isclass) + 54 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/enum.py:1507(_get_value) + 123 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:164(__len__) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:527(validate_integer) + 46 0.000 0.000 0.000 0.000 {built-in method numpy.zeros} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/common.py:192() + 91 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:293(tell) + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:344() + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/datetimelike.py:390(_get_getitem_freq) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:390(__init__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:762(get_event_loop_policy) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1472() + 50 0.000 0.000 0.000 0.000 :128() + 23 0.000 0.000 0.000 0.000 {method 'fill' of 'numpy.ndarray' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/datetimelike.py:141(equals) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:1243(_validate_path) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1955(np_can_cast_scalar) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1479() + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:2586(get_values) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:986(_validate_usecols_arg) + 238 0.000 0.000 0.000 0.000 {method 'isascii' of 'str' objects} + 157 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:518(_check_closed) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:754(_init_event_loop_policy) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/blocks.py:1249(shape) + 138 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/timeout.py:227(connect_timeout) + 184 0.000 0.000 0.000 0.000 {method 'items' of 'collections.OrderedDict' objects} + 49 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:85(add) + 46 0.000 0.000 0.000 0.000 {method 'rstrip' of 'bytes' objects} + 92 0.000 0.000 0.000 0.000 {method 'split' of 'bytes' objects} + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/email/feedparser.py:124(__iter__) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/series.py:865(_references) + 25 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:453(_uniq) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/http/client.py:895(_get_hostport) + 13/9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:436(_get_literal_prefix) + 19 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:398(_simple) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:559(_validate_names) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1721() + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/encodings/__init__.py:71(search_function) + 46 0.000 0.000 0.000 0.000 {method 'sort' of 'list' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:193(_cancel_all_tasks) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:376(_escape) + 92 0.000 0.000 0.000 0.000 {built-in method posix.fspath} + 54 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_futures.py:14(isfuture) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:655(_constructor) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:281(_remove_reader) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:743(set_environ) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:891(default_user_agent) + 92 0.000 0.000 0.000 0.000 {method 'update' of 'collections.OrderedDict' objects} + 69 0.000 0.000 0.000 0.000 {method 'startswith' of 'bytes' objects} + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1094() + 152 0.000 0.000 0.000 0.000 {built-in method builtins.min} + 52 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/futures.py:299(_get_loop) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:42(all_tasks) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/socket.py:651(socketpair) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:295(is_closed) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/response.py:79() + 10 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:86(opengroup) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:386(_mk_bitmap) + 14 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:316(_class_escape) + 155 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:1954(get_debug) + 46 0.000 0.000 0.000 0.000 {built-in method _socket.getdefaulttimeout} + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:231(__init__) + 56 0.000 0.000 0.000 0.000 {built-in method time.monotonic} + 71 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:113(__init__) + 46 0.000 0.000 0.000 0.000 {method 'reverse' of 'list' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:517(register) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/cloned_repos/FinanceDatabase/dbase/DataAPI/ThetaData.py:677(__isSuccesful) + 115 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/base.py:1979(nlevels) + 1 0.000 0.000 0.000 0.000 {built-in method builtins.print} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/base_parser.py:247() + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:1474() + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/range.py:216(_validate_dtype) + 111 0.000 0.000 0.000 0.000 {method 'find' of 'bytearray' objects} + 1 0.000 0.000 0.000 0.000 :169(__enter__) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:181(_run_until_complete_cb) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:1021(_argsort_dispatcher) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/queue.py:245(_init) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:180(get_key) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/utils.py:581(iter_slices) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/signal.py:56(signal) + 46 0.000 0.000 0.000 0.000 {function HTTPResponse.flush at 0x107ca1300} + 8 0.000 0.000 0.000 0.000 {built-in method _asyncio._get_running_loop} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/copy.py:107(_copy_immutable) + 4 0.000 0.000 0.000 0.000 {method 'sub' of 're.Pattern' objects} + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:467(_get_charset_prefix) + 55 0.000 0.000 0.000 0.000 {built-in method builtins.iter} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connection.py:183(host) + 92 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/shape_base.py:77(_atleast_2d_dispatcher) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/numpy_.py:146(dtype) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/multiarray.py:1318(shares_memory) + 11 0.000 0.000 0.000 0.000 :78(_check_methods) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/indexes/datetimes.py:265(_engine_type) + 41 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:83(groups) + 3 0.000 0.000 0.000 0.000 {method 'set_result' of '_asyncio.Future' objects} + 43 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/frozendict/monkeypatch.py:145(frozendictMutableMappingSubclasshook) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/coroutines.py:11(_is_debug_mode) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/io/parsers/readers.py:1987(__enter__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/ipaddress.py:574(__int__) + 10 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:701(time) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/fromnumeric.py:2317(_any_dispatcher) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:803(set_event_loop) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:63(__iter__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:69(__getitem__) + 78 0.000 0.000 0.000 0.000 :409(__subclasshook__) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:388() + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/frame.py:1030(axes) + 1 0.000 0.000 0.000 0.000 :179(_get_module_lock) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:509(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/internals/managers.py:1828(ndim) + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:570(isstring) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:535(unregister) + 50 0.000 0.000 0.000 0.000 :244(_verbose_message) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/unix_events.py:1431(__init__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/nanops.py:187(_get_fill_value) + 1 0.000 0.000 0.000 0.000 {built-in method _socket.socketpair} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/util/proxy.py:11(connection_requires_http_tunnel) + 23 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/lib/function_base.py:5168(_delete_dispatcher) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/models.py:216() + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:104(__init__) + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:274(getuntil) + 9 0.000 0.000 0.000 0.000 {built-in method _sre.compile} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/multiarray.py:669(result_type) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/arrays/_mixins.py:105(_box_func) + 73 0.000 0.000 0.000 0.000 {built-in method _sre.unicode_iscased} + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:963(fix_flags) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/signal.py:36(_enum_to_int) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/adapters.py:578(add_headers) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:707(__init__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/unix_events.py:1440(set_event_loop) + 49 0.000 0.000 0.000 0.000 {method 'discard' of 'set' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/cookies.py:87(get_new_headers) + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:587(_check_running) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/urllib3/connectionpool.py:339(_validate_conn) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:234(register) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/signal.py:62(getsignal) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:667(__init__) + 95 0.000 0.000 0.000 0.000 {method 'exception' of '_asyncio.Task' objects} + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:413(_splitnetloc) + 51 0.000 0.000 0.000 0.000 {method 'add' of 'set' objects} + 95 0.000 0.000 0.000 0.000 {method 'cancelled' of '_asyncio.Task' objects} + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/requests/sessions.py:451(__enter__) + 49 0.000 0.000 0.000 0.000 {method 'add_done_callback' of '_asyncio.Task' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/encodings/idna.py:298(getregentry) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/_distutils_hack/__init__.py:102(find_spec) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/encodings/__init__.py:43(normalize_encoding) + 6 0.000 0.000 0.000 0.000 {built-in method sys.set_asyncgen_hooks} + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/__init__.py:315(_subx) + 22 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:428(_get_iscased) + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:77(__init__) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:215(_fileobj_lookup) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:583(close) + 10 0.000 0.000 0.000 0.000 :283(__subclasshook__) + 1 0.000 0.000 0.000 0.000 :173(__exit__) + 2 0.000 0.000 0.000 0.000 :262(__subclasshook__) + 46 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:65(_set_task_name) + 18 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_compiler.py:31(_combine_flags) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:27(__exit__) + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:304(checkgroupname) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/coroutines.py:34(iscoroutine) + 2 0.000 0.000 0.000 0.000 {built-in method _signal.signal} + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:228(_acquireLock) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:664(close) + 51 0.000 0.000 0.000 0.000 {built-in method _sre.unicode_tolower} + 52 0.000 0.000 0.000 0.000 {method 'get_loop' of '_asyncio.Future' objects} + 6 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:1939(_set_coroutine_origin_tracking) + 49 0.000 0.000 0.000 0.000 {method 'result' of '_asyncio.Task' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/getlimits.py:685(__init__) + 11 0.000 0.000 0.000 0.000 :1424(_path_importer_cache) + 19 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:172(__setitem__) + 48 0.000 0.000 0.000 0.000 {method 'done' of '_asyncio.Future' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/weakref.py:289(update) + 2 0.000 0.000 0.000 0.000 {method 'setblocking' of '_socket.socket' objects} + 10 0.000 0.000 0.000 0.000 :67(_relax_case) + 6 0.000 0.000 0.000 0.000 {built-in method _asyncio._set_running_loop} + 1 0.000 0.000 0.000 0.000 :516(__subclasshook__) + 10 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/selector_events.py:737(_process_events) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/runners.py:49(__init__) + 46 0.000 0.000 0.000 0.000 {built-in method _asyncio.get_running_loop} + 1 0.000 0.000 0.000 0.000 {built-in method builtins.__import__} + 4 0.000 0.000 0.000 0.000 :381(__subclasshook__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:209(__init__) + 6 0.000 0.000 0.000 0.000 :1030(__exit__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:686(set_event_loop) + 1 0.000 0.000 0.000 0.000 :920(find_spec) + 1 0.000 0.000 0.000 0.000 :1081(__subclasshook__) + 1 0.000 0.000 0.000 0.000 :71(__init__) + 1 0.000 0.000 0.000 0.000 :100(acquire) + 1 0.000 0.000 0.000 0.000 :125(release) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/signal.py:24(_int_to_enum) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:540(shutdown_asyncgens) + 1 0.000 0.000 0.000 0.000 {built-in method time.get_clock_info} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:1957(set_debug) + 2 0.000 0.000 0.000 0.000 :362(__subclasshook__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:1720(getEffectiveLevel) + 6 0.000 0.000 0.000 0.000 :1026(__enter__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:72(__len__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:37(__init__) + 4 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:21(_fileobj_to_fd) + 1 0.000 0.000 0.000 0.000 {method 'close' of 'select.kqueue' objects} + 9 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:697(is_running) + 1 0.000 0.000 0.000 0.000 :94(__new__) + 6 0.000 0.000 0.000 0.000 {method 'index' of 'str' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:247(unregister) + 1 0.000 0.000 0.000 0.000 :748(find_spec) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:237(_releaseLock) + 3 0.000 0.000 0.000 0.000 {built-in method sys.get_asyncgen_hooks} + 13 0.000 0.000 0.000 0.000 {method 'replace' of 'str' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:53(_commit_removals) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1453(current_thread) + 1 0.000 0.000 0.000 0.000 :233(_call_with_frames_removed) + 8 0.000 0.000 0.000 0.000 {built-in method _imp.release_lock} + 1 0.000 0.000 0.000 0.000 :357(__init__) + 1 0.000 0.000 0.000 0.000 :198(cb) + 4 0.000 0.000 0.000 0.000 {method 'translate' of 'bytearray' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:21(__enter__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:922(_maybe_infer_dtype_type) + 8 0.000 0.000 0.000 0.000 {built-in method _imp.acquire_lock} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:268(close) + 6 0.000 0.000 0.000 0.000 {built-in method _thread.get_ident} + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/urllib/parse.py:421(_checknetloc) + 6 0.000 0.000 0.000 0.000 {method 'isidentifier' of 'str' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/events.py:68(cancel) + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:656(stop) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/getlimits.py:696(min) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:525(ensure_dtype_can_hold_na) + 1 0.000 0.000 0.000 0.000 :315(__subclasshook__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/six.py:194(find_spec) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/_weakrefset.py:17(__init__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:100(checkgroup) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/numpy/core/getlimits.py:709(max) + 1 0.000 0.000 0.000 0.000 {built-in method _imp.is_builtin} + 2 0.000 0.000 0.000 0.000 {built-in method _signal.getsignal} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/pandas/core/dtypes/cast.py:737(_ensure_dtype_type) + 1 0.000 0.000 0.000 0.000 :165(__init__) + 1 0.000 0.000 0.000 0.000 :1101(_sanity_check) + 2 0.000 0.000 0.000 0.000 {built-in method _contextvars.copy_context} + 1 0.000 0.000 0.000 0.000 {built-in method _imp.find_frozen} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:691(__del__) + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/logging/__init__.py:1319(disable) + 3 0.000 0.000 0.000 0.000 {method 'remove_done_callback' of '_asyncio.Task' objects} + 4 0.000 0.000 0.000 0.000 {method 'isalnum' of 'str' objects} + 3 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:687(is_closed) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/base_events.py:565(shutdown_default_executor) + 2 0.000 0.000 0.000 0.000 {method 'fileno' of '_socket.socket' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:63(__init__) + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/asyncio/tasks.py:61() + 2 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/selectors.py:272(get_map) + 4 0.000 0.000 0.000 0.000 {method 'exception' of '_asyncio.Future' objects} + 2 0.000 0.000 0.000 0.000 {method 'detach' of '_socket.socket' objects} + 3 0.000 0.000 0.000 0.000 {method 'done' of '_asyncio.Task' objects} + 4 0.000 0.000 0.000 0.000 {method 'cancelled' of '_asyncio.Future' objects} + 1 0.000 0.000 0.000 0.000 {built-in method atexit.register} + 2 0.000 0.000 0.000 0.000 {method 'add_done_callback' of '_asyncio.Future' objects} + 1 0.000 0.000 0.000 0.000 {built-in method sys.is_finalizing} + 2 0.000 0.000 0.000 0.000 {method 'result' of '_asyncio.Future' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/threading.py:1597(main_thread) + 1 0.000 0.000 0.000 0.000 {method 'clear' of 'collections.deque' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/re/_parser.py:103(checklookbehindgroup) + 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} + 1 0.000 0.000 0.000 0.000 {method 'remove' of 'set' objects} + 1 0.000 0.000 0.000 0.000 {method 'clear' of 'list' objects} + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/_distutils_hack/__init__.py:109() + 1 0.000 0.000 0.000 0.000 /Users/chiemelienwanisobi/miniconda3/envs/openbb/lib/python3.11/site-packages/importlib_metadata/_compat.py:44(find_spec) + + diff --git a/EventDriven/tests/data/run_async_eod_results.csv b/EventDriven/tests/data/run_async_eod_results.csv new file mode 100644 index 0000000..7003273 --- /dev/null +++ b/EventDriven/tests/data/run_async_eod_results.csv @@ -0,0 +1,989 @@ +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +18.4,18.4,18.4,18.4,4,36,18.4,335,18.95,18.674999999999997,18.896630727762805 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,133,18.05,112,18.85,18.450000000000003,18.415714285714287 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,114,16.85,438,18.05,17.450000000000003,17.80217391304348 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +0.0,0.0,0.0,0.0,0,457,15.5,335,18.4,16.95,16.72664141414141 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +17.05,17.1,17.05,17.1,2,27,17.2,396,19.1,18.15,18.97872340425532 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,116,16.65,107,17.65,17.15,17.129820627802687 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,65,13.7,46,16.3,15.0,14.777477477477476 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,93,16.8,20,17.65,17.225,16.950442477876106 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,294,15.0,50,17.85,16.425,15.414244186046512 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,8,19.3,25,19.7,19.5,19.6030303030303 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,67,18.65,29,19.1,18.875,18.785937499999996 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +0.0,0.0,0.0,0.0,0,218,16.75,160,18.25,17.5,17.384920634920633 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +15.34,15.34,15.34,15.34,5,341,13.65,60,15.15,14.4,13.874438902743142 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.9,14.9,14.9,14.9,4,384,12.45,262,15.6,14.024999999999999,13.727554179566564 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +14.2,14.75,13.92,14.75,8,357,12.65,311,16.0,14.325,14.209655688622753 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.6,15.7,15.55,15.7,361,33,15.4,5,15.6,15.5,15.426315789473685 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +15.2,16.4,15.2,16.4,35,422,14.0,452,18.7,16.35,16.43066361556064 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +17.1,17.1,17.1,17.1,1,30,17.65,160,20.5,19.075,20.05 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +0.0,0.0,0.0,0.0,0,107,16.95,139,18.25,17.6,17.684552845528454 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 +16.25,16.25,16.15,16.15,38,391,14.0,65,17.0,15.5,14.427631578947368 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +16.9,16.9,16.9,16.9,5,188,16.75,357,17.45,17.1,17.208532110091742 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +17.9,17.9,17.3,17.3,15,356,16.35,195,17.3,16.825000000000003,16.686206896551727 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,351,15.35,248,16.2,15.774999999999999,15.701919866444072 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +0.0,0.0,0.0,0.0,0,42,16.1,351,16.8,16.450000000000003,16.72519083969466 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +16.05,16.05,16.05,16.05,1,29,15.7,313,16.9,16.299999999999997,16.798245614035086 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +14.9,14.9,14.9,14.9,1,115,15.1,90,16.15,15.625,15.560975609756097 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,254,13.05,10,14.85,13.95,13.118181818181819 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,225,13.5,12,16.15,14.825,13.634177215189872 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +0.0,0.0,0.0,0.0,0,322,15.25,41,16.3,15.775,15.368595041322314 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +17.65,17.65,17.65,17.65,1,7,17.7,172,18.5,18.1,18.46871508379888 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,37,17.1,25,17.55,17.325000000000003,17.281451612903226 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +0.0,0.0,0.0,0.0,0,246,13.95,143,16.7,15.325,14.960925449871464 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.9,13.9,13.82,13.82,3,8,13.6,136,15.75,14.675,15.630555555555556 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +13.6,13.6,13.6,13.6,5,1,13.2,278,14.35,13.774999999999999,14.345878136200716 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +0.0,0.0,0.0,0.0,0,37,13.55,209,16.0,14.775,15.631504065040652 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +14.2,14.3,14.2,14.3,76,34,14.05,299,16.5,15.275,16.24984984984985 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +0.0,0.0,0.0,0.0,0,93,14.95,480,16.35,15.65,16.122774869109946 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +16.05,16.05,16.05,16.05,1,25,16.25,163,19.0,17.625,18.6343085106383 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +15.99,15.99,15.99,15.99,1,103,15.7,188,18.3,17.0,17.379725085910653 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 +14.75,14.85,14.75,14.75,125,5,14.95,397,17.5,16.225,17.46828358208955 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,38,15.35,46,15.85,15.6,15.623809523809523 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,189,14.9,39,15.75,15.325,15.045394736842105 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,26,14.2,280,15.1,14.649999999999999,15.023529411764704 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +0.0,0.0,0.0,0.0,0,190,14.6,127,15.2,14.899999999999999,14.840378548895899 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.15,14.15,14.1,14.15,4,127,14.25,415,15.1,14.675,14.900830258302582 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +14.55,14.55,14.5,14.5,9,36,13.9,7,14.3,14.100000000000001,13.965116279069768 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,109,13.0,127,13.55,13.275,13.295974576271188 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,74,13.9,7,14.75,14.325,13.973456790123457 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,19,14.6,62,14.9,14.75,14.829629629629629 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,7,16.15,26,16.5,16.325,16.425757575757576 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,64,15.6,26,15.95,15.774999999999999,15.70111111111111 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +0.0,0.0,0.0,0.0,0,169,13.0,40,14.75,13.875,13.334928229665072 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +13.0,13.0,12.8,12.8,3,82,10.85,278,14.3,12.575,13.514166666666668 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +11.84,12.35,11.84,12.35,13,5,12.0,320,13.55,12.775,13.526153846153846 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.2,12.35,12.2,12.3,7,34,12.3,49,12.5,12.4,12.418072289156626 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +12.9,13.1,12.75,12.75,60,25,12.8,250,14.6,13.7,14.436363636363636 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +13.7,13.7,13.7,13.7,1,468,11.5,239,14.6,13.05,12.547949080622349 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.05,15.53,14.05,15.25,364,12,14.75,38,16.6,15.675,16.156000000000002 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +14.6,14.6,14.5,14.5,3,42,14.45,161,17.0,15.725,16.47241379310345 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 +13.45,13.5,13.4,13.45,147,32,13.55,257,15.05,14.3,14.883910034602076 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,95,14.0,64,14.45,14.225,14.181132075471698 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +0.0,0.0,0.0,0.0,0,15,13.8,275,14.55,14.175,14.511206896551725 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.2,13.2,13.2,13.2,1,254,12.75,375,13.6,13.175,13.256756756756758 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +13.5,13.5,13.5,13.5,7,343,13.1,10,13.85,13.475,13.121246458923512 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,53,12.95,174,13.5,13.225,13.3715859030837 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +0.0,0.0,0.0,0.0,0,42,12.8,248,15.3,14.05,14.937931034482759 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +12.1,12.1,12.1,12.1,2,20,12.0,46,12.3,12.15,12.20909090909091 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +0.0,0.0,0.0,0.0,0,236,10.5,6,13.45,11.975,10.573140495867769 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +13.0,13.0,12.85,12.95,9,56,13.25,169,13.55,13.4,13.475333333333333 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +14.5,14.5,14.5,14.5,2,29,14.75,37,14.95,14.85,14.86212121212121 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +0.0,0.0,0.0,0.0,0,260,12.9,387,16.45,14.675,15.02341576506955 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +13.15,13.15,13.15,13.15,2,10,13.1,291,16.0,14.55,15.903654485049834 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +11.36,11.36,11.35,11.35,5,77,11.15,30,11.35,11.25,11.20607476635514 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.13,10.85,11.0,47,28,10.85,360,12.5,11.675,12.380927835051546 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +10.85,11.25,10.53,11.25,295,159,9.0,135,12.9,10.95,10.790816326530614 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.8,11.9,11.7,11.8,20,35,11.6,185,12.9,12.25,12.693181818181818 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +11.5,12.53,11.5,12.53,11,471,10.0,382,15.0,12.5,12.239155920281359 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +12.65,13.95,12.65,13.95,13,30,13.45,58,16.0,14.725,15.130681818181817 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +0.0,0.0,0.0,0.0,0,82,13.1,137,13.85,13.475,13.56917808219178 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 +12.2,12.25,12.2,12.25,95,90,12.3,305,13.5,12.9,13.226582278481013 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,466,12.5,80,13.2,12.85,12.602564102564102 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +0.0,0.0,0.0,0.0,0,209,12.45,201,13.1,12.774999999999999,12.768658536585365 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +12.2,12.2,12.2,12.2,20,42,11.75,375,12.4,12.075,12.33453237410072 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +0.0,0.0,0.0,0.0,0,85,12.05,357,12.8,12.425,12.65576923076923 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +11.6,11.6,11.6,11.6,1,64,11.75,526,14.2,12.975,13.934237288135593 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,7,11.6,60,12.2,11.899999999999999,12.137313432835821 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +0.0,0.0,0.0,0.0,0,162,10.55,86,11.15,10.850000000000001,10.758064516129032 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +12.3,12.3,12.3,12.3,1,107,11.35,19,12.25,11.8,11.485714285714284 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +11.93,11.93,11.93,11.93,1,271,10.05,140,12.3,11.175,10.816423357664235 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.01,13.4,13.01,13.4,319,31,13.4,36,13.6,13.5,13.507462686567164 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +13.29,13.29,13.29,13.29,1,41,12.9,41,13.1,13.0,13.0 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,204,9.9,34,12.1,11.0,10.214285714285714 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +0.0,0.0,0.0,0.0,0,56,10.1,138,12.25,11.175,11.629381443298968 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.1,10.1,9.95,9.95,47,24,9.8,101,10.95,10.375,10.7292 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.05,10.21,10.05,10.21,50,194,8.5,35,10.3,9.4,8.775109170305676 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +10.65,10.8,10.55,10.55,54,177,8.45,153,12.25,10.35,10.211818181818181 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +0.0,0.0,0.0,0.0,0,487,9.0,403,14.0,11.5,11.264044943820224 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.35,11.45,11.35,11.45,12,30,12.2,55,15.0,13.6,14.011764705882353 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.8,11.8,11.8,11.8,56,134,11.85,229,14.5,13.175,13.52176308539945 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 +11.05,11.1,10.95,10.95,36,185,11.0,450,13.5,12.25,12.771653543307085 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +10.7,11.1,10.7,11.1,94,3,11.55,165,12.05,11.8,12.04107142857143 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.35,11.62,11.35,11.62,4,101,11.3,67,11.9,11.600000000000001,11.539285714285715 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.05,11.05,10.85,10.85,21,451,8.95,101,11.15,10.05,9.352536231884057 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +11.25,11.25,11.25,11.25,1,69,10.95,470,11.45,11.2,11.38599257884972 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +10.35,10.35,10.35,10.35,10,105,10.6,395,11.4,11.0,11.232 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +11.04,11.04,10.94,10.94,2,27,10.45,87,10.95,10.7,10.83157894736842 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +9.93,9.93,9.85,9.85,1155,148,9.5,112,10.15,9.825,9.780000000000001 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +11.3,11.4,10.85,10.85,2006,210,9.25,5,11.1,10.175,9.293023255813953 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +0.0,0.0,0.0,0.0,0,26,10.9,66,11.15,11.025,11.079347826086956 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +11.86,12.26,11.86,12.26,3,7,12.1,148,12.35,12.225,12.338709677419354 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +0.0,0.0,0.0,0.0,0,64,11.65,36,11.85,11.75,11.722000000000001 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +11.0,11.0,11.0,11.0,2,20,10.65,212,12.3,11.475000000000001,12.157758620689657 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.52,9.52,9.15,9.21,6,43,9.1,32,9.3,9.2,9.185333333333334 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +9.2,9.2,8.93,9.0,135,42,8.85,230,10.0,9.425,9.822426470588235 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +8.75,9.15,8.75,9.15,8,54,9.1,9,9.25,9.175,9.12142857142857 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.65,9.65,9.64,9.65,105,150,7.45,187,11.7,9.575,9.808308605341246 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +9.45,10.25,9.45,10.25,10,377,8.0,220,10.9,9.45,9.068676716917924 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.4,11.6,10.4,11.6,9,30,10.95,50,12.25,11.6,11.7625 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.65,10.95,10.65,10.88,16,110,10.7,20,11.1,10.899999999999999,10.76153846153846 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 +10.0,10.2,10.0,10.2,89,188,9.9,367,12.25,11.075,11.453963963963965 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +10.5,10.5,10.5,10.5,10,3,10.35,87,10.95,10.649999999999999,10.93 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +11.0,11.0,10.2,10.2,456,9,10.25,22,10.8,10.525,10.640322580645162 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,48,9.6,237,10.2,9.899999999999999,10.098947368421053 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +0.0,0.0,0.0,0.0,0,45,9.9,82,10.35,10.125,10.19055118110236 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +9.45,9.63,9.4,9.6,7,31,9.55,395,10.25,9.9,10.199061032863849 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,245,9.05,47,9.85,9.45,9.178767123287672 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,188,8.5,177,9.1,8.8,8.790958904109589 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,53,8.4,14,10.1,9.25,8.755223880597015 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +0.0,0.0,0.0,0.0,0,26,9.85,56,10.05,9.95,9.98658536585366 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +10.85,10.85,10.85,10.85,1,27,10.95,43,11.15,11.05,11.072857142857144 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,89,10.5,44,10.7,10.6,10.566165413533835 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +0.0,0.0,0.0,0.0,0,37,9.55,83,9.85,9.7,9.7575 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.3,8.3,8.3,8.3,1,155,8.15,29,8.35,8.25,8.181521739130435 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +8.12,8.2,8.1,8.1,24,59,7.95,343,9.95,8.95,9.65646766169154 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +0.0,0.0,0.0,0.0,0,130,8.2,66,8.4,8.3,8.267346938775509 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +8.7,8.7,8.55,8.55,38,48,8.55,182,9.9,9.225000000000001,9.618260869565217 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +0.0,0.0,0.0,0.0,0,522,7.0,343,11.5,9.25,8.784393063583815 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +9.45,9.45,9.45,9.45,7,30,9.9,54,12.2,11.05,11.37857142857143 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +0.0,0.0,0.0,0.0,0,49,9.65,226,12.0,10.825,11.581272727272728 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 +8.95,9.15,8.95,9.15,51,102,8.9,428,10.95,9.925,10.555471698113207 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +9.5,9.65,9.45,9.65,163,70,9.35,36,9.75,9.55,9.485849056603772 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +10.2,10.2,10.2,10.2,1,22,9.25,210,9.8,9.525,9.747844827586208 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +0.0,0.0,0.0,0.0,0,372,6.7,3,8.95,7.824999999999999,6.718 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +8.85,9.05,8.75,8.94,4,590,8.7,396,9.4,9.05,8.981135902636918 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +0.0,0.0,0.0,0.0,0,462,8.2,470,9.75,8.975,8.981652360515021 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.7,8.7,8.7,8.7,2,102,8.25,58,8.8,8.525,8.449375 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +8.0,8.0,8.0,8.0,1,317,6.4,159,8.2,7.3,7.00126050420168 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +9.05,9.3,8.95,9.0,246,97,7.7,56,11.0,9.35,8.9078431372549 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +8.7,8.7,8.6,8.65,87,218,6.8,326,10.4,8.6,8.95735294117647 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,10.0,9.8,10.0,11,23,9.9,33,10.05,9.975000000000001,9.988392857142859 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.8,9.8,9.7,9.7,11,92,9.45,77,9.65,9.55,9.54112426035503 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +9.0,9.0,8.75,8.75,44,104,8.1,43,10.2,9.149999999999999,8.714285714285714 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +8.1,8.1,8.1,8.1,4,182,7.3,30,7.5,7.4,7.3283018867924525 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +7.3,7.3,7.25,7.25,19,10,7.15,20,7.35,7.25,7.283333333333333 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +0.0,0.0,0.0,0.0,0,44,7.35,74,7.55,7.449999999999999,7.47542372881356 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +7.8,7.8,7.65,7.75,29,36,7.65,160,9.85,8.75,9.445918367346938 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.29,8.29,8.27,8.27,2,197,6.15,1,8.5,7.325,6.161868686868687 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +8.4,8.5,8.4,8.5,7,33,8.85,70,11.5,10.175,10.65097087378641 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +0.0,0.0,0.0,0.0,0,69,8.65,205,11.0,9.825,10.408211678832117 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 +8.15,8.18,8.0,8.12,30,98,8.05,326,10.25,9.15,9.741509433962264 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +8.6,8.6,8.55,8.55,156,84,8.6,642,10.05,9.325,9.882231404958679 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,7,8.3,224,8.9,8.600000000000001,8.881818181818183 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +0.0,0.0,0.0,0.0,0,37,7.75,396,8.4,8.075,8.34445727482679 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +8.13,8.13,8.13,8.13,3,190,7.9,556,8.5,8.2,8.347184986595174 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,339,7.55,221,8.1,7.824999999999999,7.767053571428571 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +0.0,0.0,0.0,0.0,0,32,7.6,7,7.85,7.725,7.6448717948717935 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +7.55,7.55,7.23,7.23,2,176,6.6,118,7.35,6.975,6.901020408163265 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,54,7.2,54,10.1,8.65,8.65 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +0.0,0.0,0.0,0.0,0,134,7.2,97,8.2,7.699999999999999,7.61991341991342 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.9,8.95,8.9,8.95,31,21,8.9,27,9.05,8.975000000000001,8.984375 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +8.65,8.65,8.65,8.65,54,81,8.5,21,8.65,8.575,8.530882352941177 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.8,7.8,7.8,7.8,30,112,7.2,66,7.9,7.550000000000001,7.459550561797753 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +7.2,7.2,6.6,6.72,37,56,6.55,32,6.75,6.65,6.622727272727273 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.5,6.55,6.5,6.55,16,91,6.4,31,6.6,6.5,6.450819672131148 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +6.55,6.65,6.55,6.65,4,26,6.6,103,6.8,6.699999999999999,6.75968992248062 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +7.0,7.0,6.9,6.95,181,34,6.9,221,8.65,7.775,8.416666666666668 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +6.8,6.95,6.8,6.95,54,221,5.9,247,9.55,7.7250000000000005,7.826388888888889 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +7.55,8.45,7.55,8.45,10,33,8.05,50,10.25,9.15,9.375301204819278 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +0.0,0.0,0.0,0.0,0,103,7.75,240,9.05,8.4,8.659620991253645 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 +7.3,7.3,7.3,7.3,5,109,7.2,440,9.1,8.15,8.722768670309653 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +7.25,8.0,7.2,7.7,570,3,7.7,21,7.95,7.825,7.91875 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +8.0,8.05,7.5,7.55,2612,48,7.35,26,8.0,7.675,7.578378378378378 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.3,7.3,7.3,7.3,1,39,6.95,434,7.6,7.275,7.5464059196617335 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +7.4,7.4,7.25,7.35,20,30,7.2,96,7.5,7.35,7.428571428571428 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +6.95,6.95,6.8,6.95,148,28,6.95,181,7.85,7.4,7.729425837320574 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +7.0,7.05,7.0,7.05,12,32,6.65,17,7.1,6.875,6.8061224489795915 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +6.54,6.54,6.47,6.47,6,103,6.05,85,6.75,6.4,6.366489361702127 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.27,7.35,7.15,7.2,31,37,7.05,1,7.3,7.175,7.056578947368421 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +7.05,7.05,7.0,7.0,157,24,7.15,87,7.35,7.25,7.306756756756757 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +8.0,8.11,7.95,8.05,45,40,8.0,47,8.15,8.075,8.081034482758621 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +7.75,7.75,7.7,7.7,13,188,7.6,34,7.75,7.675,7.622972972972973 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.8,6.85,6.8,6.85,7,49,6.85,54,7.05,6.949999999999999,6.954854368932039 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +6.0,6.0,6.0,6.0,10,56,5.85,1,6.0,5.925,5.852631578947368 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.88,5.9,5.85,5.85,9,18,5.75,26,5.9,5.825,5.838636363636365 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +5.6,5.9,5.6,5.9,8,32,5.85,72,6.1,5.975,6.023076923076922 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.2,6.3,6.15,6.19,155,21,6.15,122,7.4,6.775,7.216433566433566 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.3,6.7,6.3,6.52,112,430,4.0,162,7.6,5.8,4.985135135135135 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +6.45,7.0,6.45,7.0,90,30,7.2,52,9.85,8.525,8.88048780487805 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +7.2,7.2,6.9,7.05,548,190,6.9,40,7.2,7.050000000000001,6.952173913043478 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 +6.45,6.56,6.45,6.45,135,113,6.4,187,8.1,7.25,7.459666666666666 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.4,6.95,6.4,6.9,138,228,6.85,530,7.75,7.3,7.479287598944591 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +6.85,6.85,6.85,6.85,200,452,6.35,179,7.2,6.775,6.591125198098256 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +0.0,0.0,0.0,0.0,0,191,5.85,2,6.4,6.125,5.855699481865285 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +6.7,6.7,6.45,6.6,29,184,6.4,15,6.65,6.525,6.418844221105528 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +0.0,0.0,0.0,0.0,0,320,5.8,243,6.7,6.25,6.188454706927176 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +6.45,6.45,6.35,6.35,6,97,5.9,47,6.35,6.125,6.046875 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +5.85,5.85,5.8,5.85,6,26,5.75,56,5.9,5.825,5.852439024390245 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.65,6.65,6.4,6.4,5,59,5.2,27,6.6,5.9,5.6395348837209305 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +6.4,6.4,6.25,6.25,41,20,6.4,95,6.6,6.5,6.565217391304348 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +7.16,7.2,7.16,7.2,21,57,7.15,51,7.3,7.225,7.220833333333333 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +0.0,0.0,0.0,0.0,0,50,6.8,40,6.95,6.875,6.866666666666667 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +6.0,6.1,6.0,6.1,3,106,6.15,80,6.3,6.225,6.214516129032258 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.3,5.3,5.27,5.27,2,297,3.25,320,6.1,4.675,4.728119935170178 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.2,5.22,5.2,5.2,10,91,5.1,30,5.3,5.199999999999999,5.149586776859504 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.3,5.35,5.3,5.35,7,22,5.3,82,5.45,5.375,5.418269230769231 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.5,5.7,5.49,5.5,327,35,5.5,105,6.5,6.0,6.25 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +5.95,5.95,5.95,5.95,14,431,3.5,556,8.5,6.0,6.31661600810537 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +6.0,6.75,6.0,6.75,15,28,6.3,65,9.0,7.65,8.187096774193549 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +0.0,0.0,0.0,0.0,0,141,6.15,102,8.5,7.325,7.13641975308642 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 +5.85,5.85,5.85,5.85,5,108,5.7,226,6.25,5.975,6.072155688622755 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.2,6.3,6.15,6.3,173,338,6.05,44,6.45,6.25,6.096073298429319 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +6.52,6.55,6.05,6.05,354,276,5.9,3,6.3,6.1,5.904301075268817 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.75,5.8,5.65,5.7,4,28,5.5,5,5.85,5.675,5.553030303030304 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.83,6.0,5.83,6.0,14,51,5.65,212,6.15,5.9,6.0530418250950575 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.5,5.6,5.45,5.45,7,109,5.45,390,6.05,5.75,5.918937875751503 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.7,5.7,5.6,5.6,2,7,5.5,47,5.95,5.725,5.891666666666667 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +5.15,5.15,5.15,5.15,300,55,5.15,90,5.35,5.25,5.274137931034483 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +0.0,0.0,0.0,0.0,0,58,4.2,26,5.95,5.075,4.741666666666667 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +5.59,5.6,5.59,5.6,2,50,5.65,61,5.9,5.775,5.787387387387388 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.0,6.45,6.0,6.45,19,20,4.35,94,6.55,5.449999999999999,6.164035087719299 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +6.3,6.3,6.15,6.15,4,122,6.05,40,6.2,6.125,6.087037037037037 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +5.4,5.7,5.4,5.45,4,75,5.45,51,5.6,5.525,5.510714285714286 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.85,4.85,4.65,4.75,4,72,4.6,46,4.8,4.699999999999999,4.677966101694915 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.58,4.67,4.58,4.67,8,16,4.55,27,4.7,4.625,4.644186046511628 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.6,4.75,4.6,4.75,14,46,4.7,79,4.85,4.775,4.7948 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +4.9,5.05,4.9,4.9,41,30,4.9,246,6.8,5.85,6.593478260869564 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.0,5.37,5.0,5.37,24,411,3.0,515,8.0,5.5,5.780777537796976 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.35,6.05,5.35,5.95,83,23,5.7,42,7.7,6.7,6.992307692307693 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.5,5.8,5.5,5.55,62,215,5.45,59,5.75,5.6,5.514598540145985 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 +5.05,5.15,5.05,5.15,18,47,5.05,41,5.2,5.125,5.119886363636363 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.6,5.6,5.55,5.55,108,3,5.5,1,5.7,5.6,5.55 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +5.73,5.9,5.73,5.9,2,187,5.25,206,5.7,5.475,5.485877862595419 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +0.0,0.0,0.0,0.0,0,37,4.9,134,5.35,5.125,5.252631578947368 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.3,5.3,5.1,5.1,277,65,5.1,90,5.5,5.3,5.332258064516129 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +5.02,5.02,4.95,4.95,15,142,4.85,157,5.2,5.025,5.033779264214047 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,59,4.85,35,5.3,5.074999999999999,5.017553191489361 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +0.0,0.0,0.0,0.0,0,35,4.6,61,4.85,4.725,4.758854166666666 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +5.15,5.2,5.15,5.2,18,201,2.99,56,6.85,4.92,3.8310894941634244 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +0.0,0.0,0.0,0.0,0,31,5.1,48,5.25,5.175,5.191139240506329 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.65,5.75,5.65,5.75,270,17,5.7,1,5.8,5.75,5.705555555555556 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +5.55,5.55,5.53,5.53,2,26,5.4,270,5.55,5.475,5.536824324324324 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.85,4.85,4.8,4.8,8,99,4.8,44,4.95,4.875,4.846153846153846 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.4,4.4,4.15,4.17,8,72,4.1,80,4.25,4.175,4.178947368421053 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.05,4.15,4.05,4.15,51,87,4.0,38,4.2,4.1,4.0607999999999995 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.2,4.2,4.2,4.2,4,321,4.15,73,4.3,4.225,4.177791878172589 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.5,4.35,4.4,133,95,4.35,237,6.5,5.425,5.884789156626505 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +4.4,4.72,4.4,4.6,104,67,4.65,1,4.85,4.75,4.652941176470589 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +5.0,5.35,5.0,5.25,21,30,5.05,71,7.5,6.275,6.772277227722772 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +0.0,0.0,0.0,0.0,0,143,4.85,59,5.1,4.975,4.923019801980198 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 +4.5,4.5,4.5,4.5,8,278,2.38,275,5.0,3.69,3.6828933092224228 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +4.6,5.01,4.45,5.0,11,224,4.85,1,5.2,5.025,4.851555555555556 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +5.26,5.26,4.81,4.85,728,1,4.7,1,5.05,4.875,4.875 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +4.85,4.85,4.65,4.65,1252,37,4.35,218,4.75,4.55,4.691960784313725 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +0.0,0.0,0.0,0.0,0,32,4.5,1,4.75,4.625,4.507575757575757 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +4.3,4.45,4.3,4.4,14,72,4.35,106,4.65,4.5,4.528651685393259 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +3.0,4.65,3.0,4.5,6,70,4.3,30,4.7,4.5,4.42 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.15,4.15,4.05,4.15,3,8,4.1,52,4.2,4.15,4.1866666666666665 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.34,4.78,4.34,4.6,4003,209,3.0,26,4.75,3.875,3.1936170212765953 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +4.5,4.51,4.45,4.45,10,23,4.55,119,4.7,4.625,4.675704225352113 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +5.0,5.15,4.8,5.15,13,56,5.05,75,5.2,5.125,5.13587786259542 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.95,4.95,4.85,4.86,1254,167,4.75,161,5.9,5.325,5.314481707317073 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +4.5,4.5,4.35,4.35,5,111,4.25,66,4.4,4.325,4.305932203389831 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.79,3.79,3.6,3.65,11,266,3.6,50,3.8,3.7,3.6316455696202534 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.54,3.54,3.54,3.54,1,70,3.55,48,3.75,3.65,3.63135593220339 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.45,3.7,3.45,3.7,7,102,3.7,107,3.85,3.7750000000000004,3.776794258373206 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +3.85,3.95,3.85,3.9,22,159,3.85,199,4.5,4.175,4.211312849162011 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.15,4.2,4.13,4.13,13,102,4.1,87,4.35,4.225,4.215079365079364 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.25,4.7,4.1,4.55,926,23,4.35,58,6.65,5.5,5.996913580246913 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +4.55,4.55,4.4,4.4,5,89,4.3,53,4.5,4.4,4.374647887323944 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 +3.99,4.05,3.95,4.05,39,232,2.2,288,6.1,4.15,4.36 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +4.5,4.5,4.5,4.5,23,321,4.4,2,4.75,4.575,4.402167182662539 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,277,4.2,199,4.65,4.425000000000001,4.388130252100841 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,160,3.9,243,4.35,4.125,4.171339950372208 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,53,4.1,160,4.45,4.275,4.362910798122066 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +0.0,0.0,0.0,0.0,0,242,3.8,308,4.35,4.074999999999999,4.108 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +4.15,4.15,4.15,4.15,1,8,3.95,103,4.9,4.425000000000001,4.831531531531532 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,59,3.7,98,3.95,3.825,3.8560509554140125 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,58,3.0,27,4.35,3.675,3.4288235294117646 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +0.0,0.0,0.0,0.0,0,24,4.15,21,4.25,4.2,4.196666666666667 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +4.45,4.45,4.45,4.45,1,79,4.55,123,4.75,4.65,4.671782178217821 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +0.0,0.0,0.0,0.0,0,101,4.3,15,4.45,4.375,4.319396551724138 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +3.9,3.9,3.85,3.85,2,145,3.85,144,4.0,3.925,3.9247404844290656 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +0.0,0.0,0.0,0.0,0,275,3.25,76,3.45,3.35,3.2933048433048433 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +3.3,3.3,3.3,3.3,3,182,3.2,49,3.4,3.3,3.2424242424242427 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +0.0,0.0,0.0,0.0,0,155,3.35,96,3.5,3.425,3.407370517928287 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +3.5,3.55,3.5,3.5,60,62,3.5,23,3.65,3.575,3.540588235294117 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +0.0,0.0,0.0,0.0,0,141,3.0,299,6.5,4.75,5.3784090909090905 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +4.25,4.3,4.25,4.3,4,26,4.05,58,6.25,5.15,5.56904761904762 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +0.0,0.0,0.0,0.0,0,56,3.9,60,4.1,4.0,4.003448275862069 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 +3.6,3.6,3.6,3.6,3,61,3.55,29,3.7,3.625,3.5983333333333336 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +4.4,4.4,4.4,4.4,28,290,4.3,9,4.65,4.475,4.3105351170568555 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,486,3.65,156,4.55,4.1,3.8686915887850466 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,427,2.41,284,5.5,3.955,3.6442616033755275 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,542,2.61,334,4.35,3.4799999999999995,3.273424657534246 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,379,2.97,169,4.15,3.5600000000000005,3.3339051094890513 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,47,3.85,36,4.2,4.025,4.001807228915663 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,7,3.65,26,3.75,3.7,3.728787878787879 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +0.0,0.0,0.0,0.0,0,77,1.95,84,6.2,4.075,4.167391304347826 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +3.95,3.95,3.95,3.95,1,35,4.05,22,4.15,4.1,4.088596491228071 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.5,4.5,4.5,4.5,15,53,4.45,177,4.65,4.550000000000001,4.603913043478261 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +4.35,4.35,4.35,4.35,7,118,4.2,26,4.35,4.275,4.227083333333334 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +0.0,0.0,0.0,0.0,0,110,3.75,200,3.9,3.825,3.846774193548387 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.3,3.3,3.3,3.3,1,54,3.2,41,3.35,3.2750000000000004,3.264736842105263 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +3.25,3.25,3.2,3.2,7,46,3.15,58,3.3,3.2249999999999996,3.233653846153846 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +0.0,0.0,0.0,0.0,0,287,3.25,196,3.4,3.325,3.3108695652173914 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +3.4,3.45,3.4,3.45,13,245,3.4,86,3.55,3.4749999999999996,3.438972809667674 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +0.0,0.0,0.0,0.0,0,311,1.0,39,3.85,2.425,1.3175714285714286 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +4.0,4.15,4.0,4.15,2,21,3.95,63,6.5,5.225,5.8625 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +0.0,0.0,0.0,0.0,0,76,3.75,61,4.0,3.875,3.8613138686131387 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 +3.55,3.55,3.55,3.55,1,97,3.45,304,5.0,4.225,4.625062344139651 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,284,4.0,409,4.75,4.375,4.442640692640692 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,173,3.8,292,4.25,4.025,4.082580645161291 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,111,3.55,76,3.9,3.7249999999999996,3.692245989304813 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,30,3.7,225,5.15,4.425000000000001,4.979411764705882 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,148,3.45,157,3.95,3.7,3.7073770491803284 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +0.0,0.0,0.0,0.0,0,7,3.6,73,4.4,4.0,4.330000000000001 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.5,3.5,3.3,3.45,70,63,3.35,31,3.5,3.425,3.399468085106383 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.8,3.8,3.8,3.8,1,28,3.65,27,3.95,3.8,3.797272727272727 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +3.65,3.65,3.65,3.65,2,45,3.75,127,3.9,3.825,3.860755813953488 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.02,4.2,4.02,4.2,4,46,4.15,30,4.3,4.225,4.20921052631579 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +4.0,4.0,4.0,4.0,3,94,3.9,21,4.05,3.9749999999999996,3.927391304347826 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +0.0,0.0,0.0,0.0,0,132,3.45,54,3.6,3.5250000000000004,3.4935483870967747 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,3.0,3.0,3,37,2.98,82,3.1,3.04,3.062689075630252 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +3.0,3.0,2.99,2.99,7,45,2.92,22,3.05,2.985,2.9626865671641793 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +0.0,0.0,0.0,0.0,0,15,3.05,156,3.15,3.0999999999999996,3.1412280701754383 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +3.2,3.2,3.2,3.2,37,201,3.15,86,3.3,3.2249999999999996,3.1949477351916373 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +0.0,0.0,0.0,0.0,0,365,1.0,261,4.0,2.5,2.2507987220447285 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.3,3.85,3.3,3.85,7,17,3.65,12,4.0,3.825,3.794827586206896 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.58,3.58,3.58,3.58,1,105,3.5,64,3.7,3.6,3.5757396449704144 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 +3.45,3.45,3.2,3.25,504,60,3.2,72,3.5,3.35,3.3636363636363633 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,4.0,4.0,5,97,3.8,1,4.05,3.925,3.802551020408163 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +4.0,4.0,3.72,3.72,401,100,3.55,24,4.05,3.8,3.646774193548387 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +3.65,3.65,3.6,3.6,60,62,3.35,67,3.85,3.6,3.6096899224806203 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +0.0,0.0,0.0,0.0,0,65,3.35,4,3.6,3.475,3.3644927536231886 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +3.45,3.7,3.4,3.7,3532,72,3.25,79,3.8,3.525,3.537748344370861 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +0.0,0.0,0.0,0.0,0,38,3.4,26,3.7,3.55,3.5218749999999996 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +3.5,3.5,3.5,3.5,1,37,3.2,55,3.4,3.3,3.3195652173913044 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +0.0,0.0,0.0,0.0,0,77,2.95,25,3.75,3.35,3.146078431372549 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.49,3.6,3.49,3.6,4,30,3.55,57,3.7,3.625,3.6482758620689655 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +3.8,4.02,3.8,4.0,514,22,3.95,30,4.1,4.025,4.036538461538461 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,85,3.7,26,3.85,3.7750000000000004,3.735135135135135 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,72,3.3,59,3.45,3.375,3.367557251908397 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +0.0,0.0,0.0,0.0,0,18,2.82,15,2.93,2.875,2.8699999999999997 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.85,2.87,2.83,2.83,33,22,2.78,31,2.9,2.84,2.850188679245283 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +2.82,2.87,2.82,2.87,2,20,2.89,26,2.99,2.9400000000000004,2.946521739130435 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +3.0,3.05,3.0,3.04,41,51,3.0,7,3.15,3.075,3.0181034482758617 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +2.96,3.28,2.96,3.25,18,44,3.2,1,3.35,3.2750000000000004,3.2033333333333336 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.55,3.75,3.55,3.75,13,25,3.35,57,6.0,4.675,5.192073170731707 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.4,3.45,3.35,3.35,5,40,3.35,58,3.5,3.425,3.4387755102040813 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 +3.1,3.11,3.1,3.1,160,113,3.05,41,3.2,3.125,3.0899350649350645 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +3.45,3.45,3.45,3.45,7,114,3.6,94,4.0,3.8,3.780769230769231 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,131,2.45,94,4.7,3.575,3.39 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +0.0,0.0,0.0,0.0,0,141,1.45,91,4.15,2.8000000000000003,2.509051724137931 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.35,3.45,3.3,3.45,4,150,1.87,24,3.75,2.81,2.1293103448275863 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +3.15,3.2,3.15,3.2,13,136,1.48,102,4.15,2.8150000000000004,2.6242857142857146 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,7,3.25,129,4.7,3.975,4.625367647058823 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,22,3.05,47,3.2,3.125,3.152173913043478 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,71,1.32,20,3.65,2.485,1.8320879120879119 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +0.0,0.0,0.0,0.0,0,26,3.4,49,3.55,3.4749999999999996,3.498 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +3.8,3.8,3.8,3.8,34,27,3.75,31,3.9,3.825,3.830172413793103 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +0.0,0.0,0.0,0.0,0,85,3.55,20,3.65,3.5999999999999996,3.569047619047619 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +3.23,3.23,3.23,3.23,1,40,3.1,28,3.25,3.175,3.1617647058823533 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +0.0,0.0,0.0,0.0,0,40,2.68,40,2.8,2.74,2.74 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +2.73,2.73,2.69,2.69,7,38,2.64,28,2.76,2.7,2.6909090909090914 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +0.0,0.0,0.0,0.0,0,28,2.74,32,2.84,2.79,2.7933333333333334 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +2.9,2.91,2.89,2.89,9,22,2.87,7,2.98,2.925,2.896551724137931 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,33,3.05,58,3.25,3.15,3.177472527472527 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +0.0,0.0,0.0,0.0,0,28,3.2,37,5.9,4.550000000000001,4.736923076923077 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.23,3.23,3.23,3.23,1,47,3.15,68,3.35,3.25,3.2682608695652178 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 +3.1,3.1,2.94,2.94,23,180,0.81,33,3.0,1.905,1.1492957746478873 + + +Open,High,Low,Close,Volume,Bid_size,CloseBid,Ask_size,CloseAsk,Midpoint,Weighted_midpoint +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +3.4,3.4,3.4,3.4,8,106,3.3,85,4.05,3.675,3.6337696335078533 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,39,3.1,20,3.45,3.2750000000000004,3.2186440677966104 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,90,2.54,69,3.8,3.17,3.0867924528301884 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +0.0,0.0,0.0,0.0,0,143,2.14,20,3.4,2.77,2.294601226993865 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +2.92,3.53,2.92,2.93,4,68,2.76,68,3.3,3.03,3.03 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +0.0,0.0,0.0,0.0,0,37,2.75,78,4.95,3.85,4.242173913043478 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +2.81,2.81,2.79,2.79,2,53,2.73,71,2.94,2.835,2.8502419354838713 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,72,2.1,75,5.25,3.675,3.7071428571428573 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +0.0,0.0,0.0,0.0,0,46,3.0,65,3.25,3.125,3.1463963963963963 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.39,3.39,3.39,3.39,1,187,2.1,66,5.6,3.8499999999999996,3.0130434782608693 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +3.5,3.5,3.5,3.5,9,134,3.2,34,3.3,3.25,3.2202380952380953 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.89,2.89,2.89,2.89,1,27,2.81,60,5.0,3.9050000000000002,4.320344827586207 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.49,2.49,2.49,2.49,2,26,2.42,33,2.53,2.4749999999999996,2.4815254237288134 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +2.44,2.45,2.44,2.45,11,38,2.38,22,2.5,2.44,2.424 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +0.0,0.0,0.0,0.0,0,30,2.47,17,2.57,2.52,2.5061702127659577 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.6,2.63,2.6,2.61,101,22,2.58,13,2.67,2.625,2.6134285714285714 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +2.7,2.7,2.7,2.7,1,14,2.77,81,5.5,4.135,5.097684210526316 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +3.0,3.05,3.0,3.05,6,25,2.89,37,5.15,4.0200000000000005,4.238709677419355 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +0.0,0.0,0.0,0.0,0,25,2.84,69,3.05,2.945,2.9941489361702125 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 +2.67,2.67,2.67,2.67,5,115,1.5,69,4.75,3.125,2.71875 + + diff --git a/EventDriven/tests/data/run_async_oi_results.csv b/EventDriven/tests/data/run_async_oi_results.csv new file mode 100644 index 0000000..3f423d0 --- /dev/null +++ b/EventDriven/tests/data/run_async_oi_results.csv @@ -0,0 +1,529 @@ +Open_interest,Date,time,Datetime +1399,20230626,06:30:00,2023-06-26 +1399,20230627,06:30:00,2023-06-27 +1399,20230628,06:30:01,2023-06-28 +1399,20230629,06:30:00,2023-06-29 +1399,20230630,06:30:00,2023-06-30 +1399,20230703,06:30:01,2023-07-03 +1399,20230705,06:30:00,2023-07-05 +1399,20230706,06:30:01,2023-07-06 +1399,20230707,06:30:00,2023-07-07 +1399,20230710,06:30:01,2023-07-10 +1399,20230711,06:30:01,2023-07-11 +1399,20230712,06:30:01,2023-07-12 +1399,20230713,06:30:01,2023-07-13 +1404,20230714,06:30:00,2023-07-14 +1406,20230717,06:30:00,2023-07-17 +1412,20230718,06:30:00,2023-07-18 +1356,20230719,06:30:00,2023-07-19 +1351,20230720,06:30:00,2023-07-20 +1352,20230721,06:30:00,2023-07-21 +1352,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1912,20230626,06:30:00,2023-06-26 +1912,20230627,06:30:00,2023-06-27 +1910,20230628,06:30:01,2023-06-28 +1910,20230629,06:30:00,2023-06-29 +1910,20230630,06:30:01,2023-06-30 +1910,20230703,06:30:00,2023-07-03 +1909,20230705,06:30:01,2023-07-05 +1909,20230706,06:30:00,2023-07-06 +1909,20230707,06:30:01,2023-07-07 +1909,20230710,06:30:00,2023-07-10 +1909,20230711,06:30:00,2023-07-11 +1909,20230712,06:30:00,2023-07-12 +1909,20230713,06:30:01,2023-07-13 +1908,20230714,06:30:00,2023-07-14 +1908,20230717,06:30:01,2023-07-17 +1908,20230718,06:30:00,2023-07-18 +1900,20230719,06:30:01,2023-07-19 +1900,20230720,06:30:01,2023-07-20 +1900,20230721,06:30:00,2023-07-21 +1901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +818,20230626,06:30:00,2023-06-26 +818,20230627,06:30:00,2023-06-27 +818,20230628,06:30:01,2023-06-28 +818,20230629,06:30:00,2023-06-29 +818,20230630,06:30:01,2023-06-30 +820,20230703,06:30:00,2023-07-03 +826,20230705,06:30:01,2023-07-05 +826,20230706,06:30:00,2023-07-06 +826,20230707,06:30:01,2023-07-07 +826,20230710,06:30:00,2023-07-10 +826,20230711,06:30:00,2023-07-11 +826,20230712,06:30:00,2023-07-12 +826,20230713,06:30:01,2023-07-13 +825,20230714,06:30:00,2023-07-14 +826,20230717,06:30:01,2023-07-17 +823,20230718,06:30:00,2023-07-18 +814,20230719,06:30:01,2023-07-19 +813,20230720,06:30:01,2023-07-20 +1004,20230721,06:30:00,2023-07-21 +1006,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1622,20230626,06:30:00,2023-06-26 +1622,20230627,06:30:00,2023-06-27 +1622,20230628,06:30:01,2023-06-28 +1622,20230629,06:30:00,2023-06-29 +1616,20230630,06:30:01,2023-06-30 +1616,20230703,06:30:00,2023-07-03 +1616,20230705,06:30:01,2023-07-05 +1616,20230706,06:30:00,2023-07-06 +1616,20230707,06:30:01,2023-07-07 +1624,20230710,06:30:00,2023-07-10 +1625,20230711,06:30:00,2023-07-11 +1625,20230712,06:30:00,2023-07-12 +1625,20230713,06:30:01,2023-07-13 +1625,20230714,06:30:00,2023-07-14 +1650,20230717,06:30:01,2023-07-17 +1876,20230718,06:30:00,2023-07-18 +1863,20230719,06:30:01,2023-07-19 +1864,20230720,06:30:01,2023-07-20 +1859,20230721,06:30:00,2023-07-21 +1861,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1709,20230626,06:30:00,2023-06-26 +1709,20230627,06:30:00,2023-06-27 +1709,20230628,06:30:01,2023-06-28 +1709,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1709,20230705,06:30:01,2023-07-05 +1709,20230706,06:30:00,2023-07-06 +1710,20230707,06:30:01,2023-07-07 +1710,20230710,06:30:00,2023-07-10 +2025,20230711,06:30:00,2023-07-11 +2027,20230712,06:30:00,2023-07-12 +2027,20230713,06:30:01,2023-07-13 +2027,20230714,06:30:00,2023-07-14 +2020,20230717,06:30:01,2023-07-17 +2030,20230718,06:30:00,2023-07-18 +1993,20230719,06:30:01,2023-07-19 +1993,20230720,06:30:01,2023-07-20 +2000,20230721,06:30:00,2023-07-21 +2000,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +4405,20230626,06:30:00,2023-06-26 +4462,20230627,06:30:00,2023-06-27 +4462,20230628,06:30:01,2023-06-28 +4463,20230629,06:30:00,2023-06-29 +4464,20230630,06:30:01,2023-06-30 +4464,20230703,06:30:00,2023-07-03 +4464,20230705,06:30:01,2023-07-05 +5124,20230706,06:30:00,2023-07-06 +7080,20230707,06:30:01,2023-07-07 +7080,20230710,06:30:00,2023-07-10 +7081,20230711,06:30:00,2023-07-11 +7081,20230712,06:30:00,2023-07-12 +7081,20230713,06:30:01,2023-07-13 +7082,20230714,06:30:00,2023-07-14 +7102,20230717,06:30:01,2023-07-17 +7107,20230718,06:30:00,2023-07-18 +7133,20230719,06:30:01,2023-07-19 +7140,20230720,06:30:01,2023-07-20 +7141,20230721,06:30:00,2023-07-21 +7152,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +998,20230626,06:30:00,2023-06-26 +1007,20230627,06:30:00,2023-06-27 +1007,20230628,06:30:01,2023-06-28 +1007,20230629,06:30:00,2023-06-29 +1007,20230630,06:30:01,2023-06-30 +1009,20230703,06:30:00,2023-07-03 +1009,20230705,06:30:01,2023-07-05 +1009,20230706,06:30:00,2023-07-06 +1009,20230707,06:30:01,2023-07-07 +1009,20230710,06:30:00,2023-07-10 +1009,20230711,06:30:00,2023-07-11 +1009,20230712,06:30:00,2023-07-12 +1009,20230713,06:30:01,2023-07-13 +1009,20230714,06:30:00,2023-07-14 +1002,20230717,06:30:01,2023-07-17 +1002,20230718,06:30:00,2023-07-18 +966,20230719,06:30:01,2023-07-19 +966,20230720,06:30:01,2023-07-20 +968,20230721,06:30:00,2023-07-21 +968,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1918,20230626,06:30:00,2023-06-26 +2046,20230627,06:30:00,2023-06-27 +2046,20230628,06:30:01,2023-06-28 +2046,20230629,06:30:00,2023-06-29 +2046,20230630,06:30:00,2023-06-30 +2046,20230703,06:30:01,2023-07-03 +2048,20230705,06:30:00,2023-07-05 +2048,20230706,06:30:01,2023-07-06 +2250,20230707,06:30:00,2023-07-07 +2235,20230710,06:30:01,2023-07-10 +2259,20230711,06:30:01,2023-07-11 +2258,20230712,06:30:01,2023-07-12 +2248,20230713,06:30:01,2023-07-13 +2248,20230714,06:30:00,2023-07-14 +2229,20230717,06:30:00,2023-07-17 +2229,20230718,06:30:00,2023-07-18 +2228,20230719,06:30:00,2023-07-19 +2227,20230720,06:30:00,2023-07-20 +2230,20230721,06:30:01,2023-07-21 +2230,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +2239,20230626,06:30:00,2023-06-26 +2395,20230627,06:30:00,2023-06-27 +2395,20230628,06:30:01,2023-06-28 +2395,20230629,06:30:00,2023-06-29 +2398,20230630,06:30:01,2023-06-30 +2398,20230703,06:30:00,2023-07-03 +2398,20230705,06:30:01,2023-07-05 +2399,20230706,06:30:00,2023-07-06 +2399,20230707,06:30:01,2023-07-07 +2399,20230710,06:30:00,2023-07-10 +2414,20230711,06:30:00,2023-07-11 +2425,20230712,06:30:00,2023-07-12 +2425,20230713,06:30:01,2023-07-13 +2425,20230714,06:30:00,2023-07-14 +2411,20230717,06:30:01,2023-07-17 +2415,20230718,06:30:00,2023-07-18 +2386,20230719,06:30:01,2023-07-19 +2432,20230720,06:30:01,2023-07-20 +2439,20230721,06:30:00,2023-07-21 +2441,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +2872,20230626,06:30:00,2023-06-26 +3361,20230627,06:30:00,2023-06-27 +3955,20230628,06:30:01,2023-06-28 +4985,20230629,06:30:00,2023-06-29 +4969,20230630,06:30:01,2023-06-30 +4863,20230703,06:30:00,2023-07-03 +4863,20230705,06:30:01,2023-07-05 +4867,20230706,06:30:00,2023-07-06 +4888,20230707,06:30:01,2023-07-07 +4890,20230710,06:30:00,2023-07-10 +4930,20230711,06:30:00,2023-07-11 +4923,20230712,06:30:00,2023-07-12 +4923,20230713,06:30:01,2023-07-13 +4917,20230714,06:30:00,2023-07-14 +4918,20230717,06:30:01,2023-07-17 +4913,20230718,06:30:00,2023-07-18 +4924,20230719,06:30:01,2023-07-19 +5020,20230720,06:30:01,2023-07-20 +5101,20230721,06:30:00,2023-07-21 +5601,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1460,20230626,06:30:00,2023-06-26 +1524,20230627,06:30:00,2023-06-27 +1724,20230628,06:30:01,2023-06-28 +1724,20230629,06:30:00,2023-06-29 +1709,20230630,06:30:01,2023-06-30 +1709,20230703,06:30:00,2023-07-03 +1714,20230705,06:30:01,2023-07-05 +1716,20230706,06:30:00,2023-07-06 +1718,20230707,06:30:01,2023-07-07 +1702,20230710,06:30:00,2023-07-10 +1720,20230711,06:30:00,2023-07-11 +1720,20230712,06:30:00,2023-07-12 +1721,20230713,06:30:01,2023-07-13 +1721,20230714,06:30:00,2023-07-14 +1731,20230717,06:30:01,2023-07-17 +1738,20230718,06:30:00,2023-07-18 +1942,20230719,06:30:01,2023-07-19 +1956,20230720,06:30:01,2023-07-20 +1961,20230721,06:30:00,2023-07-21 +1961,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +857,20230626,06:30:00,2023-06-26 +949,20230627,06:30:00,2023-06-27 +1151,20230628,06:30:01,2023-06-28 +1155,20230629,06:30:00,2023-06-29 +1160,20230630,06:30:00,2023-06-30 +1162,20230703,06:30:01,2023-07-03 +1162,20230705,06:30:00,2023-07-05 +1439,20230706,06:30:01,2023-07-06 +1439,20230707,06:30:00,2023-07-07 +1440,20230710,06:30:01,2023-07-10 +1455,20230711,06:30:01,2023-07-11 +1459,20230712,06:30:01,2023-07-12 +1463,20230713,06:30:01,2023-07-13 +1466,20230714,06:30:00,2023-07-14 +1466,20230717,06:30:00,2023-07-17 +1466,20230718,06:30:00,2023-07-18 +1501,20230719,06:30:00,2023-07-19 +1524,20230720,06:30:00,2023-07-20 +1605,20230721,06:30:01,2023-07-21 +1636,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +976,20230626,06:30:01,2023-06-26 +1052,20230627,06:30:01,2023-06-27 +1054,20230628,06:30:01,2023-06-28 +1054,20230629,06:30:01,2023-06-29 +1143,20230630,06:30:00,2023-06-30 +1149,20230703,06:30:01,2023-07-03 +1149,20230705,06:30:01,2023-07-05 +1149,20230706,06:30:00,2023-07-06 +1149,20230707,06:30:01,2023-07-07 +1149,20230710,06:30:00,2023-07-10 +1220,20230711,06:30:01,2023-07-11 +1265,20230712,06:30:01,2023-07-12 +1264,20230713,06:30:01,2023-07-13 +1264,20230714,06:30:01,2023-07-14 +1293,20230717,06:30:01,2023-07-17 +1296,20230718,06:30:01,2023-07-18 +1374,20230719,06:30:01,2023-07-19 +1452,20230720,06:30:01,2023-07-20 +1455,20230721,06:30:01,2023-07-21 +1455,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +8245,20230626,06:30:00,2023-06-26 +8245,20230627,06:30:00,2023-06-27 +8941,20230628,06:30:01,2023-06-28 +10141,20230629,06:30:00,2023-06-29 +10141,20230630,06:30:00,2023-06-30 +10146,20230703,06:30:01,2023-07-03 +10148,20230705,06:30:00,2023-07-05 +10148,20230706,06:30:01,2023-07-06 +13459,20230707,06:30:00,2023-07-07 +13459,20230710,06:30:01,2023-07-10 +13477,20230711,06:30:01,2023-07-11 +14711,20230712,06:30:01,2023-07-12 +14727,20230713,06:30:01,2023-07-13 +14726,20230714,06:30:00,2023-07-14 +14726,20230717,06:30:00,2023-07-17 +14723,20230718,06:30:00,2023-07-18 +14738,20230719,06:30:00,2023-07-19 +14751,20230720,06:30:00,2023-07-20 +15159,20230721,06:30:00,2023-07-21 +15160,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +826,20230626,06:30:00,2023-06-26 +811,20230627,06:30:00,2023-06-27 +811,20230628,06:30:01,2023-06-28 +811,20230629,06:30:00,2023-06-29 +811,20230630,06:30:01,2023-06-30 +811,20230703,06:30:00,2023-07-03 +811,20230705,06:30:01,2023-07-05 +811,20230706,06:30:00,2023-07-06 +811,20230707,06:30:01,2023-07-07 +811,20230710,06:30:00,2023-07-10 +812,20230711,06:30:00,2023-07-11 +812,20230712,06:30:00,2023-07-12 +810,20230713,06:30:01,2023-07-13 +810,20230714,06:30:00,2023-07-14 +807,20230717,06:30:01,2023-07-17 +807,20230718,06:30:00,2023-07-18 +831,20230719,06:30:01,2023-07-19 +831,20230720,06:30:01,2023-07-20 +830,20230721,06:30:00,2023-07-21 +830,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1394,20230626,06:30:01,2023-06-26 +1404,20230627,06:30:00,2023-06-27 +1404,20230628,06:30:00,2023-06-28 +1404,20230629,06:30:01,2023-06-29 +1404,20230630,06:30:01,2023-06-30 +1404,20230703,06:30:01,2023-07-03 +1404,20230705,06:30:01,2023-07-05 +1404,20230706,06:30:00,2023-07-06 +1404,20230707,06:30:01,2023-07-07 +1404,20230710,06:30:01,2023-07-10 +1409,20230711,06:30:00,2023-07-11 +1417,20230712,06:30:00,2023-07-12 +1417,20230713,06:30:00,2023-07-13 +1416,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1415,20230719,06:30:00,2023-07-19 +1415,20230720,06:30:00,2023-07-20 +1417,20230721,06:30:00,2023-07-21 +1417,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +1413,20230626,06:30:01,2023-06-26 +1413,20230627,06:30:00,2023-06-27 +1413,20230628,06:30:00,2023-06-28 +1413,20230629,06:30:01,2023-06-29 +1413,20230630,06:30:01,2023-06-30 +1413,20230703,06:30:01,2023-07-03 +1413,20230705,06:30:01,2023-07-05 +1424,20230706,06:30:00,2023-07-06 +1424,20230707,06:30:01,2023-07-07 +1426,20230710,06:30:01,2023-07-10 +1429,20230711,06:30:00,2023-07-11 +1426,20230712,06:30:00,2023-07-12 +1426,20230713,06:30:00,2023-07-13 +1423,20230714,06:30:01,2023-07-14 +1416,20230717,06:30:00,2023-07-17 +1416,20230718,06:30:01,2023-07-18 +1395,20230719,06:30:00,2023-07-19 +1395,20230720,06:30:00,2023-07-20 +1400,20230721,06:30:00,2023-07-21 +1400,20230724,06:30:00,2023-07-24 + + +Open_interest,Date,time,Datetime +7477,20230626,06:30:01,2023-06-26 +7482,20230627,06:30:01,2023-06-27 +7482,20230628,06:30:01,2023-06-28 +7503,20230629,06:30:01,2023-06-29 +7503,20230630,06:30:00,2023-06-30 +8562,20230703,06:30:01,2023-07-03 +8562,20230705,06:30:01,2023-07-05 +8562,20230706,06:30:00,2023-07-06 +8562,20230707,06:30:01,2023-07-07 +8561,20230710,06:30:00,2023-07-10 +8947,20230711,06:30:01,2023-07-11 +8947,20230712,06:30:01,2023-07-12 +8947,20230713,06:30:01,2023-07-13 +8947,20230714,06:30:01,2023-07-14 +8914,20230717,06:30:01,2023-07-17 +8913,20230718,06:30:01,2023-07-18 +8890,20230719,06:30:01,2023-07-19 +8891,20230720,06:30:01,2023-07-20 +8904,20230721,06:30:01,2023-07-21 +8901,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1361,20230626,06:30:01,2023-06-26 +1361,20230627,06:30:01,2023-06-27 +1361,20230628,06:30:01,2023-06-28 +1361,20230629,06:30:01,2023-06-29 +1361,20230630,06:30:00,2023-06-30 +1374,20230703,06:30:01,2023-07-03 +1374,20230705,06:30:01,2023-07-05 +1374,20230706,06:30:00,2023-07-06 +1374,20230707,06:30:01,2023-07-07 +1374,20230710,06:30:00,2023-07-10 +1361,20230711,06:30:01,2023-07-11 +1374,20230712,06:30:01,2023-07-12 +1375,20230713,06:30:01,2023-07-13 +1375,20230714,06:30:01,2023-07-14 +1368,20230717,06:30:01,2023-07-17 +1368,20230718,06:30:01,2023-07-18 +1361,20230719,06:30:01,2023-07-19 +1361,20230720,06:30:01,2023-07-20 +1361,20230721,06:30:01,2023-07-21 +1361,20230724,06:30:01,2023-07-24 + + +Open_interest,Date,time,Datetime +1056,20230626,06:30:00,2023-06-26 +1056,20230627,06:30:01,2023-06-27 +1056,20230628,06:30:00,2023-06-28 +1056,20230629,06:30:00,2023-06-29 +1056,20230630,06:30:00,2023-06-30 +1054,20230703,06:30:00,2023-07-03 +1054,20230705,06:30:00,2023-07-05 +1055,20230706,06:30:01,2023-07-06 +1055,20230707,06:30:00,2023-07-07 +1055,20230710,06:30:00,2023-07-10 +1056,20230711,06:30:00,2023-07-11 +1062,20230712,06:30:00,2023-07-12 +1063,20230713,06:30:00,2023-07-13 +1061,20230714,06:30:00,2023-07-14 +1060,20230717,06:30:01,2023-07-17 +1060,20230718,06:30:00,2023-07-18 +1056,20230719,06:30:00,2023-07-19 +1057,20230720,06:30:01,2023-07-20 +1057,20230721,06:30:01,2023-07-21 +1057,20230724,06:30:01,2023-07-24 + + diff --git a/EventDriven/tests/data/run_async_tick_results.txt b/EventDriven/tests/data/run_async_tick_results.txt new file mode 100644 index 0000000..b582be2 --- /dev/null +++ b/EventDriven/tests/data/run_async_tick_results.txt @@ -0,0 +1,46 @@ +GOOGL20240621P132.5 + +GOOGL20240621P130 + +GOOGL20240621P127.5 + +GOOGL20240621P125 + +GOOGL20240621P122.5 + +GOOGL20240621P120 + +GOOGL20240621P117.5 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P107.5 + +GOOGL20240621P105 + +GOOGL20240621P102.5 + +GOOGL20240621P100 + +GOOGL20240621P98 + +GOOGL20240621P97.5 + +GOOGL20240621P96 + +GOOGL20240621P95 + +GOOGL20240621P94 + +GOOGL20240621P92 + diff --git a/EventDriven/tests/data/tick_results.txt b/EventDriven/tests/data/tick_results.txt new file mode 100644 index 0000000..b582be2 --- /dev/null +++ b/EventDriven/tests/data/tick_results.txt @@ -0,0 +1,46 @@ +GOOGL20240621P132.5 + +GOOGL20240621P130 + +GOOGL20240621P127.5 + +GOOGL20240621P125 + +GOOGL20240621P122.5 + +GOOGL20240621P120 + +GOOGL20240621P117.5 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P115 + +GOOGL20240621P112.5 + +GOOGL20240621P110 + +GOOGL20240621P107.5 + +GOOGL20240621P105 + +GOOGL20240621P102.5 + +GOOGL20240621P100 + +GOOGL20240621P98 + +GOOGL20240621P97.5 + +GOOGL20240621P96 + +GOOGL20240621P95 + +GOOGL20240621P94 + +GOOGL20240621P92 + diff --git a/EventDriven/tests/riskmanager_test.py b/EventDriven/tests/riskmanager_test.py new file mode 100644 index 0000000..be3f042 --- /dev/null +++ b/EventDriven/tests/riskmanager_test.py @@ -0,0 +1,87 @@ +from EventDriven.riskmanager import RiskManager +from dbase.DataAPI.ThetaData import list_contracts, retrieve_option_ohlc, is_theta_data_retrieval_successful #type: ignore +import datetime +import pandas as pd +import pandas_market_calendars as mcal +import unittest +import numpy as np + +tickers = ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA', 'META', 'NVDA', 'NFLX', 'AMD'] +date_range = pd.date_range() + +#generate date range +nyse = mcal.get_calendar('NYSE') +year_ago_date = datetime.datetime.now() - datetime.timedelta(days=365) +schedule = nyse.schedule(start_date=year_ago_date, end_date=datetime.datetime.now()) +date_range = mcal.date_range(schedule, frequency='1D') +dates = [date.strftime('%Y-%m-%d') for date in date_range] + + + + +class RiskManagerOperations(unittest.TestCase): + def set_up(self): + self.risk_manager = RiskManager() + + def test_order_picker(self): + ticker = np.random.choice(tickers) + contract_date = np.random.choice(dates) + contracts = list_contracts(ticker, pd.to_datetime(contract_date).strftime('%Y%m%d')) + self.assertTrue(is_theta_data_retrieval_successful(contracts)) + + contract = contracts.sample() + contract_right = contract['right'] + contract_expiration = pd.to_datetime(contract['expiration']).strftime('%Y%m%d') + contract_strike = float(contract['strike']) + max_close = np.random.randint(1, 10) + + #order settings + moneyness_width = np.random.uniform(0.01, 0.05) + rel_strike_long = np.random.uniform(1.05, 1.3) + rel_strike_short = np.random.uniform(0.7, 0.95) + dte = np.random.randint(30, 365) + + order_settings = { + 'type': 'spread', + 'specifics': [ + {'direction': 'long', 'rel_strike': rel_strike_long, 'dte': dte, 'moneyness_width': moneyness_width}, + {'direction': 'short', 'rel_strike': rel_strike_short, 'dte': dte, 'moneyness_width': moneyness_width} + ], + 'name': 'vertical_spread' + } + + try: + self.order = self.risk_manager.OrderPicker.get_order(ticker, contract_expiration, contract_right, max_close, order_settings) + self.assertIsInstance(self.order, dict) + self.assertIsInstance(self.order['long'], list) + self.assertIsInstance(self.order['short'], list) + self.assertGreater(len(self.order['long']), 0) + self.assertGreater(len(self.order['short']), 0) + self.assertIsInstance(self.order['close'], float) + except AssertionError as e: + print(f"AssertionError: {e}") + print(f"Ticker: {ticker}") + print(f"Contract Date: {contract_date}") + print(f"Contracts: {contracts}") + print(f"Contract: {contract}") + print(f"Contract Right: {contract_right}") + print(f"Contract Expiration: {contract_expiration}") + print(f"Contract Strike: {contract_strike}") + print(f"Max Close: {max_close}") + print(f"Order Settings: {order_settings}") + raise + except Exception as e: + print(f"Exception: {e}") + print(f"Ticker: {ticker}") + print(f"Contract Date: {contract_date}") + print(f"Contracts: {contracts}") + print(f"Contract: {contract}") + print(f"Contract Right: {contract_right}") + print(f"Contract Expiration: {contract_expiration}") + print(f"Contract Strike: {contract_strike}") + print(f"Max Close: {max_close}") + print(f"Order Settings: {order_settings}") + raise + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/EventDriven/tests/test.py b/EventDriven/tests/test.py new file mode 100644 index 0000000..fb7f7e1 --- /dev/null +++ b/EventDriven/tests/test.py @@ -0,0 +1,35 @@ +""" +This is used to test the proxy server works to fetch thetadata +""" + +import http.client +import json +import os + +# print(os.environ['PROXY_URL']) +# {'end_date': 20250619, 'root': 'AAPL', 'use_csv': 'true', 'exp': 20241220, 'right': 'C', 'start_date': 20170101, 'strike': 220000, 'url': 'http://127.0.0.1:25510/v2/hist/option/eod?end_date=20250619&root=AAPL&use_csv=true&exp=20241220&right=C&start_date=20170101&strike=220000'} +conn = http.client.HTTPConnection("54.205.248.219", 5500) +payload = json.dumps({ + "method": "GET", + "url": 'http://127.0.0.1:25510/v2/hist/option/eod?end_date=20250619&root=AAPL&use_csv=true&exp=20241220&right=C&start_date=20240101&strike=220000' +}) +url_old = "http://127.0.0.1:25510/v2/hist/option/eod?exp=20231103&right=C&strike=170000&start_date=20231103&end_date=20231103&root=AAPL" +headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json' +} +conn.request("POST", "/thetadata", payload, headers) +res = conn.getresponse() +data = res.read() +print(data.decode("utf-8")) + +print("\n\n\n") +retrieve_quote_payload = json.dumps({ + "method": "GET", + "url": "http://127.0.0.1:25510/v2/hist/option/quote?end_date=20230706&root=MSFT&use_csv=true&exp=20240621&ivl=1800000&right=C&start_date=20230706&strike=355000&start_time=34200000&rth=False&end_time=57600000" +}) +conn.request("POST", "/thetadata", retrieve_quote_payload, headers) +res = conn.getresponse() +data = res.read() +print(data.decode("utf-8")) +conn.close() \ No newline at end of file diff --git a/EventDriven/tests/text.txt b/EventDriven/tests/text.txt new file mode 100644 index 0000000..96a07d7 --- /dev/null +++ b/EventDriven/tests/text.txt @@ -0,0 +1,72 @@ +Current time is 2026-01-07 19:38:00.379396 +Current time is 2026-01-08 19:38:00.854582 +Current time is 2026-01-09 19:38:01.294838 +Current time is 2026-01-10 19:38:01.294813 +Current time is 2026-01-11 19:38:00.816443 +Current time is 2026-01-12 19:38:00.590963 +Current time is 2026-01-14 19:38:01.625687 +Current time is 2026-01-15 19:38:01.134328 +Current time is 2026-01-16 19:38:01.192866 +Current time is 2026-01-17 19:38:01.485891 +Current time is 2026-01-18 19:38:03.371884 +Current time is 2026-01-19 19:38:01.239734 +Current time is 2026-01-20 19:38:01.563849 +Current time is 2026-01-22 19:38:01.785768 +Current time is 2026-01-23 19:38:00.504924 +Current time is 2026-01-24 19:38:00.723301 +Current time is 2026-01-25 19:38:00.776985 +Current time is 2026-01-26 19:38:00.737705 +Current time is 2026-01-27 19:38:00.667648 +Current time is 2026-01-28 19:38:00.625588 +Current time is 2026-01-29 19:38:00.576900 +Current time is 2026-01-30 19:38:00.875793 +Current time is 2026-01-31 19:38:00.592736 +Current time is 2026-02-01 19:38:00.831116 +Current time is 2026-02-02 19:38:00.557793 +Current time is 2026-02-04 19:38:01.559188 +Current time is 2026-02-05 19:38:01.587129 +Current time is 2026-02-06 19:38:00.891593 +Current time is 2026-02-07 19:38:00.780620 +Current time is 2026-02-08 19:38:00.991872 +Current time is 2026-02-09 19:38:00.840477 +Current time is 2026-02-10 19:38:01.336460 +Current time is 2026-02-13 19:38:01.156833 +Current time is 2026-02-14 19:38:00.757660 +Current time is 2026-02-15 19:38:00.851748 +Current time is 2026-02-16 19:38:00.932141 +Current time is 2026-02-19 19:38:01.537709 +Current time is 2026-02-20 19:38:00.765617 +Current time is 2026-02-21 19:38:02.213471 +Current time is 2026-02-22 19:38:01.360431 +Current time is 2026-02-23 19:38:01.614357 +Current time is 2026-02-24 19:38:00.566710 +Current time is 2026-02-27 19:38:01.749666 +Current time is 2026-02-28 19:38:00.920661 +Current time is 2026-03-01 19:38:00.311558 +Current time is 2026-03-02 19:38:01.848592 +Current time is 2026-03-04 19:38:00.801107 +Current time is 2026-03-05 19:38:01.048031 +Current time is 2026-03-06 19:38:00.812919 +Current time is 2026-03-08 19:38:00.685235 +Current time is 2026-03-09 19:38:01.251121 +Current time is 2026-03-10 19:38:00.818313 +Current time is 2026-03-11 19:38:01.871341 +Current time is 2026-03-13 19:38:01.607868 +Current time is 2026-03-14 19:38:00.916970 +Current time is 2026-03-15 19:38:00.958060 +Current time is 2026-03-16 19:38:00.825002 +Current time is 2026-03-19 19:38:01.849785 +Current time is 2026-03-20 19:38:01.230870 +Current time is 2026-03-21 19:38:00.877359 +Current time is 2026-03-22 19:38:01.016070 +Current time is 2026-03-23 19:38:00.945916 +Current time is 2026-03-26 19:38:01.431744 +Current time is 2026-03-28 19:38:01.438732 +Current time is 2026-03-29 19:43:59.174085 +Current time is 2026-03-30 19:38:00.791561 +Current time is 2026-03-31 19:38:01.070004 +Current time is 2026-04-01 19:38:01.116752 +Current time is 2026-04-02 19:38:00.926718 +Current time is 2026-04-03 19:38:00.985205 +Current time is 2026-04-04 19:38:01.184752 +Current time is 2026-04-05 19:38:01.092865 From f3528a629cfab18387c2b92ef8b8f3a6ec5e7c54 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:24:23 -0400 Subject: [PATCH 24/81] feat: Add trade aggregation API with multiple granularity levels - Add AggregationLevel enum (BY_TRADE_SIGNAL, BY_SIGNAL, BY_TRADE) to types.py - Fix signal_id typo in trade.py constructor - Implement aggregate_trades() method with weighted/additive column specs - Support flexible trade grouping for reporting and analysis --- EventDriven/new_portfolio.py | 242 ++++++++++++++++++++++++++++++----- EventDriven/trade.py | 4 +- EventDriven/types.py | 14 ++ 3 files changed, 224 insertions(+), 36 deletions(-) diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index fd4152c..d762a99 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -12,7 +12,16 @@ from EventDriven.dataclasses.orders import OrderRequest from EventDriven.eventScheduler import EventScheduler from EventDriven.trade import Trade -from EventDriven.types import EventTypes, FillDirection, ResultsEnum, SignalTypes, OrderData, Order, TradeID +from EventDriven.types import ( + AggregationLevel, + EventTypes, + FillDirection, + ResultsEnum, + SignalTypes, + OrderData, + Order, + TradeID, +) from EventDriven.riskmanager.new_base import RiskManager, order_failed from EventDriven.riskmanager.utils import parse_position_id from trade.helpers.Logging import setup_logger @@ -32,9 +41,15 @@ from trade.helpers.helper import change_to_last_busday, is_USholiday from trade.backtester_.utils.aggregators import AggregatorParent from trade.backtester_.utils.utils import plot_portfolio -from typing import Optional +from typing import Optional, Union import plotly -from EventDriven.dataclasses.states import AtTimePositionData, PositionState, PortfolioMetaInfo, PortfolioState, PositionAnalysisContext +from EventDriven.dataclasses.states import ( + AtTimePositionData, + PositionState, + PortfolioMetaInfo, + PortfolioState, + PositionAnalysisContext, +) from EventDriven.dataclasses.states import StrategyChangeMeta from EventDriven.configs.core import PortfolioManagerConfig, CashAllocatorConfig from EventDriven.portfolio_utils import extract_events @@ -81,6 +96,37 @@ class OptionSignalPortfolio(Portfolio): initial_capital: int """ + # ------------------------------------------------------------------ + # Class-level aggregation spec constants + # ------------------------------------------------------------------ + + _ADDITIVE_COLS: list[str] = [ + "EntryCommission", + "ExitCommission", + "EntrySlippage", + "ExitSlippage", + "EntryAuxilaryCost", + "ExitAuxilaryCost", + "TotalCommission", + "TotalSlippage", + "TotalAuxilaryCost", + "ClosedPnL", + "UnrealizedPnL", + "PnL", + "EntryQuantity", + "ExitQuantity", + "ClosedQuantity", + "OpenQuantity", + ] + + # (column_to_average, column_used_as_weight) + _WEIGHTED_COLS: list[tuple[str, str]] = [ + ("EntryPrice", "EntryQuantity"), + ("ExitPrice", "ExitQuantity"), + ("TotalEntryCost", "EntryQuantity"), + ("TotalExitCost", "ExitQuantity"), + ] + def __init__( self, bars: HistoricTradeDataHandler, @@ -301,8 +347,9 @@ def _equity(self): @property def trades(self): - """ - Returns a DataFrame of trades executed in the portfolio. + """Returns a DataFrame of trades aggregated at BY_TRADE_SIGNAL granularity (default). + + For other aggregation levels call aggregate_trades(level=...) directly. """ if self.trades_df is not None: return self.trades_df @@ -310,10 +357,132 @@ def trades(self): self.trades_df = self.aggregate_trades() return self.trades_df - def aggregate_trades(self): - # trades_data = [self.trades_map[trade_id].stats for trade_id in self.trades_map.keys()] + def aggregate_trades( + self, + level: Union[AggregationLevel, str] = AggregationLevel.BY_TRADE_SIGNAL, + ) -> Optional[pd.DataFrame]: + """Returns a DataFrame of trade statistics aggregated at the requested granularity. + + Args: + level: Aggregation granularity. Accepts an AggregationLevel enum value or its + string equivalent (e.g. ``"by_signal"``). Defaults to BY_TRADE_SIGNAL. + + Returns: + DataFrame with one row per group at the requested level, or None when no trades + have been recorded yet. + """ + if isinstance(level, str): + level = AggregationLevel(level) + trades_data = [trade.stats for trade in self.trades_map.values()] - return pd.concat(trades_data, ignore_index=True) if trades_data else None + if not trades_data: + return None + + base = pd.concat(trades_data, ignore_index=True) + + if level == AggregationLevel.BY_TRADE_SIGNAL: + return base + + return self._aggregate_by_level(base, level) + + # ------------------------------------------------------------------ + # Internal trade aggregation helpers + # ------------------------------------------------------------------ + + def _weighted_mean(self, df: pd.DataFrame, value_col: str, weight_col: str) -> float: + """Weighted average of value_col weighted by weight_col; returns 0 on zero-weight groups.""" + weights = df[weight_col].fillna(0) + total_weight = weights.sum() + if total_weight == 0: + return 0.0 + return (df[value_col].fillna(0) * weights).sum() / total_weight + + def _aggregate_by_level( + self, + base: pd.DataFrame, + level: AggregationLevel, + ) -> pd.DataFrame: + """Performs multi-row aggregation over *base* for the given *level*. + + Fully closed groups (every row has a non-null ExitTime) are fully aggregated. + Not-fully-closed groups mirror the open-trade NaN behaviour from Trade.aggregate(), + with ClosedPnL and ReturnPct as the only exceptions — those are always populated + from whatever portion of the group has already been closed. + """ + group_key = "SignalID" if level == AggregationLevel.BY_SIGNAL else "TradeID" + other_id = "TradeID" if level == AggregationLevel.BY_SIGNAL else "SignalID" + + # Columns to null for groups that are not fully closed. + # ClosedPnL and ReturnPct are intentionally excluded — they are always populated. + _NULL_WHEN_OPEN: set[str] = { + "ExitPrice", + "TotalExitCost", + "ExitCommission", + "ExitSlippage", + "ExitAuxilaryCost", + "ExitQuantity", + "ClosedQuantity", + "TotalCommission", + "TotalSlippage", + "TotalAuxilaryCost", + "Duration", + "UnrealizedPnL", + "PnL", + "Quantity", + } + + rows = [] + for group_val, grp in base.groupby(group_key, sort=False): + row: dict = {group_key: group_val} + + # Other ID: preserve single value or mark as MIXED + other_vals = grp[other_id].dropna().unique().tolist() + row[other_id] = other_vals[0] if len(other_vals) == 1 else "MIXED" + + # Ticker: preserve single value or mark as MULTI + tickers = grp["Ticker"].dropna().unique().tolist() + row["Ticker"] = tickers[0] if len(tickers) == 1 else "MULTI" + + # Time boundaries + row["EntryTime"] = grp["EntryTime"].dropna().min() + exit_times = grp["ExitTime"].dropna() + is_fully_closed = len(exit_times) == len(grp) + row["ExitTime"] = exit_times.max() if is_fully_closed else None + + # Additive columns — always sum; open-group nulls applied below + for col in self._ADDITIVE_COLS: + if col in grp.columns: + row[col] = grp[col].fillna(0).sum() + + # Weighted-average price / cost columns — always compute; open-group nulls applied below + for val_col, wt_col in self._WEIGHTED_COLS: + if val_col in grp.columns and wt_col in grp.columns: + row[val_col] = self._weighted_mean(grp, val_col, wt_col) + + # ReturnPct: always derived from ClosedPnL / TotalEntryCost so it is + # meaningful even when the group is not fully closed. + total_entry_cost = row.get("TotalEntryCost", 0) + closed_pnl = row.get("ClosedPnL", 0) + row["ReturnPct"] = (closed_pnl / total_entry_cost) if total_entry_cost > 0 else 0 + + if is_fully_closed: + # Quantity as net open (should be 0 for a fully closed group) + entry_qty = row.get("EntryQuantity", 0) + exit_qty = row.get("ExitQuantity", 0) + row["Quantity"] = entry_qty - exit_qty + + # Duration from earliest entry to latest exit + entry_dt = row["EntryTime"] + exit_dt = row["ExitTime"] + row["Duration"] = (exit_dt - entry_dt).days if (exit_dt is not None and entry_dt is not None) else None + else: + # Null exit-side and derived combined columns; keep ClosedPnL + ReturnPct + for col in _NULL_WHEN_OPEN: + row[col] = None + + rows.append(row) + + return pd.DataFrame(rows) @property def _trades(self): @@ -336,9 +505,13 @@ def dates_(self, start: bool = True): def buyNhold(self): stock_ts = pd.DataFrame() for stock in self.symbol_list: - data = self.risk_manager.market_data.market_timeseries.get_timeseries( - sym=stock, factor="spot", start_date=self.start_date, end_date=self.eventScheduler.end_date - ).spot["close"].rename(stock) + data = ( + self.risk_manager.market_data.market_timeseries.get_timeseries( + sym=stock, factor="spot", start_date=self.start_date, end_date=self.eventScheduler.end_date + ) + .spot["close"] + .rename(stock) + ) stock_ts = pd.concat([stock_ts, data], axis=1) stock_ts["Total"] = stock_ts.sum(axis=1) @@ -355,12 +528,11 @@ def generate_order(self, signal_event: SignalEvent): signal_type = signal_event.signal_type order_type = "MKT" order = None - if signal_type != "CLOSE": # generate order for LONG or SHORT order = self.create_order(signal_event, order_type) self.order_cache["OPEN"].setdefault(signal_event.datetime, {})[signal_event.symbol] = order - + elif signal_type == "CLOSE": ## Check if we have signal_id in current positions. If not, log warning and skip. if signal_event.signal_id not in self.current_positions[symbol]: @@ -429,10 +601,12 @@ def generate_order(self, signal_event: SignalEvent): parent_event=signal_event, ) self.order_cache["CLOSE"].setdefault(signal_event.datetime, {})[signal_event.symbol] = order - + ## Update order with at-time data for execution handler to use for slippage calculations and order generation logic. if order is not None: - at_time_data = self.get_at_time_position_data(position_id=order.position["trade_id"], date=signal_event.datetime) + at_time_data = self.get_at_time_position_data( + position_id=order.position["trade_id"], date=signal_event.datetime + ) spread = at_time_data.get_spread() close = at_time_data.get_price() self.logger.info( @@ -440,8 +614,7 @@ def generate_order(self, signal_event: SignalEvent): ) order.position["spread"] = spread order.position["close"] = close - - + return order def resolve_order_result(self, position_result, signal): @@ -471,9 +644,7 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type ## Determine max contract price based on cash allocator config or default to 50% of allocated cash max_contract_dict = ( - self.cash_allocator_config.build_max_cash_map( - weights=self.__weight_map, cash=self.initial_capital - ) + self.cash_allocator_config.build_max_cash_map(weights=self.__weight_map, cash=self.initial_capital) if self.cash_allocator_config is not None else {} ) @@ -517,7 +688,6 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type self._process_failed_order(signal_event, order_result=order) return None - return OrderEvent( signal_event.symbol, signal_event.datetime, @@ -530,7 +700,7 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type parent_event=signal_event, ) - def _process_failed_order(self, signal_event: SignalEvent, order_result:Order=None): + def _process_failed_order(self, signal_event: SignalEvent, order_result: Order = None): """ Process a failed order by either rolling it forward or logging it. """ @@ -544,7 +714,7 @@ def _process_failed_order(self, signal_event: SignalEvent, order_result:Order=No self.logger.critical( f"Failed to process signal for {signal_event.signal_id} on {signal_event.datetime}, not rolling forward as per config." ) - + ## Log unprocessed signal for analysis unprocess_dict = signal_event.__dict__ unprocess_dict["reason"] = reason @@ -850,10 +1020,10 @@ def __normalize_dollar_amount(self, price: float) -> float: def _get_trade_object(self, trade_id: str, signal_id: str) -> Trade: return self.trades_map.get(self._get_trade_key(trade_id, signal_id)) - + def _get_trade_key(self, trade_id: str, signal_id: str) -> tuple: return (trade_id, signal_id) - + def _set_trade_object(self, trade_id: str, signal_id: str, trade: Trade): self.trades_map[self._get_trade_key(trade_id, signal_id)] = trade @@ -867,7 +1037,7 @@ def update_positions_on_fill(self, fill_event: FillEvent): """ # Check whether the fill is a buy or sell new_position_data = {} - trade_map_key = self._get_trade_key(fill_event.position["trade_id"], fill_event.signal_id) # noqa + trade_map_key = self._get_trade_key(fill_event.position["trade_id"], fill_event.signal_id) # noqa trade_id = fill_event.position["trade_id"] symbol = fill_event.symbol if trade_map_key not in self.trades_map: @@ -1011,8 +1181,9 @@ def update_timeindex(self): current_position["position"]["spread"] = spread current_trade_id = current_position["position"]["trade_id"] current_trade = self._get_trade_object(current_trade_id, signal_id) - current_trade.update_current_price(self.__normalize_dollar_amount(current_close)) ## Update current price on trade object for PnL calculations in risk manager analysis. This is important to have accurate and up to date price information on the trade object for the position analysis and risk management decisions. - + current_trade.update_current_price( + self.__normalize_dollar_amount(current_close) + ) ## Update current price on trade object for PnL calculations in risk manager analysis. This is important to have accurate and up to date price information on the trade object for the position analysis and risk management decisions. self.current_positions[sym][signal_id]["position"]["close"] = ( current_close ##Update close price for every iteration @@ -1067,8 +1238,8 @@ def calculate_close_on_position(self, position, date=None) -> float: return self.risk_manager.market_data.get_at_time_position_data( position["trade_id"], date or self.eventScheduler.current_date ).get_price() - - def calculate_spread_on_position(self, position_id:TradeID, date=None) -> float: + + def calculate_spread_on_position(self, position_id: TradeID, date=None) -> float: """ Calculate the spread on a position the spread is the difference between the bid and ask price of the position @@ -1077,11 +1248,14 @@ def calculate_spread_on_position(self, position_id:TradeID, date=None) -> float: position_id=position_id, date=date or self.eventScheduler.current_date ).get_spread() - def get_at_time_position_data(self, position_id:TradeID, date=None) -> AtTimePositionData: + def get_at_time_position_data(self, position_id: TradeID, date=None) -> AtTimePositionData: """ Get the position data at a given time """ - return self.risk_manager.market_data.get_at_time_position_data(position_id=position_id, date=date or self.eventScheduler.current_date) + return self.risk_manager.market_data.get_at_time_position_data( + position_id=position_id, date=date or self.eventScheduler.current_date + ) + # Getters def get_weighted_holdings(self) -> pd.DataFrame: """ @@ -1186,9 +1360,9 @@ def plot_portfolio( tr["Size"] = tr["Quantity"] return plot_portfolio(tr, eq, dd, _bnch, plot_bnchmk=plot_bnchmk, return_plot=return_plot, **kwargs) - + def get_strategy_class(self) -> Optional[MultiAssetStrategy]: """ Returns the multi-asset strategy class if it exists, else returns None """ - return self.eq_strategy.strategy_class if self.eq_strategy is not None else None \ No newline at end of file + return self.eq_strategy.strategy_class if self.eq_strategy is not None else None diff --git a/EventDriven/trade.py b/EventDriven/trade.py index b7ee8cb..47219c2 100644 --- a/EventDriven/trade.py +++ b/EventDriven/trade.py @@ -16,7 +16,7 @@ class Trade: Has separate ledgers for buy and sell events. """ - def __init__(self, trade_id: str, symbol: str, signa_id: str = None): + def __init__(self, trade_id: str, symbol: str, signal_id: str = None): self.trade_id = trade_id self.symbol = symbol self.buy_ledger = TradeLedger(f"{trade_id}_buy") @@ -25,7 +25,7 @@ def __init__(self, trade_id: str, symbol: str, signa_id: str = None): self.exit_date = None self.current_price = None self.stats = None - self.signal_id = signa_id + self.signal_id = signal_id def __getitem__(self, key): """ diff --git a/EventDriven/types.py b/EventDriven/types.py index fa428a0..22f930e 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -181,6 +181,20 @@ class FillDirection(Enum): EXERCISE = "EXERCISE" +class AggregationLevel(Enum): + """Aggregation granularity for portfolio trade reporting. + + Values: + BY_TRADE_SIGNAL: One row per (TradeID, SignalID) pair. Default behaviour. + BY_SIGNAL: One row per SignalID, collapsing all TradeIDs under that signal. + BY_TRADE: One row per TradeID, collapsing all SignalIDs under that trade. + """ + + BY_TRADE_SIGNAL = "by_trade_signal" + BY_SIGNAL = "by_signal" + BY_TRADE = "by_trade" + + class PositionAdjustmentReason(Enum): DTE_ROLL = "DTE_ROLL" MONEYNESS_ROLL = "MONEYNESS_ROLL" From da7cce963ef5ff10b2c16dc4486ef0df9fce2db6 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:25:34 -0400 Subject: [PATCH 25/81] fix: Align position limit effective-date to business day after t+n settlement - Ensure effective_date respects next trading day after (last_updated + t_plus_n) - Prevents limits from taking effect intra-t+n period - Maintains correct settlement timeline for portfolio constraints --- EventDriven/riskmanager/position/cogs/limits.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index a554e72..eeb8bb7 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -561,7 +561,9 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction ## Update effective date to be the next trading day after last_updated + t_plus_n ## This is because analysis is based on EOD data at last_updated. Execution therefore has to start from the next trading day. ## If t_plus_n is 0, then effective date will be the next trading day after last_updated, which is the expected behavior. - action.effective_date = last_updated + t_plus_n_timedelta + pd.Timedelta(days=1) # Add 1 day to ensure we are on the next trading day after last_updated + t_plus_n + ## If t_plus_n is > 0, then effective date will be last_updated + t_plus_n, but if that falls on a non-trading day, we need to move it to the next trading day. Therefore, we add a buffer of 1 day to ensure we move to the next trading day if last_updated + t_plus_n falls on a non-trading day. + tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day + action.effective_date = last_updated + tplus_n_timedelta ## Only generate verbose_info for non-HOLD actions (Task #4 optimization) if action.action != "HOLD": From 827f3f9c81599502d26bdb1314b900202cb17b62 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:26:09 -0400 Subject: [PATCH 26/81] feat: Two-stage centralized liquidity gating Add LiquidityConfig and LiquidityPolicy for shared liquidity control: - Level 0: Disabled (all trades execute normally) - Level 1: Quantity haircut in risk manager for wide spreads - Level 2: Silent drop + next-business-day reschedule at threshold Implementation: - LiquidityConfig with level (0..2) and max_spread_pct threshold - LiquidityPolicy service with enabled(level) and should_drop_for_spread() checks - Inject shared policy into RiskManager (level-1 quantity gate) - Inject shared policy into SimulatedExecutionHandler (level-2 silent drop + tracking) - Backtest orchestrator wires policy to both components - Add finalize-trades infrastructure for eq_strategy: close all open positions on final day --- EventDriven/backtest.py | 102 +++++++-- EventDriven/configs/core.py | 13 ++ EventDriven/execution.py | 321 ++++++++++++++++++---------- EventDriven/liquidity.py | 27 +++ EventDriven/riskmanager/new_base.py | 13 +- 5 files changed, 347 insertions(+), 129 deletions(-) create mode 100644 EventDriven/liquidity.py diff --git a/EventDriven/backtest.py b/EventDriven/backtest.py index 99013ca..124a5da 100644 --- a/EventDriven/backtest.py +++ b/EventDriven/backtest.py @@ -2,7 +2,7 @@ from typing import Dict, Optional, cast import pandas as pd from EventDriven.data import HistoricTradeDataHandler -from EventDriven.event import Event +from EventDriven.event import Event, SignalEvent from EventDriven.strategy import OptionSignalStrategy from EventDriven.new_portfolio import OptionSignalPortfolio from EventDriven.execution import SimulatedExecutionHandler @@ -10,13 +10,14 @@ from EventDriven.eventScheduler import EventScheduler from trade.backtester_._multi_asset_strategy import MultiAssetStrategy from trade.helpers.Logging import setup_logger -from trade.helpers.helper import change_to_last_busday, is_USholiday +from trade.helpers.helper import change_to_last_busday, is_USholiday, to_datetime from EventDriven.helpers import generate_signal_id -from copy import deepcopy # noqa +from copy import deepcopy # noqa import traceback from pandas.tseries.offsets import BDay from EventDriven.types import EventTypes, SignalTypes from EventDriven.configs.core import BacktesterConfig +from EventDriven.liquidity import LiquidityPolicy LOGGER = setup_logger("EventDriven.backtest", stream_log_level="WARNING") @@ -200,6 +201,7 @@ def __init__( ## Tracker for eq_strategy runs, to ensure we only run the strategy once per date self.run_dates: Dict[pd.Timestamp, bool] = {} + self._finalize_close_injected_dates: set[str] = set() if config is not None and not isinstance(config, BacktesterConfig): raise TypeError("config must be an instance of BacktesterConfig or None") @@ -207,6 +209,7 @@ def __init__( BacktesterConfig, config if config is not None else BacktesterConfig(), ) + self.liquidity_policy = LiquidityPolicy(self.config.liquidity) self.end_date = end_date if self.is_eq_strategy: self.logger.info("Initializing backtest with equity strategy. Trades DataFrame will be ignored.") @@ -256,12 +259,13 @@ def __init__with_equity_strategy( start_date=start_date, end_date=end_date, ) - self.executor = SimulatedExecutionHandler(self.eventScheduler) + self.executor = SimulatedExecutionHandler(self.eventScheduler, liquidity_policy=self.liquidity_policy) self.risk_manager = RiskManager( symbol_list=self.bars.symbol_list, bkt_start=start_date, bkt_end=end_date, initial_capital=cash, + liquidity_policy=self.liquidity_policy, ) self.portfolio = OptionSignalPortfolio( self.bars, @@ -339,12 +343,13 @@ def __construct_data(self, trades: pd.DataFrame, initial_capital: int, symbol_li end_date=self.end_date, ) self.strategy = OptionSignalStrategy(self.bars, self.eventScheduler) - self.executor = SimulatedExecutionHandler(self.eventScheduler) + self.executor = SimulatedExecutionHandler(self.eventScheduler, liquidity_policy=self.liquidity_policy) self.risk_manager = RiskManager( symbol_list=self.bars.symbol_list, bkt_start=self.start_date, bkt_end=self.end_date, initial_capital=initial_capital, + liquidity_policy=self.liquidity_policy, ) self.portfolio = OptionSignalPortfolio( @@ -374,7 +379,7 @@ def __handle_t_plus_n(self, trades: pd.DataFrame) -> pd.DataFrame: ) ) ## Adjust ExitTime by t_plus_n business days, and offseting to next business day if holiday return trades - + def reset(self): """ Resets the backtest to its initial state, allowing for a fresh run with the original trades and configuration. @@ -389,6 +394,55 @@ def reset(self): end_date=self.end_date, symbol_list=self.symbol_list, ) + + def _is_last_backtest_day(self) -> bool: + """ + Returns True if the scheduler is currently on the final backtest date. + """ + if self.eventScheduler.current_date is None: + return False + + current_str = to_datetime(self.eventScheduler.current_date).strftime("%Y-%m-%d") + end_str = to_datetime(self.eventScheduler.end_date).strftime("%Y-%m-%d") + return current_str == end_str + + def _inject_finalize_close_signals_for_current_day(self) -> int: + """ + On the final backtest day, force CLOSE signals for all open positions. + Returns the number of CLOSE signals successfully scheduled. + """ + if not self.is_eq_strategy or not self.config.finalize_trades: + return 0 + if self.eventScheduler.current_date is None: + return 0 + + day_key = self.eventScheduler.current_date + if day_key in self._finalize_close_injected_dates: + return 0 + + current_dt = to_datetime(self.eventScheduler.current_date) + injected = 0 + + for symbol, positions_by_signal in self.portfolio.current_positions.items(): + for signal_id, position_pack in positions_by_signal.items(): + if "position" not in position_pack: + continue + if "exit_price" in position_pack: + continue + + close_signal = SignalEvent( + symbol=symbol, + datetime=current_dt, + signal_type=SignalTypes.CLOSE.value, + signal_id=signal_id, + parent_event=None, + ) + if self.eventScheduler.schedule_event(current_dt, close_signal): + injected += 1 + + self._finalize_close_injected_dates.add(day_key) + return injected + def _pre_signal_analysis(self): """ Placeholder for any analysis or operations that need to be performed before signal processing in each loop iteration. @@ -397,14 +451,14 @@ def _pre_signal_analysis(self): has_run_strategy = self.run_dates.get(self.eventScheduler.current_date, False) if self.is_eq_strategy and self.eq_strategy.tplusn == 0 and not has_run_strategy: - ## For equity strategy, we want to run the strategy at the beginning of the loop before processing any events, - ## to ensure that we capture signals generated for the current date in the same loop iteration. If we put it after get_nowait, + ## For equity strategy, we want to run the strategy at the beginning of the loop before processing any events, + ## to ensure that we capture signals generated for the current date in the same loop iteration. If we put it after get_nowait, ## we might miss signals generated for the current date until the next loop iteration, which could lead to delayed signal processing and execution self.logger.info(f"Running equity strategy with T+0 settlement on {self.eventScheduler.current_date}") self.portfolio.analyze_multiasset_strategy() self.run_dates[self.eventScheduler.current_date] = True self.logger.info(f"Completed running equity strategy for {self.eventScheduler.current_date}") - + def _post_signal_analysis(self): """ Placeholder for any analysis or operations that need to be performed after signal processing in each loop iteration. @@ -413,12 +467,30 @@ def _post_signal_analysis(self): meta = self.portfolio.analyze_positions() # noqa self.logger.info(f"Position Analysis Meta: {meta}") - ## For equity strategy with T+n (n>=1), we want to run the strategy after analyzing positions, - ## to ensure that we are using the most up-to-date position information for the strategy analysis. - ## This is especially important for T+1 strategies, where the signals generated on the current date will only be actionable on the next business day. + # On final backtest day, finalize any open positions and avoid scheduling new + # forward-settled equity signals that may fall outside the configured range. + if self.is_eq_strategy and self._is_last_backtest_day(): + injected = self._inject_finalize_close_signals_for_current_day() + if injected > 0: + self.logger.info( + f"Injected {injected} forced CLOSE signal(s) on final backtest day {self.eventScheduler.current_date}" + ) + + if self.eq_strategy.tplusn >= 1: + self.logger.info( + f"Skipping equity strategy run on final backtest day {self.eventScheduler.current_date} " + f"for T+{self.eq_strategy.tplusn} settlement." + ) + return + + ## For equity strategy with T+n (n>=1), we want to run the strategy after analyzing positions, + ## to ensure that we are using the most up-to-date position information for the strategy analysis. + ## This is especially important for T+1 strategies, where the signals generated on the current date will only be actionable on the next business day. ## By running the strategy after position analysis, we can ensure that any new signals generated based on the current positions and market data are captured and processed in a timely manner in the next loop iteration. if self.is_eq_strategy and self.eq_strategy.tplusn >= 1: - self.logger.info(f"Running equity strategy with T+{self.eq_strategy.tplusn} settlement on {self.eventScheduler.current_date}") + self.logger.info( + f"Running equity strategy with T+{self.eq_strategy.tplusn} settlement on {self.eventScheduler.current_date}" + ) self.portfolio.analyze_multiasset_strategy() def run(self): @@ -444,7 +516,7 @@ def run(self): # Process events for the current bar # Avoid blocking. Loops through the event queue while True: - self._pre_signal_analysis() # Placeholder for any pre-signal processing logic + self._pre_signal_analysis() # Placeholder for any pre-signal processing logic try: # ## Placing before get_nowait because I want to check for roll, and if there is no roll, I want to break out of the loop @@ -456,7 +528,7 @@ def run(self): if current_event_queue.empty() and not _post_signal_ran: self.logger.info(f"Event queue is empty, processed {event_count} event(s)") - self._post_signal_analysis() + self._post_signal_analysis() _post_signal_ran = True event = current_event_queue.get_nowait() diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 73d7d68..e465d63 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -198,6 +198,18 @@ class PortfolioManagerConfig(BaseConfigs): roll_failed_orders: bool = True # Whether signals that fail to be processed should be rolled forward +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) +class LiquidityConfig(BaseConfigs): + """Centralized liquidity control for both risk and execution layers.""" + + level: int = 1 + max_spread_pct: float = 0.25 + + def __post_init__(self, ctx=None): + super().__post_init__(ctx) + self.level = max(0, min(2, int(self.level))) + + @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) class BacktesterConfig(BaseConfigs): """ @@ -210,6 +222,7 @@ class BacktesterConfig(BaseConfigs): min_slippage_pct: float = 0.075 max_slippage_pct: float = 0.15 commission_per_contract_in_units: float = 0.0065 + liquidity: LiquidityConfig = Field(default_factory=LiquidityConfig) @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) diff --git a/EventDriven/execution.py b/EventDriven/execution.py index d268ba4..0435956 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -1,29 +1,27 @@ import math from typing import Optional import numpy as np +import pandas as pd from abc import ABCMeta, abstractmethod from EventDriven.event import ExerciseEvent, FillEvent, OrderEvent from trade.helpers.helper import parse_option_tick from trade.helpers.Logging import setup_logger from .configs.core import ExecutionHandlerConfig +from EventDriven.liquidity import LiquidityPolicy from copy import deepcopy - -logger = setup_logger('EventDriven.execution') -exec_cache = { - 'order': {}, - 'fill': {}, - 'exercise': {} -} +logger = setup_logger("EventDriven.execution") +exec_cache = {"order": {}, "fill": {}, "exercise": {}} # execution.py + class ExecutionHandler(object): """ The ExecutionHandler abstract class handles the interaction between a set of order objects generated by a Portfolio and the ultimate set of Fill objects that actually occur in the - market. + market. The handlers can be used to subclass simulated brokerages or live brokerages, with identical interfaces. This allows @@ -43,8 +41,8 @@ def execute_order_naively(self, event): event - Contains an Event object with order information. """ raise NotImplementedError("Should implement execute_order()") - - + + class SimulatedExecutionHandler(ExecutionHandler): """ The simulated execution handler simply converts all order @@ -56,8 +54,15 @@ class SimulatedExecutionHandler(ExecutionHandler): before implementation with a more sophisticated execution handler. """ - - def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : float = 0.0065, config: Optional[ExecutionHandlerConfig] = None): + + def __init__( + self, + events, + max_slippage_pct: float = 0.002, + commission_rate: float = 0.0065, + config: Optional[ExecutionHandlerConfig] = None, + liquidity_policy: Optional[LiquidityPolicy] = None, + ): """ Initialises the handler, setting the event queues up internally. @@ -66,15 +71,69 @@ def __init__(self, events, max_slippage_pct : float = 0.002, commission_rate : f events - The Queue of Event objects. max_slippage_pct - The slippage range for the market default is 0.002 commission_rate - The commission rate for the market default is 35 cents per contract. - Option price is in contract units, so commission is also in contract units. For example, if commission_rate is 0.0035, then scaled commission for 1 contract is 0.0035 * 100 = $0.35, + Option price is in contract units, so commission is also in contract units. For example, if commission_rate is 0.0035, then scaled commission for 1 contract is 0.0035 * 100 = $0.35, """ self.events = events self.max_slippage_pct = max_slippage_pct - self.min_slippage_pct = max_slippage_pct / 2 # Minimum slippage is half of max slippage + self.min_slippage_pct = max_slippage_pct / 2 # Minimum slippage is half of max slippage self.commission_rate = commission_rate self.config: ExecutionHandlerConfig = config or ExecutionHandlerConfig() - + self.liquidity_policy = liquidity_policy or LiquidityPolicy() + self.liquidity_drops: list[dict] = [] + self.drop_counts_by_signal_trade: dict[tuple[str, str], int] = {} + + def _spread_pct(self, order_event: OrderEvent) -> Optional[float]: + """Returns spread percentage as spread/close, or None if unavailable.""" + position = order_event.position or {} + spread = position.get("spread") + close = position.get("close") + if spread is None or close in [None, 0]: + return None + try: + return abs(float(spread) / float(close)) + except Exception: + return None + + def _track_drop(self, order_event: OrderEvent, spread_pct: float): + """Track silent drops by signal/trade pair and keep a detailed event log.""" + trade_id = (order_event.position or {}).get("trade_id", "") + key = (str(order_event.signal_id), str(trade_id)) + count = self.drop_counts_by_signal_trade.get(key, 0) + 1 + self.drop_counts_by_signal_trade[key] = count + self.liquidity_drops.append( + { + "signal_id": order_event.signal_id, + "trade_id": trade_id, + "datetime": order_event.datetime, + "spread_pct": spread_pct, + "threshold": self.liquidity_policy.max_spread_pct, + "drop_count_for_pair": count, + } + ) + + def _maybe_drop_for_liquidity_level_2(self, order_event: OrderEvent) -> bool: + """Level 2 liquidity gate: silently drop and reschedule order when spread_pct is too wide.""" + if not self.liquidity_policy.enabled(2): + return False + + spread_pct = self._spread_pct(order_event) + if not self.liquidity_policy.should_drop_for_spread(spread_pct): + return False + + self._track_drop(order_event, spread_pct) + + # Silent drop with retry: move the same order event to the next business day. + next_trading_day = pd.to_datetime(order_event.datetime) + pd.offsets.BusinessDay(1) + next_order_event = deepcopy(order_event) + next_order_event.datetime = next_trading_day + self.events.schedule_event(next_trading_day, next_order_event) + logger.info( + f"Liquidity level-2 drop for signal {order_event.signal_id}, trade {(order_event.position or {}).get('trade_id', '')}. " + f"spread_pct={spread_pct:.4f} > {self.liquidity_policy.max_spread_pct:.4f}. Rescheduled to {next_trading_day}." + ) + return True + def execute_order_naively(self, order_event: OrderEvent): """ Simply converts Order objects into Fill objects naively, @@ -85,44 +144,55 @@ def execute_order_naively(self, order_event: OrderEvent): """ ##TODO: Need to add market_value here - if order_event.type == 'ORDER': - fill_event = FillEvent(order_event.datetime, order_event.symbol, - 'ARCA', order_event.quantity, order_event.direction, fill_cost=0, commission=None, option=order_event.option, parent_event=order_event) + if order_event.type == "ORDER": + fill_event = FillEvent( + order_event.datetime, + order_event.symbol, + "ARCA", + order_event.quantity, + order_event.direction, + fill_cost=0, + commission=None, + option=order_event.option, + parent_event=order_event, + ) self.events.put(fill_event) - - def calculate_slippage_value_randomized(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on a random percentage within the specified slippage range. The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). """ - if order_event.direction == 'BUY': - slippage_pct = np.random.uniform(self.min_slippage_pct, self.max_slippage_pct) ## Ensure that slippage is always positive, and never 0 or more than max_slippage_pct - elif order_event.direction == 'SELL': - slippage_pct = np.random.uniform(-self.max_slippage_pct, -self.min_slippage_pct) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct + if order_event.direction == "BUY": + slippage_pct = np.random.uniform( + self.min_slippage_pct, self.max_slippage_pct + ) ## Ensure that slippage is always positive, and never 0 or more than max_slippage_pct + elif order_event.direction == "SELL": + slippage_pct = np.random.uniform( + -self.max_slippage_pct, -self.min_slippage_pct + ) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct else: raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") - slippage_value = order_event.position['close'] * slippage_pct * order_event.quantity - + slippage_value = order_event.position["close"] * slippage_pct * order_event.quantity + return slippage_value - + def calculate_slippage_value_fixed(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on a fixed percentage defined by max_slippage_pct. The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). """ - if order_event.direction == 'BUY': + if order_event.direction == "BUY": slippage_pct = self.max_slippage_pct - elif order_event.direction == 'SELL': + elif order_event.direction == "SELL": slippage_pct = -self.max_slippage_pct else: raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") - slippage_value = order_event.position['close'] * slippage_pct * order_event.quantity + slippage_value = order_event.position["close"] * slippage_pct * order_event.quantity return slippage_value - + def calculate_slippage_pct_of_spread(self, order_event: OrderEvent) -> float: """ Calculate slippage value as a percentage of the bid-ask spread. @@ -130,67 +200,88 @@ def calculate_slippage_pct_of_spread(self, order_event: OrderEvent) -> float: """ spread = order_event.position.get("spread", None) if spread is None: - raise ValueError("Spread information is required in the order_event position data to calculate slippage as a percentage of the spread.") + raise ValueError( + "Spread information is required in the order_event position data to calculate slippage as a percentage of the spread." + ) spread_slippage = spread * self.config.pct_alpha - if order_event.direction == 'BUY': - slippage = spread_slippage * order_event.quantity - elif order_event.direction == 'SELL': + if order_event.direction == "BUY": + slippage = spread_slippage * order_event.quantity + elif order_event.direction == "SELL": slippage = -spread_slippage * order_event.quantity else: raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") - close = order_event.position['close'] + close = order_event.position["close"] pct_spread_slippage = (spread_slippage / close) if close != 0 else 0 - print(f"Spread: {spread}, Spread Slippage: {spread_slippage}, Total Slippage: {slippage}, Pct Spread Slippage: {pct_spread_slippage}, Signal ID: {order_event.signal_id}, Direction: {order_event.direction}, Date: {order_event.datetime}") + print( + f"Spread: {spread}, Spread Slippage: {spread_slippage}, Total Slippage: {slippage}, Pct Spread Slippage: {pct_spread_slippage}, Signal ID: {order_event.signal_id}, Direction: {order_event.direction}, Date: {order_event.datetime}" + ) return slippage + def calculate_slippage_value(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on the specified slippage model in the config. """ - if self.config.slippage_model == 'randomized': + if self.config.slippage_model == "randomized": return self.calculate_slippage_value_randomized(order_event) - elif self.config.slippage_model == 'fixed': + elif self.config.slippage_model == "fixed": return self.calculate_slippage_value_fixed(order_event) - elif self.config.slippage_model == 'spread_pct': + elif self.config.slippage_model == "spread_pct": return self.calculate_slippage_pct_of_spread(order_event) else: - raise ValueError(f"Invalid slippage model: {self.config.slippage_model}. Must be 'randomized', 'fixed', or 'spread_pct'.") + raise ValueError( + f"Invalid slippage model: {self.config.slippage_model}. Must be 'randomized', 'fixed', or 'spread_pct'." + ) - def execute_order_randomized_slippage(self, order_event: OrderEvent): """ This method will execute an order with a random slippage based on the max_slippage_pct attribute of the class. Note: Quantity takes precedence if both quantity and cash are provided, else quantity is determined by cash / price_of_contract """ - assert order_event.type == 'ORDER', f"Event type must be 'ORDER' received {order_event.type}" - assert order_event.direction == 'BUY' or order_event.direction == 'SELL', f"Event direction must be 'BUY' or 'SELL' received {order_event.direction}" - exec_cache['order'][f'{order_event.signal_id}_{order_event.datetime.strftime("%Y-%m-%d")}'] = deepcopy(order_event) + assert order_event.type == "ORDER", f"Event type must be 'ORDER' received {order_event.type}" + assert order_event.direction == "BUY" or order_event.direction == "SELL", ( + f"Event direction must be 'BUY' or 'SELL' received {order_event.direction}" + ) + + if self._maybe_drop_for_liquidity_level_2(order_event): + return + + exec_cache["order"][f"{order_event.signal_id}_{order_event.datetime.strftime('%Y-%m-%d')}"] = deepcopy( + order_event + ) # Generate slippage as a percentage ## Slippage improvement - if order_event.direction == 'BUY': + if order_event.direction == "BUY": ## We want to increase the price for buys by slippage - slippage_pct = np.random.uniform(self.min_slippage_pct, self.max_slippage_pct) ## Ensure that slippage is always positive, and never 0 or more than max_slippage_pct - elif order_event.direction == 'SELL': + slippage_pct = np.random.uniform( + self.min_slippage_pct, self.max_slippage_pct + ) ## Ensure that slippage is always positive, and never 0 or more than max_slippage_pct + elif order_event.direction == "SELL": ## We want to decrease the price for sells by slippage - slippage_pct = np.random.uniform(-self.max_slippage_pct, -self.min_slippage_pct) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct - + slippage_pct = np.random.uniform( + -self.max_slippage_pct, -self.min_slippage_pct + ) ## Ensure that slippage is always negative, and never 0 or less than -max_slippage_pct + slippage_pct_value = self.calculate_slippage_value(order_event) slippage_per_contract = slippage_pct_value / order_event.quantity if order_event.quantity != 0 else 0 - slippage_pct = slippage_per_contract / order_event.position['close'] if order_event.position['close'] != 0 else 0 - - - #slippage may increase or decrease intended price - price = order_event.position['close'] * (1 + slippage_pct) - + slippage_pct = ( + slippage_per_contract / order_event.position["close"] if order_event.position["close"] != 0 else 0 + ) + + # slippage may increase or decrease intended price + price = order_event.position["close"] * (1 + slippage_pct) + # Ensuring cash doesn't go below zero raw_quantity = order_event.quantity - + try: if raw_quantity is not None: # Recompute quantity downward if cost exceeds available cash unit_cost = price + self.commission_rate - logger.info(f"Unit cost: {unit_cost}, Cash available: {order_event.cash}, Direction: {order_event.direction}, Signal ID: {order_event.signal_id}") - if order_event.direction == 'BUY': + logger.info( + f"Unit cost: {unit_cost}, Cash available: {order_event.cash}, Direction: {order_event.direction}, Signal ID: {order_event.signal_id}" + ) + if order_event.direction == "BUY": max_affordable_quantity = math.floor(order_event.cash / unit_cost) # Ensure we never exceed max affordable quantity @@ -201,9 +292,11 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): while total_cost > order_event.cash: quantity -= 1 total_cost = quantity * price + self.commission_rate - logger.info(f"Max affordable quantity: {max_affordable_quantity}, Raw quantity: {raw_quantity}, Final quantity: {quantity}, Signal ID: {order_event.signal_id}, Total Cost: {quantity * price + self.commission_rate}, Cash: {order_event.cash}") + logger.info( + f"Max affordable quantity: {max_affordable_quantity}, Raw quantity: {raw_quantity}, Final quantity: {quantity}, Signal ID: {order_event.signal_id}, Total Cost: {quantity * price + self.commission_rate}, Cash: {order_event.cash}" + ) - elif order_event.direction == 'SELL': + elif order_event.direction == "SELL": # For SELL, we can only sell what we have in the position quantity = raw_quantity else: @@ -212,15 +305,19 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): except: pass - commission = self.commission_rate * quantity * (len(order_event.position.get('trade_id', '&L:').split('&')) - 1) #commission is per trade(leg) there should always be a long in a position, naked or spread + commission = ( + self.commission_rate * quantity * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) + ) # commission is per trade(leg) there should always be a long in a position, naked or spread - market_value = (order_event.position['close'] * quantity) # market value is based on the position's close price, not the slippage adjusted price - # This is to ensure that the market value is not affected by slippage, as slippage is a cost incurred after the market value is determined. + market_value = ( + order_event.position["close"] * quantity + ) # market value is based on the position's close price, not the slippage adjusted price + # This is to ensure that the market value is not affected by slippage, as slippage is a cost incurred after the market value is determined. # Adjust price based on order direction - if order_event.direction == 'BUY': - fill_cost = (price * quantity) + commission ## Total Cost for BUY includes commission and slippage - elif order_event.direction == 'SELL': + if order_event.direction == "BUY": + fill_cost = (price * quantity) + commission ## Total Cost for BUY includes commission and slippage + elif order_event.direction == "SELL": fill_cost = (price * quantity) - commission ##NOTE: @@ -228,66 +325,76 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): # - Fill cost is based on the slippage adjusted price and commission. This serves as entry cost # - Market value != fill cost, as market value is based on the position's close price, not the slippage adjusted price. - slippage_diff = (price - order_event.position['close'] ) * quantity + slippage_diff = (price - order_event.position["close"]) * quantity fill_event = FillEvent( - order_event.datetime, - order_event.symbol, - 'ARCA', - quantity, - order_event.direction, - fill_cost=fill_cost, - market_value=market_value, - commission=commission, - position=order_event.position, - slippage=slippage_diff, - signal_id=order_event.signal_id, - parent_event=order_event + order_event.datetime, + order_event.symbol, + "ARCA", + quantity, + order_event.direction, + fill_cost=fill_cost, + market_value=market_value, + commission=commission, + position=order_event.position, + slippage=slippage_diff, + signal_id=order_event.signal_id, + parent_event=order_event, ) - exec_cache['fill'][f'{order_event.signal_id}_{order_event.datetime.strftime("%Y-%m-%d")}_{order_event.direction}'] = deepcopy(fill_event) + exec_cache["fill"][ + f"{order_event.signal_id}_{order_event.datetime.strftime('%Y-%m-%d')}_{order_event.direction}" + ] = deepcopy(fill_event) self.events.put(fill_event) - + def execute_exercise(self, exercise_event: ExerciseEvent): """ This method will execute an exercise event, calculate the pnl of the exercise and put a fill event on the queue """ - assert exercise_event.type == 'EXERCISE', f"Event type must be 'EXERCISE' received {exercise_event.type}" + assert exercise_event.type == "EXERCISE", f"Event type must be 'EXERCISE' received {exercise_event.type}" long_pnl = 0.0 short_pnl = 0.0 - if exercise_event.long_premiums and 'long' in exercise_event.position: - assert all(option in exercise_event.position['long'] for option in exercise_event.long_premiums.keys()), f"option_id in premiums must be present in position. long_premium: {exercise_event.long_premiums.keys()} long position: {exercise_event.position['long']}" - assert len(exercise_event.long_premiums) == len(exercise_event.position['long']), f"number of options in long_premiums must be equal to number of options in long position. long_premium: {len(exercise_event.long_premiums)} long position: {len(exercise_event.position['long'])}" + if exercise_event.long_premiums and "long" in exercise_event.position: + assert all(option in exercise_event.position["long"] for option in exercise_event.long_premiums.keys()), ( + f"option_id in premiums must be present in position. long_premium: {exercise_event.long_premiums.keys()} long position: {exercise_event.position['long']}" + ) + assert len(exercise_event.long_premiums) == len(exercise_event.position["long"]), ( + f"number of options in long_premiums must be equal to number of options in long position. long_premium: {len(exercise_event.long_premiums)} long position: {len(exercise_event.position['long'])}" + ) for option_id, premium in exercise_event.long_premiums.items(): option_meta = parse_option_tick(option_id) long_pnl += self.__calculate_premium_pnl(option_meta, exercise_event.spot, premium) - - if exercise_event.short_premiums and 'short' in exercise_event.position: - assert all(option in exercise_event.position['short'] for option in exercise_event.short_premiums.keys()), f"option_id in premiums must be present in position. short_premium: {exercise_event.short_premiums.keys()} short position: {exercise_event.position['short']}" - assert len(exercise_event.short_premiums) == len(exercise_event.position['short']), f"number of options in short_premiums must be equal to number of options in short position. short_premium: {len(exercise_event.short_premiums)} short position: {len(exercise_event.position['short'])}" + + if exercise_event.short_premiums and "short" in exercise_event.position: + assert all(option in exercise_event.position["short"] for option in exercise_event.short_premiums.keys()), ( + f"option_id in premiums must be present in position. short_premium: {exercise_event.short_premiums.keys()} short position: {exercise_event.position['short']}" + ) + assert len(exercise_event.short_premiums) == len(exercise_event.position["short"]), ( + f"number of options in short_premiums must be equal to number of options in short position. short_premium: {len(exercise_event.short_premiums)} short position: {len(exercise_event.position['short'])}" + ) for option_id, premium in exercise_event.short_premiums.items(): option_meta = parse_option_tick(option_id) short_pnl += self.__calculate_premium_pnl(option_meta, exercise_event.spot, premium) - + total_pnl = long_pnl + short_pnl market_value = exercise_event.spot * exercise_event.quantity - + fill_event = FillEvent( - exercise_event.datetime, - exercise_event.symbol, - 'ARCA', - exercise_event.quantity, - 'EXERCISE', - fill_cost=total_pnl, + exercise_event.datetime, + exercise_event.symbol, + "ARCA", + exercise_event.quantity, + "EXERCISE", + fill_cost=total_pnl, position=exercise_event.position, - market_value=market_value, - signal_id=exercise_event.signal_id, - parent_event=exercise_event + market_value=market_value, + signal_id=exercise_event.signal_id, + parent_event=exercise_event, ) self.events.put(fill_event) - + def __calculate_premium_pnl(self, option_meta, spot, premium): - if option_meta['option_type'] == 'C': - return max(0, spot - option_meta['strike']) - premium - elif option_meta['option_type'] == 'P': - return max(0, option_meta['strike'] - spot) - premium + if option_meta["option_type"] == "C": + return max(0, spot - option_meta["strike"]) - premium + elif option_meta["option_type"] == "P": + return max(0, option_meta["strike"] - spot) - premium else: - raise ValueError(f"Invalid option type: {option_meta['option_type']}") \ No newline at end of file + raise ValueError(f"Invalid option type: {option_meta['option_type']}") diff --git a/EventDriven/liquidity.py b/EventDriven/liquidity.py new file mode 100644 index 0000000..cb84116 --- /dev/null +++ b/EventDriven/liquidity.py @@ -0,0 +1,27 @@ +"""Liquidity policy utilities shared across risk and execution layers.""" + +from typing import Optional +from EventDriven.configs.core import LiquidityConfig + + +class LiquidityPolicy: + """Encapsulates liquidity-level decisions from a shared LiquidityConfig.""" + + def __init__(self, config: Optional[LiquidityConfig] = None): + self.config = config or LiquidityConfig() + + @property + def level(self) -> int: + return self.config.level + + @property + def max_spread_pct(self) -> float: + return self.config.max_spread_pct + + def enabled(self, required_level: int) -> bool: + return self.level >= required_level + + def should_drop_for_spread(self, spread_pct: Optional[float]) -> bool: + if spread_pct is None: + return False + return self.enabled(2) and spread_pct > self.max_spread_pct diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index ec4dd4b..00037ae 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -208,6 +208,7 @@ from EventDriven.types import ResultsEnum, Order from EventDriven.configs.core import RiskManagerConfig from EventDriven._vars import CONTRACT_MULTIPLIER, load_riskmanager_cache +from EventDriven.liquidity import LiquidityPolicy from pprint import pprint logger = setup_logger("EventDriven.riskmanager.new_base", stream_log_level="WARNING") @@ -288,6 +289,7 @@ def __init__( ## Configs self.config = RiskManagerConfig() + self.liquidity_policy = kwargs.pop("liquidity_policy", None) or LiquidityPolicy() ## Get start and end dates for timeseries loading start, end = get_timeseries_start_end() @@ -379,7 +381,7 @@ def _liquity_multiplier(self, pct_ratio: float, good: Optional[float] = None, ba decay = np.exp(-5 * x) # Exponential decay factor multiplier = round(0.25 + 0.75 * decay, 4) # Scale to [0.25, 1.0] return multiplier - + def _quantiy_liquidity_adjustment(self, quantity: int, pct_ratio: float) -> int: """ Adjust the order quantity based on the liquidity of the option, as measured by the bid-ask spread percentage. @@ -443,10 +445,7 @@ def get_order(self, req: OrderRequest) -> NewPositionState: req.spot = spot req.chain_spot = chain_spot req.delta_lmt = self.position_analyzer.get_delta_limit( - tick_cash=req.tick_cash, - chain_spot=chain_spot, - date=req.date, - ticker=req.symbol + tick_cash=req.tick_cash, chain_spot=chain_spot, date=req.date, ticker=req.symbol ) ## Get order @@ -520,8 +519,8 @@ def get_order(self, req: OrderRequest) -> NewPositionState: order = updated_pos_state.order.to_dict() - ## Update order for liquidity - if not order_failed(order) and q > 0: + ## Level 1 liquidity measure: quantity haircut by spread_pct_ratio + if not order_failed(order) and q > 0 and self.liquidity_policy.enabled(1): pct_ratio = updated_pos_state.order["metrics"]["spread_pct_ratio"] q = self._quantiy_liquidity_adjustment(q, pct_ratio) updated_pos_state.order.data["quantity"] = q From 7b67d3fadbfc2f49a59eb0daab5f0bfc5fc54b48 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:26:18 -0400 Subject: [PATCH 27/81] refactor: Market data continuity & code hygiene - Add _ffill_adj_strike_business_days() for continuous adjusted-strike cache - Reindex strike data on explicit business-day range to prevent data gaps - Update .gitignore with debug/ folder rules (excludes development utilities) - Clean trailing whitespace and improve formatting in utils.py --- .gitignore | 6 ++- EventDriven/riskmanager/market_timeseries.py | 44 +++++++++++++++----- EventDriven/riskmanager/utils.py | 12 +++--- 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index fe59e85..4c7717d 100644 --- a/.gitignore +++ b/.gitignore @@ -212,4 +212,8 @@ trade/assets/notebooks .decomission/ .decommission/ EventDriven/riskmanager/.decomission/ -EventDriven/riskmanager/.decommission/ \ No newline at end of file +EventDriven/riskmanager/.decommission/ + +# Local debug folders (keep out of remote) +debug/ +**/debug/ \ No newline at end of file diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index f8c6317..219d75b 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -469,6 +469,23 @@ def load_position_data(self, opttick) -> pd.DataFrame: # noqa opttick=opttick, processed_option_data=self.options_cache, start=self.start_date, end=self.end_date ) + def _ffill_adj_strike_business_days( + self, + strike_series: pd.Series, + start_date: Union[datetime, str], + end_date: Union[datetime, str], + ) -> pd.Series: + """Forward-fill adjusted strike values across business days only.""" + if strike_series is None or strike_series.empty: + return strike_series + + series = strike_series.copy() + series.index = pd.DatetimeIndex(to_datetime(series.index)).normalize() + series = series[~series.index.duplicated(keep="last")].sort_index() + + bday_index = pd.bdate_range(start=to_datetime(start_date), end=to_datetime(end_date)) + return series.reindex(bday_index).ffill() + def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: """ Generate option data for a given trade. @@ -523,11 +540,15 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: data = self.load_position_data(opttick).copy() ## Copy to avoid modifying the original data data["adj_strike"] = meta["strike"] data["factor"] = 1.0 - self.adjusted_strike_cache[opttick] = data["adj_strike"] - return data[ - (data.index >= pd.to_datetime(self.start_date) - relativedelta(months=3)) - & (data.index <= pd.to_datetime(effective_end) + relativedelta(months=3)) - ] + window_start = to_datetime(self.start_date) - relativedelta(months=3) + window_end = to_datetime(effective_end) + relativedelta(months=3) + data = data[(data.index >= window_start) & (data.index <= window_end)] + self.adjusted_strike_cache[opttick] = self._ffill_adj_strike_business_days( + data["adj_strike"], + start_date=window_start, + end_date=window_end, + ) + return data # If there are splits, we need to load the data for each tick after adjusting strikes else: @@ -601,9 +622,12 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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.start_date) - relativedelta(months=3)) - & (final_data.index <= pd.to_datetime(effective_end) + relativedelta(months=3)) - ] - self.adjusted_strike_cache[opttick] = final_data["adj_strike"] + window_start = to_datetime(self.start_date) - relativedelta(months=3) + window_end = to_datetime(effective_end) + relativedelta(months=3) + final_data = final_data[(final_data.index >= window_start) & (final_data.index <= window_end)] + self.adjusted_strike_cache[opttick] = self._ffill_adj_strike_business_days( + final_data["adj_strike"], + start_date=window_start, + end_date=window_end, + ) return final_data diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index e81f411..e8cdbb6 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -210,7 +210,7 @@ TIMESERIES_START = pd.to_datetime(OPTION_TIMESERIES_START_DATE) _NOW = datetime.now() # Set end date to one year ago to avoid lookahead bias and ensure we have enough data for backtesting -TIMESERIES_END = _NOW.replace(year=_NOW.year - 1, month=12, day=31).strftime("%Y-%m-%d") +TIMESERIES_END = _NOW.replace(year=_NOW.year - 1, month=12, day=31).strftime("%Y-%m-%d") LOOKBACKS = {} ## Paths @@ -425,9 +425,9 @@ def get_cache(name: str) -> CustomCache: # @dynamic_memoize def populate_cache_with_chain( - tick, - date, - chain_spot=None, + tick, + date, + chain_spot=None, print_url=True, add_greeks=False, ): @@ -498,7 +498,9 @@ def save_to_cache(id, date, spot): ] ## Filter out extreme moneyness to reduce size chain_clipped.columns = chain_clipped.columns.str.lower() chain_clipped["pct_spread"] = (chain_clipped["closeask"] - chain_clipped["closebid"]) / chain_clipped["midpoint"] - chain_clipped[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = np.nan # Placeholder for Greeks, to be filled in later when we have the data + chain_clipped[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = ( + np.nan + ) # Placeholder for Greeks, to be filled in later when we have the data return chain_clipped From d42aea0e8a26fae6c6fa896db947031cec27b777 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:46:54 -0400 Subject: [PATCH 28/81] feat: honor preset orders in order picker get_order - Check get_preset_order() first in get_order() - Return preset order directly when available - Normalize preset payload date/quantity to Order shape - Use helper to_datetime for date conversions --- EventDriven/riskmanager/picker/order_picker.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index ae09144..bf707e0 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -25,7 +25,6 @@ from copy import deepcopy from datetime import datetime -import pandas as pd from trade.datamanager.market_data import Optional from ..utils import ( LOOKBACKS, @@ -41,6 +40,7 @@ from EventDriven.dataclasses.orders import OrderRequest from EventDriven.types import Order import numpy as np +from trade.helpers.helper import to_datetime logger = setup_logger("EventDriven.riskmanager.picker.order_picker") @@ -103,7 +103,7 @@ def register_preset_order(self, signal_id: str, trade_id: str, date: str | datet """ self.preset_orders[signal_id] = { "trade_id": trade_id, - "date": pd.to_datetime(date, format="%Y-%m-%d").date(), + "date": to_datetime(date, format="%Y-%m-%d").date(), "close_price": close_price, } @@ -120,7 +120,7 @@ def get_preset_order(self, signal_id: str, date: str | datetime) -> dict: 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(): + if preset_order and preset_order["date"] == 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 { @@ -179,6 +179,13 @@ def get_order(self, request: OrderRequest) -> Order: """ Get the order based on the request. """ + preset_order = self.get_preset_order(signal_id=request.signal_id, date=request.date) + if preset_order: + if preset_order.get("data") is not None and "quantity" not in preset_order["data"]: + preset_order["data"]["quantity"] = 1 + preset_order["date"] = to_datetime(request.date).date() + return Order.from_dict(preset_order) + tick_cash = request.tick_cash if not request.is_tick_cash_scaled else request.tick_cash / 100 if request.max_close > tick_cash: logger.warning( @@ -195,7 +202,7 @@ def get_order(self, request: OrderRequest) -> Order: logger.warning(f"Order failed to resolve for request: {request}") return Order.from_dict(order) order["data"]["quantity"] = 1 - order["date"] = pd.to_datetime(request.date).date() + order["date"] = to_datetime(request.date).date() order = Order.from_dict(order) return order From 27fbf1860f7b973af35abbf6ef1251b4a4389ca1 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:47:02 -0400 Subject: [PATCH 29/81] feat: add trade timeseries lookup to portfolio - add get_trade_timeseries(trade_id, signal_id=None) - resolve signal_id from aggregated trades when unambiguous - slice position market data by entry/exit dates - use backtest end date for still-open trades --- EventDriven/new_portfolio.py | 55 ++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index d762a99..4a36b05 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -1248,6 +1248,61 @@ def calculate_spread_on_position(self, position_id: TradeID, date=None) -> float position_id=position_id, date=date or self.eventScheduler.current_date ).get_spread() + def get_trade_timeseries(self, trade_id: str, signal_id: Optional[str] = None) -> pd.DataFrame: + """Return position timeseries for a trade, bounded by entry and exit dates. + + If ``signal_id`` is omitted, it is inferred from aggregated trade rows for the + provided ``trade_id``. If inference is ambiguous, a ValueError is raised. + If the trade has no exit date, the backtest end date is used. + """ + # Use normalized trade rows as the single source of entry/exit bounds. + trades_df = self.aggregate_trades(level=AggregationLevel.BY_TRADE_SIGNAL) + if trades_df is None or trades_df.empty: + raise ValueError("No trades available in portfolio") + + trade_rows = trades_df[trades_df["TradeID"] == trade_id] + if trade_rows.empty: + raise ValueError(f"TradeID {trade_id} not found in portfolio trades") + + # If signal_id is omitted, it must resolve to exactly one candidate. + if signal_id is None: + signal_ids = trade_rows["SignalID"].dropna().unique().tolist() + if len(signal_ids) != 1: + raise ValueError( + f"signal_id cannot be inferred for trade_id {trade_id}; multiple signal_ids found: {signal_ids}" + ) + signal_id = signal_ids[0] + + trade_rows = trade_rows[trade_rows["SignalID"] == signal_id] + if trade_rows.empty: + raise ValueError(f"TradeID {trade_id} with signal_id {signal_id} not found in portfolio trades") + + row = trade_rows.iloc[0] + entry_time = row.get("EntryTime") + if pd.isna(entry_time): + raise ValueError(f"TradeID {trade_id} with signal_id {signal_id} has no entry time") + + # Open trades are sliced through configured backtest end date. + exit_time = row.get("ExitTime") + if pd.isna(exit_time): + exit_time = self.eventScheduler.end_date + + start_dt = pd.Timestamp(to_datetime(entry_time)) + end_dt = pd.Timestamp(to_datetime(exit_time)) + + position_data = self.risk_manager.market_data.get_position_data(position_id=trade_id) + if position_data.empty: + return position_data + + # Normalize index to daily keys before date-window slicing. + position_slice = position_data.copy() + if isinstance(position_slice.index, pd.DatetimeIndex): + position_slice.index = position_slice.index.normalize() + else: + position_slice.index = pd.DatetimeIndex(to_datetime(position_slice.index)).normalize() + + return position_slice.loc[start_dt.normalize() : end_dt.normalize()] + def get_at_time_position_data(self, position_id: TradeID, date=None) -> AtTimePositionData: """ Get the position data at a given time From e5ac99d4384aea9e3030f818dc9ef59430f950dd Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:47:14 -0400 Subject: [PATCH 30/81] feat: add daily attribution aggregation mode - extend convert_attribution_to_df with groupby='daily' - aggregate attribution rows by date index - drop signal_id and trade_id in daily mode - keep existing signal and trade aggregation behavior --- EventDriven/attribution.py | 41 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index 788d9cf..6926909 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -180,10 +180,9 @@ def _get_trade_quantity_time_series( signal_id=trade_obj.signal_id, daily_qty=qty_frame["cumulative_qty"], quantity_change=qty_frame["qty_change"], - ## Scale everything to per-unit exec_price=qty_frame["per_unit_market_value"] / 100, - commission=abs(qty_frame["per_unit_commission"].fillna(0)/100), + commission=abs(qty_frame["per_unit_commission"].fillna(0) / 100), slippage=abs(qty_frame["per_unit_slippage"].fillna(0) / 100), trade_entry=trade_entry, trade_exit=trade_exit, @@ -280,7 +279,7 @@ def compute_position_attribution( attribution = attribution.copy() commission = qty_ts.commission slippage = qty_ts.slippage - + ## Ensure attribution has necessary columns, if not create them with default values if "commission_cost" not in attribution.columns: attribution["commission_cost"] = commission.fillna(0) @@ -303,13 +302,13 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: """ if qty > 0: # OPEN: entry is execution price on this date, close is current position price - entry_p = abs(exec_price.loc[date]) #+ slippage.loc[date] + commission.loc[date] + entry_p = abs(exec_price.loc[date]) # + slippage.loc[date] + commission.loc[date] close_p = get_position_price_func(_id=trade_id, date=date, force=True) else: # CLOSE: entry is previous day's position price, close is execution price on this date prev_date = change_to_last_busday(date - BDay(1)) entry_p = get_position_price_func(_id=trade_id, date=prev_date, force=True) - close_p = abs(exec_price.loc[date]) #- slippage.loc[date] - commission.loc[date] + close_p = abs(exec_price.loc[date]) # - slippage.loc[date] - commission.loc[date] pnl = (close_p - entry_p) * abs(qty) return pnl, entry_p, close_p @@ -354,7 +353,12 @@ def _compute_pnl_for_change(date, qty) -> Tuple[float, float, float]: logger.info( f"Date: {date.date()}, Qty: {qty_change}, Entry: {entry_p}, Close: {close_p}, PnL: {trade_pnl}, PrevQty: {prev_qty}, Commission: {commission_cost}, Slippage: {slippage_cost}" ) - attribution["opt_plus_adj"] = attribution["opt_dod_change"] + attribution["trade_pnl_adjustment"] + attribution["commission_cost"] + attribution["slippage_cost"] + attribution["opt_plus_adj"] = ( + attribution["opt_dod_change"] + + attribution["trade_pnl_adjustment"] + + attribution["commission_cost"] + + attribution["slippage_cost"] + ) attribution = attribution[ [ "opt_dod_change", @@ -449,7 +453,9 @@ def analyze_trade(self, trade_id: TradeID, signal_id: SignalID, force: bool = Fa """ trade_key = self.portfolio._get_trade_key(trade_id, signal_id) if trade_key not in self.attribution_cache or force: - self.attribution_cache[trade_key] = compute_backtest_position_attribution(self.portfolio, trade_id, signal_id) + self.attribution_cache[trade_key] = compute_backtest_position_attribution( + self.portfolio, trade_id, signal_id + ) return self.attribution_cache[trade_key] def analyze_all_trades(self, force: bool = False) -> Dict[Tuple[TradeID, SignalID], BacktestPositionAttribution]: @@ -463,15 +469,17 @@ def analyze_all_trades(self, force: bool = False) -> Dict[Tuple[TradeID, SignalI """ for trade_key, trade_obj in tqdm(self.portfolio.trades_map.items(), desc="Analyzing trades"): if trade_key not in self.attribution_cache or force: - self.attribution_cache[trade_key] = compute_backtest_position_attribution(self.portfolio, trade_obj.trade_id, trade_obj.signal_id) + self.attribution_cache[trade_key] = compute_backtest_position_attribution( + self.portfolio, trade_obj.trade_id, trade_obj.signal_id + ) return self.attribution_cache def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: bool = False) -> pd.DataFrame: """Convert cached attributions to a grouped summary DataFrame. Args: - groupby: Column by which to group results. Must be ``"signal_id"`` or - ``"trade_id"``. + groupby: Aggregation mode. Must be ``"signal_id"``, ``"trade_id"``, + or ``"daily"``. ignore_missing: If True, skips trades without computed attributions. If False, raises an error for any missing trades. @@ -482,9 +490,12 @@ def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: Raises: ValueError: If no attributions have been computed yet. ValueError: If ``ignore_missing=False`` and any trades are missing attributions. - AssertionError: If ``groupby`` is not ``"signal_id"`` or ``"trade_id"``. + AssertionError: If ``groupby`` is not ``"signal_id"``, ``"trade_id"``, + or ``"daily"``. """ - assert groupby in ["signal_id", "trade_id"], "groupby must be either 'signal_id' or 'trade_id'" + assert groupby in ["signal_id", "trade_id", "daily"], ( + "groupby must be one of 'signal_id', 'trade_id', or 'daily'" + ) if not self.attribution_cache: raise ValueError("No attributions computed yet. Please run analyze_all_trades first.") if not ignore_missing: @@ -502,5 +513,9 @@ def convert_attribution_to_df(self, groupby: str = "signal_id", ignore_missing: combined_df = pd.concat(records) if groupby == "signal_id": return combined_df.drop(columns=["trade_id"]).groupby("signal_id").sum() * 100 - else: + if groupby == "trade_id": return combined_df.drop(columns=["signal_id"]).groupby("trade_id").sum() * 100 + + # Daily aggregation drops both IDs and sums across all trades/signals per date. + daily_df = combined_df.drop(columns=["signal_id", "trade_id"]) + return daily_df.groupby(daily_df.index).sum() * 100 From e55857bd5bec5a0fcb1e6e1c22cf717e72444c69 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:48:45 -0400 Subject: [PATCH 31/81] fix: apply pct_alpha when computing execution spread ratio Scale the execution-time spread ratio by execution config pct_alpha before liquidity checks. This keeps the spread threshold comparison aligned with the configured execution sensitivity instead of using the raw spread/close ratio directly. --- EventDriven/execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/EventDriven/execution.py b/EventDriven/execution.py index 0435956..a048aae 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -91,7 +91,7 @@ def _spread_pct(self, order_event: OrderEvent) -> Optional[float]: if spread is None or close in [None, 0]: return None try: - return abs(float(spread) / float(close)) + return abs(float(spread) / float(close) * self.config.pct_alpha) except Exception: return None From 1f4afe2f7d32328a19a4933c07c8eb613d2dd853 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:48:54 -0400 Subject: [PATCH 32/81] feat: store realized-vol metadata for sized positions - deduplicate symbol inputs before loading z-score scaler data - expose get_rvol_on_date() on the z-score scaler - capture per-trade rvol in limits metadata when non-default sizing is used - normalize underlier symbol lists to unique values before metadata tracking This makes position metadata reflect the realized-vol context that drove size selection and avoids duplicate symbol work in the scaler path. --- .../riskmanager/position/cogs/limits.py | 10 ++++++- EventDriven/riskmanager/sizer/_utils.py | 26 ++++++++++++++++--- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index eeb8bb7..dd77516 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -263,6 +263,7 @@ class _LimitsMetaData: undl_price: Optional[float] = None prev_quantity: Optional[int] = None new_quantity: Optional[int] = None + rvol: Optional[float] = None class LimitsAndSizingCog(BaseCog): @@ -281,7 +282,7 @@ def __init__( self._sizer_configs: Optional[Union[DefaultSizerConfigs, ZscoreSizerConfigs]] = sizer_configs self.position_limits: Dict[str, PositionLimits] = {} self.position_metadata: Dict[str, _LimitsMetaData] = {} - self.underlier_list = underlier_list if underlier_list is not None else [] + self.underlier_list = list(set(underlier_list if underlier_list is not None else [])) if config is None: config = LimitsEnabledConfig() @@ -421,6 +422,11 @@ def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: if isinstance(self.sizer, DefaultSizer) else self.sizer.scaler.get_scaler_on_date(sym=new_pos_state.symbol, date=request.date) ) + rvol = ( + None + if isinstance(self.sizer, DefaultSizer) + else self.sizer.scaler.get_rvol_on_date(sym=new_pos_state.symbol, date=request.date) + ) metadata = _LimitsMetaData( trade_id=order["data"]["trade_id"], date=request.date, @@ -432,6 +438,8 @@ def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: undl_price=undl_data.chain_spot["close"], delta_lmt=new_pos_state.limits.delta, new_quantity=order["data"]["quantity"], + rvol=rvol, + ) logger.info(f"Storing position metadata: {metadata}") self.position_metadata[order["data"]["trade_id"]] = metadata diff --git a/EventDriven/riskmanager/sizer/_utils.py b/EventDriven/riskmanager/sizer/_utils.py index 2149ecc..5db74b5 100644 --- a/EventDriven/riskmanager/sizer/_utils.py +++ b/EventDriven/riskmanager/sizer/_utils.py @@ -317,6 +317,8 @@ def __post_init__(self): if isinstance(self.rvol_window, list): self.rvol_window = tuple(self.rvol_window) + self.syms = list(set(self.syms)) ## Ensure syms is a list of unique symbols + ## Assert rvol_window is valid self.rvol_window = self.__rvol_window_assert(self.vol_type, self.rvol_window) assert self.vol_type in self.VOL_TYPES, f"vol_type must be one of {self.VOL_TYPES}, got {self.vol_type}" @@ -379,7 +381,7 @@ def load_scalers(self, syms: list = None, force=False) -> None: ## If syms is None, use the existing syms ## This is to avoid reloading timeseries if already loaded if syms is None: - syms = self.syms + syms = set(self.syms) if not isinstance(syms, list): raise TypeError(f"syms must be a list, got {type(syms)}") @@ -403,8 +405,7 @@ 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()) - ts = timeseries.get_timeseries(sym=sym).spot["close"] + ts = timeseries.get_timeseries(sym=sym, start_date=Y2_LAGGED_START_DATE, end_date=datetime.now()).spot["close"] if self.vol_type == "window": func = lambda x: realized_vol(x, self.rvol_window) @@ -479,3 +480,22 @@ def get_scaler_on_date(self, sym: str, date: pd.Timestamp | str | datetime) -> f raise ValueError(f"Date {date} not found in scaler_ts index for symbol {sym}.") return scaler_ts.loc[date] + + def get_rvol_on_date(self, sym: str, date: pd.Timestamp | str | datetime) -> float: + """ + Get the realized volatility for a specific symbol on a specific date. + """ + if isinstance(date, str): + date = pd.Timestamp(date).date() + elif isinstance(date, datetime): + date = date.date() + + if sym not in self.rvol_timeseries: + raise ValueError(f"Symbol {sym} not found in rvol_timeseries.") + + rvol_ts = self.rvol_timeseries[sym] + date = pd.to_datetime(date).strftime("%Y-%m-%d") + if date not in rvol_ts.index: + raise ValueError(f"Date {date} not found in rvol_ts index for symbol {sym}.") + + return rvol_ts.loc[date] From 970ce674b624f34137fefb470b17c0c3bb9ca801 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:49:00 -0400 Subject: [PATCH 33/81] fix: tighten cached date bounds for datamanager queries - cache both min_date and max_date for option date lookups - clamp requested end dates to the actual cached maximum available date - return normalized datetime bounds from _sync_date - register dividends helper logger with datamanager logging config This prevents cached range reuse from widening end dates past known data availability and keeps the related datamanager logs wired into the shared logging controls. --- trade/datamanager/utils/date.py | 9 +++++---- trade/datamanager/utils/logging.py | 3 ++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index 53661cc..f1eea13 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -191,7 +191,7 @@ def _get_max_date(requested_end: datetime) -> datetime: 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) + max_date = cached_dates["max_date"] start_date = max(min_date, start_date) end_date = min(max_date, end_date) @@ -212,12 +212,13 @@ def _get_max_date(requested_end: datetime) -> datetime: ## Adjust start date to min min_date = min(dates) + max_date = max(dates) start_date = max(min_date, start_date) - end_date = _get_max_date(end_date) + end_date = min(_get_max_date(end_date), max_date) - LIST_DATE_CACHE.set(key=opttick, value={"min_date": min_date}, expire=None) + LIST_DATE_CACHE.set(key=opttick, value={"min_date": min_date, "max_date": end_date}, expire=None) - return min(start_date, end_date), max(start_date, end_date) + return to_datetime(min(start_date, end_date)), to_datetime(max(start_date, end_date)) @dataclass(slots=True) diff --git a/trade/datamanager/utils/logging.py b/trade/datamanager/utils/logging.py index 346e78a..97b8a43 100644 --- a/trade/datamanager/utils/logging.py +++ b/trade/datamanager/utils/logging.py @@ -13,7 +13,8 @@ "trade.datamanager.vol", "trade.datamanager.option_spot", "trade.datamanager.greeks", - "trade.datamanager.base" + "trade.datamanager.base", + "trade.datamanager.market_data_helpers.dividends", } VARS = [ From f9725121b73db727eae6ab3e20378f924ecfe9c4 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:16 -0400 Subject: [PATCH 34/81] refactor: add pre-run hooks across backtest components --- EventDriven/backtest.py | 67 ++++--- EventDriven/riskmanager/market_timeseries.py | 193 +++++++++++-------- EventDriven/riskmanager/new_base.py | 62 +++++- EventDriven/types.py | 18 ++ 4 files changed, 239 insertions(+), 101 deletions(-) diff --git a/EventDriven/backtest.py b/EventDriven/backtest.py index 124a5da..61605d3 100644 --- a/EventDriven/backtest.py +++ b/EventDriven/backtest.py @@ -15,14 +15,14 @@ from copy import deepcopy # noqa import traceback from pandas.tseries.offsets import BDay -from EventDriven.types import EventTypes, SignalTypes +from EventDriven.types import EventTypes, SignalTypes, BacktestRunMixin from EventDriven.configs.core import BacktesterConfig from EventDriven.liquidity import LiquidityPolicy LOGGER = setup_logger("EventDriven.backtest", stream_log_level="WARNING") -class OptionSignalBacktest: +class OptionSignalBacktest(BacktestRunMixin): """ Event-driven backtesting engine for option trading strategies. @@ -192,8 +192,9 @@ def __init__( if eq_strategy is not None and trades is not None: raise ValueError("Cannot provide both trades DataFrame and eq_strategy. Please choose one.") + self._eq_strategy: Optional[MultiAssetStrategy] = None + self.eq_strategy = eq_strategy self.is_eq_strategy = eq_strategy is not None - self.eq_strategy: Optional[MultiAssetStrategy] = eq_strategy self.strategy: Optional[OptionSignalStrategy] = None self.initial_capital = initial_capital self.trades = trades @@ -220,6 +221,24 @@ def __init__( raise ValueError("Trades DataFrame cannot be None or empty when not using an equity strategy.") self.__init__with_trades(trades, initial_capital, symbol_list, end_date=end_date) + @property + def eq_strategy(self) -> Optional[MultiAssetStrategy]: + """Return equity strategy and keep dependent components synchronized.""" + return self._eq_strategy + + @eq_strategy.setter + def eq_strategy(self, strategy: Optional[MultiAssetStrategy]) -> None: + """Set equity strategy and propagate it to risk manager and portfolio if initialized.""" + self._eq_strategy = strategy + self.is_eq_strategy = strategy is not None + + if hasattr(self, "risk_manager") and self.risk_manager is not None: + self.risk_manager.eq_strategy = strategy + + if hasattr(self, "portfolio") and self.portfolio is not None: + self.portfolio.eq_strategy = strategy + self.portfolio.using_eq_strategy = strategy is not None + def __init__with_equity_strategy( self, eq_strategy: MultiAssetStrategy, @@ -274,6 +293,7 @@ def __init__with_equity_strategy( initial_capital=float(cash), eq_strategy=eq_strategy, ) + self.eq_strategy = eq_strategy self.events = [] def __init__with_trades( @@ -355,6 +375,7 @@ def __construct_data(self, trades: pd.DataFrame, initial_capital: int, symbol_li self.portfolio = OptionSignalPortfolio( self.bars, self.eventScheduler, risk_manager=self.risk_manager, initial_capital=float(initial_capital) ) + self.eq_strategy = self._eq_strategy self.events = [] def __handle_t_plus_n(self, trades: pd.DataFrame) -> pd.DataFrame: @@ -493,12 +514,20 @@ def _post_signal_analysis(self): ) self.portfolio.analyze_multiasset_strategy() - def run(self): + def pre_run_setup(self): + """Pre-run setup for the backtest. This method is called before the main run loop starts and is responsible for any necessary initialization or setup steps that need to be performed before processing events. + This includes pre-run setup for the market timeseries, risk manager, and any other components that require initialization before the backtest run begins. + """ ## Runtime configurations changes self.portfolio.t_plus_n = self.config.t_plus_n self.executor.max_slippage_pct = self.config.max_slippage_pct self.executor.min_slippage_pct = self.config.min_slippage_pct self.executor.commission_rate = self.config.commission_per_contract_in_units + self.risk_manager.pre_run_setup() + + def run(self): + self.logger.info("Starting backtest run.") + self.pre_run_setup() ## Begin backtest by looping through event scheduler dates while True: @@ -519,15 +548,13 @@ def run(self): self._pre_signal_analysis() # Placeholder for any pre-signal processing logic try: - # ## Placing before get_nowait because I want to check for roll, and if there is no roll, I want to break out of the loop - # if len(list(deepcopy(current_event_queue.queue))) == 0: - # meta = self.portfolio.analyze_positions() # noqa - # # print(f"Position Analysis Meta: {meta}") - ## Placing before get_nowait because I want to check for roll, and if there is no roll, I want to break out of the loop if current_event_queue.empty() and not _post_signal_ran: self.logger.info(f"Event queue is empty, processed {event_count} event(s)") - + + # Update portfolio time index after processing all events. + # Update before analysis, so analysis uses eod info + self.portfolio.update_timeindex() self._post_signal_analysis() _post_signal_ran = True @@ -536,9 +563,6 @@ def run(self): except emptyEventQueue: self.logger.info(f"Event queue is empty, processed {event_count} event(s)") - # Update portfolio time index after processing all events - self.portfolio.update_timeindex() - # advance scheduler queue to next date self.eventScheduler.advance_date() break @@ -557,7 +581,7 @@ def run(self): if event.type == EventTypes.SIGNAL.value: self.portfolio.analyze_signal(event) elif event.type == EventTypes.ORDER.value: - self.executor.execute_order_randomized_slippage(event) + self.executor.execute_order(event) elif event.type == EventTypes.FILL.value: self.portfolio.update_fill(event) self.portfolio.update_timeindex() @@ -600,14 +624,6 @@ def get_all_positions(self) -> pd.DataFrame: return timeseries of portfolio positions """ pos_arr = [] - for position in self.portfolio.all_positions: - pos_obj = {} - pos_obj["AMD"] = position["AMD"]["option"] - pos_obj["AAPL"] = position["AAPL"]["option"] - pos_obj["MSFT"] = position["MSFT"]["option"] - pos_obj["GOOGL"] = position["GOOGL"]["option"] - pos_obj["datetime"] = position["datetime"] - pos_arr.append(pos_obj) pos_df = pd.DataFrame(pos_arr) pos_df.set_index("datetime", inplace=True) return pos_df @@ -616,7 +632,12 @@ def store_event(self, event: Event): """ Store an event in the events list """ - self.events.append(event.__dict__) + event_dict = event.__dict__.copy() + if "position" in event_dict: + event_dict["trade_id"] = event_dict["position"].get("trade_id", None) + else: + event_dict["trade_id"] = None + self.events.append(event_dict) def get_events(self) -> pd.DataFrame: """ diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 219d75b..4ddbd43 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -149,6 +149,7 @@ 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, add_skip_columns, load_position_data_new +from EventDriven.types import BacktestRunMixin from trade.helpers.decorators import timeit from trade.helpers.threads import runThreads # noqa from trade.helpers.pools import runProcesses # noqa @@ -168,7 +169,7 @@ _change_global_stream_level("WARNING") -class BacktestTimeseries: +class BacktestTimeseries(BacktestRunMixin): """ Class for managing and retrieving market timeseries data for options and positions during backtesting. """ @@ -178,11 +179,16 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): self.end_date = _end self._skip_calc_config = SkipCalcConfig(skip_columns=["Midpoint"]) self.market_timeseries = MarketTimeseries(_start=self.start_date, _end=self.end_date) - self.options_cache = load_riskmanager_cache(target="processed_option_data") + self.options_cache = load_riskmanager_cache( + target="processed_option_data", clear_on_exit=True + ) ## Cache to store processed option data, cleared on exit to avoid polluting the cache with potentially large data that might not be needed in future sessions. self.position_data_cache = load_riskmanager_cache(target="position_data") self.special_dividends = load_riskmanager_cache(target="special_dividend") self.splits = load_riskmanager_cache(target="splits_raw") self.adjusted_strike_cache = load_riskmanager_cache(target="adjusted_strike_cache") + self.session_loaded_option_cache = load_riskmanager_cache( + target="session_loaded_option_cache", create_on_missing=True, clear_on_exit=True + ) ## Cache to store options that have been loaded during the session, to avoid repeated loading of the same option data during the session. Cleared on exit to avoid polluting the persistent cache. self.rf_timeseries = get_risk_free_rate_helper_v2()["annualized"] self.undl_timeseries_config = UndlTimeseriesConfig() self.option_price_config = OptionPriceConfig() @@ -199,6 +205,15 @@ def skip(self, position_id: str, date: Union[datetime, str], column: str = "Midp skip = skips_meta.get(column.capitalize()) return skip.get("skip_day", False) + + def pre_run_setup(self): + """ + Pre-run setup for BacktestTimeseries. Currently, all necessary setup is done in __init__, so this method is a placeholder for any future setup steps that might be needed before the backtest run starts. + """ + ## Clear session related caches + self.session_loaded_option_cache.clear() + self.position_data_cache.clear() + self.adjusted_strike_cache.clear() def set_splits(self, d): """ @@ -216,7 +231,13 @@ def get_option_data(self, opttick: str) -> pd.DataFrame: """ Retrieve option data for a given option ticker. """ - return self.options_cache.get(opttick, pd.DataFrame()) + if opttick in self.options_cache: + logger.info(f"Option data for {opttick} found in options cache, returning cached data") + return self.options_cache[opttick] + + raise KeyError( + f"Option data for {opttick} not found in options cache. Please ensure option data is loaded before access." + ) def get_position_data(self, position_id: str) -> pd.DataFrame: """ @@ -292,84 +313,91 @@ def get_at_time_position_data(self, position_id: str, date: Union[datetime, str] def calculate_option_data(self, position_id: str, date: Union[datetime, str]) -> Dict[str, Any]: """ Calculate Greeks for a given position at a specific date. + + If position data is already cached, returns cached data immediately. + Otherwise, calculates greeks, applies skip column adjustments, caches, and returns. """ import time logger.info( f"Calculate Greeks Dates Start: {self.start_date}, End: {self.end_date}, Position ID: {position_id}, Date: {date}" ) + + ## Check cache first - early return if data exists if position_id in self.position_data_cache: - logger.info(f"Position Data for {position_id} already available, skipping calculation") - position_data = self.position_data_cache[position_id] - else: - logger.critical(f"Position Data for {position_id} not available, calculating greeks. Load time ~5 minutes") - - ## Initialize the Long and Short Lists - long = [] - short = [] - thread_input_list = [[], []] - - date = pd.to_datetime(date) ## Ensure date is in datetime format - - ## First get position info - position_dict, positon_meta = parse_position_id(position_id) - - ## Now ensure that the spot and dividend data is available - for p in position_dict.values(): - for s in p: - ticker = swap_ticker(s["ticker"]) - start_time = time.time() - # self.market_timeseries.load_timeseries(sym=ticker) - timeseries_data = self.market_timeseries.get_timeseries(sym=ticker) - logger.info(f"Timeseries loading for {ticker} took {time.time() - start_time:.2f} seconds") - - @timeit - 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") - with self.lock: - 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": - ask = data["Closeask"] - bid = data["Closebid"] - - ## Swap bid and ask for short positions to reflect the perspective of the position holder - data["Closeask"] = bid - data["Closebid"] = ask - short.append(data) - else: - raise ValueError(f"Position Type {_set[0]} not recognized") - - 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 - - start_time = time.time() - runThreads( - get_timeseries, thread_input_list, block=True - ) ## Run the threads to get the timeseries data for the options - print(f"Threads execution took {time.time() - start_time:.2f} seconds") - 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"] = timeseries_data.spot["close"] - position_data["s"] = timeseries_data.chain_spot["close"] - position_data["r"] = self.rf_timeseries - position_data["y"] = timeseries_data.dividends - position_data["spread"] = position_data["Closeask"] - position_data["Closebid"] - - ## Apply skip columns adjustment + logger.info(f"Position Data for {position_id} already available in cache, returning cached data") + return self.position_data_cache[position_id] + + ## Data not in cache - perform full calculation + logger.critical(f"Position Data for {position_id} not available, calculating greeks. Load time ~5 minutes") + + ## Initialize the Long and Short Lists + long = [] + short = [] + thread_input_list = [[], []] + + date = pd.to_datetime(date) ## Ensure date is in datetime format + + ## First get position info + position_dict, positon_meta = parse_position_id(position_id) + + ## Now ensure that the spot and dividend data is available + for p in position_dict.values(): + for s in p: + ticker = swap_ticker(s["ticker"]) + start_time = time.time() + + ## TODO: Take this out, instead, use the info from loaded per leg option timeseries to get spot, dividend, etc. It's redundant to load the entire timeseries just to get the spot and dividend data for the position, when we can just get it from the option timeseries that we will be loading for the position anyway. This is especially important if we are calculating the greeks for a single point in time, as we don't need the entire timeseries for that. + timeseries_data = self.market_timeseries.get_timeseries(sym=ticker) + logger.info(f"Timeseries loading for {ticker} took {time.time() - start_time:.2f} seconds") + + @timeit + 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") + with self.lock: + 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": + ask = data["Closeask"] + bid = data["Closebid"] + + ## Swap bid and ask for short positions to reflect the perspective of the position holder + data["Closeask"] = bid + data["Closebid"] = ask + short.append(data) + else: + raise ValueError(f"Position Type {_set[0]} not recognized") + + 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 + + start_time = time.time() + runThreads( + get_timeseries, thread_input_list, block=True + ) ## Run the threads to get the timeseries data for the options + print(f"Threads execution took {time.time() - start_time:.2f} seconds") + 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"] = timeseries_data.spot["close"] + position_data["s"] = timeseries_data.chain_spot["close"] + position_data["r"] = self.rf_timeseries + position_data["y"] = timeseries_data.dividends + position_data["spread"] = position_data["Closeask"] - position_data["Closebid"] + + ## Apply skip columns adjustment (only on newly calculated data) position_data = self._skip_columns_adjustment(position_data=position_data, position_id=position_id) logger.info(f"Completed calculation of Greeks for Position ID: {position_id}") @@ -452,7 +480,6 @@ def _skip_columns_adjustment( position_data["Midpoint"] = position_data["Midpoint"].replace(0, np.nan).ffill() return position_data - # self.position_data[position_id] = position_data def load_position_data(self, opttick) -> pd.DataFrame: # noqa """ @@ -465,9 +492,10 @@ def load_position_data(self, opttick) -> pd.DataFrame: # noqa meta = parse_option_tick(opttick) self.market_timeseries.load_timeseries(sym=meta["ticker"]) - return load_position_data_new( + data = load_position_data_new( opttick=opttick, processed_option_data=self.options_cache, start=self.start_date, end=self.end_date ) + return data def _ffill_adj_strike_business_days( self, @@ -493,6 +521,12 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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. """ + key = (opttick, to_datetime(check_date).strftime("%Y-%m-%d")) + if key in self.session_loaded_option_cache: + logger.info( + f"Option data for {opttick} on {check_date} already generated in session, returning cached data" + ) + return self.session_loaded_option_cache[key] meta = parse_option_tick(opttick) exp = to_datetime(meta["exp_date"]) @@ -548,6 +582,9 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: start_date=window_start, end_date=window_end, ) + self.session_loaded_option_cache[key] = ( + data ## Cache the loaded data for the session to avoid re-loading it if the same option and date is requested again during the session + ) return data # If there are splits, we need to load the data for each tick after adjusting strikes @@ -572,6 +609,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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"] ) @@ -630,4 +668,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: start_date=window_start, end_date=window_end, ) + self.session_loaded_option_cache[key] = ( + final_data ## Cache the generated data for the session to avoid re-generating it if the same option and date is requested again during the session + ) return final_data diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index 00037ae..00093e5 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -205,7 +205,7 @@ from EventDriven.riskmanager.position.cogs.limits import LimitsAndSizingCog from EventDriven.dataclasses.orders import OrderRequest from EventDriven.dataclasses.states import NewPositionState, PositionAnalysisContext, StrategyChangeMeta -from EventDriven.types import ResultsEnum, Order +from EventDriven.types import ResultsEnum, Order, BacktestRunMixin from EventDriven.configs.core import RiskManagerConfig from EventDriven._vars import CONTRACT_MULTIPLIER, load_riskmanager_cache from EventDriven.liquidity import LiquidityPolicy @@ -214,7 +214,7 @@ logger = setup_logger("EventDriven.riskmanager.new_base", stream_log_level="WARNING") -class RiskManager: +class RiskManager(BacktestRunMixin): """ Manages portfolio risk and executes options trading strategies with position sizing and Greek-based limits. @@ -290,6 +290,7 @@ def __init__( ## Configs self.config = RiskManagerConfig() self.liquidity_policy = kwargs.pop("liquidity_policy", None) or LiquidityPolicy() + self._eq_strategy = None ## Get start and end dates for timeseries loading start, end = get_timeseries_start_end() @@ -332,6 +333,25 @@ def __init__( if len(self.order_request_cache.values()) > 0: logger.info(f"Order request cache loaded with {len(self.order_request_cache.values())} requests") + @property + def eq_strategy(self): + """Return equity strategy reference used by this risk manager.""" + return self._eq_strategy + + @eq_strategy.setter + def eq_strategy(self, value) -> None: + """Set equity strategy reference used by this risk manager.""" + self._eq_strategy = value + + def pre_run_setup(self): + """ + Pre-run setup for RiskManager. This method is called before the backtest run starts and is responsible for any necessary initialization or setup steps that need to be performed before processing orders and analyzing positions. + """ + self.market_data.pre_run_setup() + self.analysis_cache.clear() + self.order_cache.clear() + self.order_request_cache.clear() + def clear_caches(self): """ Clears all caches used by the RiskManager. @@ -428,6 +448,10 @@ def get_order(self, req: OrderRequest) -> NewPositionState: if is_USholiday(req.date): logger.info(f"Date {req.date} is a US Holiday, skipping order generation") return {"result": ResultsEnum.IS_HOLIDAY.value, "data": None} + + ## Run through position analyzer first + print(f"Running order request through position analyzer: {req}") + self.position_analyzer.on_new_order_request(new_request_state=req) ## Investigate if tick cash is scaled if not req.is_tick_cash_scaled: @@ -544,6 +568,40 @@ def analyze_position(self, context: PositionAnalysisContext) -> StrategyChangeMe self.analysis_cache[dt] = analysis return analysis + def get_position_analysis_df(self) -> pd.DataFrame: + """Build a flat DataFrame from the position analysis cache. + + Returns: + pd.DataFrame: One row per (date, position) with columns: + ``date``, ``trade_id``, ``signal_id``, ``underlier_tick``, + ``entry_price``, ``pnl``, ``last_updated``, + ``action_name``, ``action_reason``, plus any key/value pairs + from ``action.action`` dict (e.g. ``quantity_diff``, + ``new_quantity``). + """ + rows = [] + for date_key, meta in self.analysis_cache.items(): + for pos_state in meta.actionables: + action = pos_state.action + row = { + "date": date_key, + "trade_id": pos_state.trade_id, + "signal_id": pos_state.signal_id, + "underlier_tick": pos_state.underlier_tick, + "entry_price": pos_state.entry_price, + "current_quantity": pos_state.quantity, + "pnl": pos_state.pnl, + "last_updated": pos_state.last_updated, + "action_name": action.name if action is not None else None, + "action_reason": action.reason if action is not None else None, + "entry_date": pos_state.entry_date, + } + action_dict = (action.action if action is not None else None) or {} + if isinstance(action_dict, dict): + row.update(action_dict) + rows.append(row) + return pd.DataFrame(rows) + def append_option_data( self, option_id: str = None, diff --git a/EventDriven/types.py b/EventDriven/types.py index 22f930e..078c077 100644 --- a/EventDriven/types.py +++ b/EventDriven/types.py @@ -422,3 +422,21 @@ def from_dict(d: Dict[str, Any]) -> "Order": metrics=d["metrics"], scores=d["scores"], ) + + +class BacktestRunMixin: + """ + Mixin class to provide common functionality for backtest run objects. + + This class can be inherited by any backtest run implementation to ensure + consistent handling of trade updates and stats management across different + backtesting frameworks or implementations. + """ + + def pre_run_setup(self): + """Initialize the classes behavior before the backtest run starts.""" + pass + + def post_run_cleanup(self): + """Clean up any resources or perform finalization after the backtest run ends.""" + pass \ No newline at end of file From 8ec08b7681fe730c197176e874bab431650f57fd Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:20 -0400 Subject: [PATCH 35/81] feat: add configurable slippage modes and fill tracking --- EventDriven/configs/core.py | 7 +- EventDriven/execution.py | 157 +++++++++++++++++++++++++-- module_test/test_no_slippage.py | 186 ++++++++++++++++++++++++++++++++ 3 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 module_test/test_no_slippage.py diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index e465d63..f38bfad 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -217,7 +217,7 @@ class BacktesterConfig(BaseConfigs): """ t_plus_n: int = 1 - finalize_trades: bool = False + finalize_trades: bool = True raise_errors: bool = False min_slippage_pct: float = 0.075 max_slippage_pct: float = 0.15 @@ -285,6 +285,7 @@ class ScoringConfigs(BaseConfigs): spread_ticks: int = 2 structure_direction: Literal["long", "short"] = "long" strategy: Literal["vertical", "naked"] = "vertical" + hybrid_strategy_enabled: bool = False # Moneyness m_target: numbers.Number = 0.8 @@ -321,7 +322,7 @@ class ScoringConfigs(BaseConfigs): @pydantic_dataclass class ExecutionHandlerConfig(BaseConfigs): - slippage_model: Literal["randomized", "fixed", "spread_pct"] = ( - "randomized" # Whether to use randomized slippage, fixed slippage, or slippage as a percentage of the spread. Default is randomized slippage. + slippage_model: Literal["randomized", "fixed", "spread_pct", "none"] = ( + "randomized" # Whether to use randomized slippage, fixed slippage, slippage as a percentage of the spread, or no slippage. Default is randomized slippage. ) pct_alpha: float = 0.25 # If using spread_pct slippage model, this is the percentage of the spread to use as slippage. For example, if pct_alpha is 0.25 and the spread is $0.20, then the slippage will be $0.05. diff --git a/EventDriven/execution.py b/EventDriven/execution.py index a048aae..1472fe5 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -11,8 +11,9 @@ from copy import deepcopy -logger = setup_logger("EventDriven.execution") +logger = setup_logger("EventDriven.execution", stream_log_level="WARNING") exec_cache = {"order": {}, "fill": {}, "exercise": {}} +fill_tracking = {} # Tracks spread & commission per fill # execution.py @@ -82,6 +83,7 @@ def __init__( self.liquidity_policy = liquidity_policy or LiquidityPolicy() self.liquidity_drops: list[dict] = [] self.drop_counts_by_signal_trade: dict[tuple[str, str], int] = {} + self.fill_tracking: list[dict] = [] # Track spread & commission per fill def _spread_pct(self, order_event: OrderEvent) -> Optional[float]: """Returns spread percentage as spread/close, or None if unavailable.""" @@ -95,6 +97,38 @@ def _spread_pct(self, order_event: OrderEvent) -> Optional[float]: except Exception: return None + def _track_fill_metrics( + self, order_event: OrderEvent, spread: Optional[float], commission: float, slippage: float, slippage_model: str + ): + """Track spread & commission metrics for each fill.""" + position = order_event.position or {} + close = position.get("close") + spread_pct = None + if spread is not None and close not in [None, 0]: + spread_pct = abs(float(spread) / float(close)) + + tracking_entry = { + "signal_id": order_event.signal_id, + "trade_id": position.get("trade_id", ""), + "datetime": order_event.datetime, + "direction": order_event.direction, + "symbol": order_event.symbol, + "quantity": order_event.quantity, + "close": close, + "spread": spread, + "spread_pct": spread_pct, + "commission": commission, + "slippage": slippage, + "slippage_model": slippage_model, + } + self.fill_tracking.append(tracking_entry) + key = f"{order_event.signal_id}::{order_event.datetime.strftime('%Y-%m-%d')}::{order_event.direction}" + fill_tracking[key] = tracking_entry + logger.debug( + f"Fill tracking: signal={order_event.signal_id}, spread={spread}, spread_pct={spread_pct}, " + f"commission={commission}, slippage={slippage}, model={slippage_model}" + ) + def _track_drop(self, order_event: OrderEvent, spread_pct: float): """Track silent drops by signal/trade pair and keep a detailed event log.""" trade_id = (order_event.position or {}).get("trade_id", "") @@ -162,6 +196,8 @@ def calculate_slippage_value_randomized(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on a random percentage within the specified slippage range. The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + + Creates a tracking entry for spread & commission metrics. """ if order_event.direction == "BUY": slippage_pct = np.random.uniform( @@ -176,12 +212,23 @@ def calculate_slippage_value_randomized(self, order_event: OrderEvent) -> float: slippage_value = order_event.position["close"] * slippage_pct * order_event.quantity + # Track spread & commission for this fill + spread = order_event.position.get("spread") + commission = ( + self.commission_rate + * order_event.quantity + * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) + ) + self._track_fill_metrics(order_event, spread, commission, slippage_value, "randomized") + return slippage_value def calculate_slippage_value_fixed(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on a fixed percentage defined by max_slippage_pct. The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + + Creates a tracking entry for spread & commission metrics. """ if order_event.direction == "BUY": slippage_pct = self.max_slippage_pct @@ -191,12 +238,24 @@ def calculate_slippage_value_fixed(self, order_event: OrderEvent) -> float: raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") slippage_value = order_event.position["close"] * slippage_pct * order_event.quantity + + # Track spread & commission for this fill + spread = order_event.position.get("spread") + commission = ( + self.commission_rate + * order_event.quantity + * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) + ) + self._track_fill_metrics(order_event, spread, commission, slippage_value, "fixed") + return slippage_value def calculate_slippage_pct_of_spread(self, order_event: OrderEvent) -> float: """ Calculate slippage value as a percentage of the bid-ask spread. The slippage percentage is positive for BUY orders (increasing the price) and negative for SELL orders (decreasing the price). + + Creates a tracking entry for spread & commission metrics. """ spread = order_event.position.get("spread", None) if spread is None: @@ -212,14 +271,56 @@ def calculate_slippage_pct_of_spread(self, order_event: OrderEvent) -> float: raise ValueError(f"Invalid order direction: {order_event.direction}. Must be 'BUY' or 'SELL'.") close = order_event.position["close"] pct_spread_slippage = (spread_slippage / close) if close != 0 else 0 - print( + logger.info( f"Spread: {spread}, Spread Slippage: {spread_slippage}, Total Slippage: {slippage}, Pct Spread Slippage: {pct_spread_slippage}, Signal ID: {order_event.signal_id}, Direction: {order_event.direction}, Date: {order_event.datetime}" ) + + # Track spread & commission for this fill + commission = ( + self.commission_rate + * order_event.quantity + * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) + ) + self._track_fill_metrics(order_event, spread, commission, slippage, "spread_pct") + return slippage + def calculate_slippage_value_none(self, order_event: OrderEvent) -> float: + """ + Calculate zero slippage value. + Used when slippage_model is 'none' for perfect execution at market price. + + Creates a tracking entry for spread & commission metrics. + + Args: + order_event: The order event (unused, but kept for interface consistency). + + Returns: + float: Always returns 0.0 (no slippage). + """ + # Track spread & commission for this fill (with zero slippage) + spread = order_event.position.get("spread") + commission = ( + self.commission_rate + * order_event.quantity + * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) + ) + self._track_fill_metrics(order_event, spread, commission, 0.0, "none") + + return 0.0 + def calculate_slippage_value(self, order_event: OrderEvent) -> float: """ Calculate slippage value based on the specified slippage model in the config. + + Args: + order_event: The order event containing position and direction information. + + Returns: + float: The calculated slippage value. + + Raises: + ValueError: If an invalid slippage model is configured. """ if self.config.slippage_model == "randomized": return self.calculate_slippage_value_randomized(order_event) @@ -227,16 +328,16 @@ def calculate_slippage_value(self, order_event: OrderEvent) -> float: return self.calculate_slippage_value_fixed(order_event) elif self.config.slippage_model == "spread_pct": return self.calculate_slippage_pct_of_spread(order_event) + elif self.config.slippage_model == "none": + return self.calculate_slippage_value_none(order_event) else: raise ValueError( - f"Invalid slippage model: {self.config.slippage_model}. Must be 'randomized', 'fixed', or 'spread_pct'." + f"Invalid slippage model: {self.config.slippage_model}. Must be 'randomized', 'fixed', 'spread_pct', or 'none'." ) - def execute_order_randomized_slippage(self, order_event: OrderEvent): + def execute_order(self, order_event: OrderEvent): """ - This method will execute an order with a random slippage - based on the max_slippage_pct attribute of the class. - Note: Quantity takes precedence if both quantity and cash are provided, else quantity is determined by cash / price_of_contract + This method will execute an order event, calculate the fill cost including slippage and commission, and put a fill event on the queue. """ assert order_event.type == "ORDER", f"Event type must be 'ORDER' received {order_event.type}" assert order_event.direction == "BUY" or order_event.direction == "SELL", ( @@ -290,6 +391,7 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): ## Clamp quantity to ensure we don't exceed available cash while total_cost > order_event.cash: + logger.info(f"Total cost {total_cost} exceeds available cash {order_event.cash}. Reducing quantity.") quantity -= 1 total_cost = quantity * price + self.commission_rate logger.info( @@ -343,7 +445,12 @@ def execute_order_randomized_slippage(self, order_event: OrderEvent): exec_cache["fill"][ f"{order_event.signal_id}_{order_event.datetime.strftime('%Y-%m-%d')}_{order_event.direction}" ] = deepcopy(fill_event) - self.events.put(fill_event) + if quantity > 0: + self.events.put(fill_event) + else: + logger.warning( + f"Calculated quantity is zero or negative for signal {order_event.signal_id} after slippage and cash constraints. Order will not be executed. Final quantity: {quantity}, Signal ID: {order_event.signal_id}" + ) def execute_exercise(self, exercise_event: ExerciseEvent): """ @@ -398,3 +505,37 @@ def __calculate_premium_pnl(self, option_meta, spot, premium): return max(0, option_meta["strike"] - spot) - premium else: raise ValueError(f"Invalid option type: {option_meta['option_type']}") + + def get_fill_tracking_df(self) -> pd.DataFrame: + """ + Returns fill tracking data as a DataFrame. + + Provides detailed spread, commission, and slippage metrics for each fill + executed during the backtest. Useful for analyzing transaction costs and + execution quality. + + Returns: + pd.DataFrame: DataFrame with columns: + - signal_id: Signal identifier + - trade_id: Trade identifier + - datetime: Execution datetime + - direction: BUY or SELL + - symbol: Ticker symbol + - quantity: Number of contracts + - close: Close price used for execution + - spread: Bid-ask spread from position data + - spread_pct: Spread as percentage of close price + - commission: Total commission charged + - slippage: Total slippage cost + - slippage_model: Slippage model used (randomized, fixed, spread_pct, none) + + Examples: + >>> handler = SimulatedExecutionHandler(events) + >>> # ... run backtest ... + >>> df = handler.get_fill_tracking_df() + >>> avg_commission = df['commission'].mean() + >>> avg_spread_pct = df['spread_pct'].mean() + """ + if not self.fill_tracking: + return pd.DataFrame() + return pd.DataFrame(self.fill_tracking) diff --git a/module_test/test_no_slippage.py b/module_test/test_no_slippage.py new file mode 100644 index 0000000..e07fab2 --- /dev/null +++ b/module_test/test_no_slippage.py @@ -0,0 +1,186 @@ +"""Test no slippage execution option. + +Validates that: +1. ExecutionHandlerConfig accepts 'none' slippage model +2. calculate_slippage_value returns 0.0 when slippage_model='none' +3. All other slippage models still work correctly + +Usage: + /Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/bin/python \ + module_test/test_no_slippage.py +""" + +from __future__ import annotations + +from queue import Queue +from datetime import datetime + +from EventDriven.configs.core import ExecutionHandlerConfig +from EventDriven.execution import SimulatedExecutionHandler +from EventDriven.event import OrderEvent + + +def test_no_slippage_config(): + """Test that ExecutionHandlerConfig accepts 'none' slippage model.""" + print("=" * 70) + print("Testing No Slippage Configuration") + print("=" * 70) + + # Test 1: Create config with 'none' slippage model + print("\n[Test 1] Create ExecutionHandlerConfig with slippage_model='none'") + config = ExecutionHandlerConfig(slippage_model="none") + print(f" Config slippage_model: {config.slippage_model}") + assert config.slippage_model == "none", "Config should accept 'none' slippage model" + print(" ✓ Config created successfully with slippage_model='none'") + + # Test 2: Create execution handler with no slippage config + print("\n[Test 2] Create SimulatedExecutionHandler with no slippage") + events = Queue() + handler = SimulatedExecutionHandler(events=events, config=config) + print(f" Handler config slippage_model: {handler.config.slippage_model}") + assert handler.config.slippage_model == "none" + print(" ✓ Handler created successfully") + + # Test 3: Calculate slippage for BUY order with 'none' model + print("\n[Test 3] Calculate slippage for BUY order (should be 0.0)") + order_event = OrderEvent( + datetime=datetime(2024, 12, 10), + symbol="HD", + order_type="MKT", + direction="BUY", + quantity=10, + cash=1000.0, + position={"close": 5.0, "spread": 0.10, "trade_id": "&L:HD20250620C500"}, + signal_id="test_signal_001", + ) + slippage = handler.calculate_slippage_value(order_event) + print(f" Slippage value: {slippage}") + assert slippage == 0.0, f"Expected 0.0, got {slippage}" + print(" ✓ Slippage is 0.0 for BUY order") + + # Test 4: Calculate slippage for SELL order with 'none' model + print("\n[Test 4] Calculate slippage for SELL order (should be 0.0)") + order_event.direction = "SELL" + slippage = handler.calculate_slippage_value(order_event) + print(f" Slippage value: {slippage}") + assert slippage == 0.0, f"Expected 0.0, got {slippage}" + print(" ✓ Slippage is 0.0 for SELL order") + + print("\n" + "=" * 70) + print("SUMMARY: All no slippage tests passed!") + print("=" * 70) + + +def test_other_slippage_models(): + """Test that other slippage models still work correctly.""" + print("\n" + "=" * 70) + print("Testing Other Slippage Models") + print("=" * 70) + + events = Queue() + + # Test randomized model + print("\n[Test 5] Randomized slippage model") + config_random = ExecutionHandlerConfig(slippage_model="randomized") + handler_random = SimulatedExecutionHandler(events=events, config=config_random, max_slippage_pct=0.002) + order_event = OrderEvent( + datetime=datetime(2024, 12, 10), + symbol="HD", + order_type="MKT", + direction="BUY", + quantity=10, + cash=1000.0, + position={"close": 5.0, "spread": 0.10, "trade_id": "&L:HD20250620C500"}, + signal_id="test_signal_002", + ) + slippage = handler_random.calculate_slippage_value(order_event) + print(f" Randomized slippage: {slippage:.6f}") + assert slippage != 0.0, "Randomized slippage should not be 0" + assert slippage > 0, "BUY order should have positive slippage" + print(f" ✓ Randomized slippage working: {slippage:.6f}") + + # Test fixed model + print("\n[Test 6] Fixed slippage model") + config_fixed = ExecutionHandlerConfig(slippage_model="fixed") + handler_fixed = SimulatedExecutionHandler(events=events, config=config_fixed, max_slippage_pct=0.002) + slippage = handler_fixed.calculate_slippage_value(order_event) + print(f" Fixed slippage: {slippage:.6f}") + expected_fixed = 5.0 * 0.002 * 10 # close * max_slippage_pct * quantity + assert abs(slippage - expected_fixed) < 0.001, f"Expected {expected_fixed}, got {slippage}" + print(f" ✓ Fixed slippage working: {slippage:.6f}") + + # Test spread_pct model + print("\n[Test 7] Spread percentage slippage model") + config_spread = ExecutionHandlerConfig(slippage_model="spread_pct", pct_alpha=0.25) + handler_spread = SimulatedExecutionHandler(events=events, config=config_spread) + slippage = handler_spread.calculate_slippage_value(order_event) + print(f" Spread pct slippage: {slippage:.6f}") + expected_spread = 0.10 * 0.25 * 10 # spread * pct_alpha * quantity + assert abs(slippage - expected_spread) < 0.001, f"Expected {expected_spread}, got {slippage}" + print(f" ✓ Spread pct slippage working: {slippage:.6f}") + + print("\n" + "=" * 70) + print("SUMMARY: All slippage model tests passed!") + print("=" * 70) + + +def test_comparison_with_without_slippage(): + """Compare execution with and without slippage.""" + print("\n" + "=" * 70) + print("Comparison: With vs Without Slippage") + print("=" * 70) + + events = Queue() + + order_event = OrderEvent( + datetime=datetime(2024, 12, 10), + symbol="HD", + order_type="MKT", + direction="BUY", + quantity=10, + cash=1000.0, + position={"close": 5.0, "spread": 0.10, "trade_id": "&L:HD20250620C500"}, + signal_id="test_signal_003", + ) + + # With slippage + print("\n[Test 8] Execution with fixed slippage (0.2%)") + config_with = ExecutionHandlerConfig(slippage_model="fixed") + handler_with = SimulatedExecutionHandler(events=events, config=config_with, max_slippage_pct=0.002) + slippage_with = handler_with.calculate_slippage_value(order_event) + print(f" Slippage amount: ${slippage_with:.4f}") + print(f" Price impact: {(slippage_with / (order_event.position['close'] * order_event.quantity)) * 100:.2f}%") + + # Without slippage + print("\n[Test 9] Execution with NO slippage") + config_without = ExecutionHandlerConfig(slippage_model="none") + handler_without = SimulatedExecutionHandler(events=events, config=config_without) + slippage_without = handler_without.calculate_slippage_value(order_event) + print(f" Slippage amount: ${slippage_without:.4f}") + print(f" Price impact: {(slippage_without / (order_event.position['close'] * order_event.quantity)) * 100:.2f}%") + + print(f"\n Difference: ${abs(slippage_with - slippage_without):.4f}") + print(f" Cost savings with no slippage: ${slippage_with:.4f}") + + print("\n" + "=" * 70) + print("SUMMARY: Comparison complete!") + print("=" * 70) + + +if __name__ == "__main__": + try: + test_no_slippage_config() + test_other_slippage_models() + test_comparison_with_without_slippage() + print("\n" + "=" * 70) + print("✅ ALL TESTS PASSED!") + print("=" * 70) + print("\nNo slippage option successfully implemented and verified.") + print("To use: ExecutionHandlerConfig(slippage_model='none')") + print() + except AssertionError as e: + print(f"\n✗ TEST FAILED: {e}") + raise + except Exception as e: + print(f"\n✗ TEST ERROR: {type(e).__name__}: {e}") + raise From 4a39d9b08bd6d9b6680ba85fcd2658a5ce50c2e5 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:25 -0400 Subject: [PATCH 36/81] feat: improve portfolio state and trade pnl consistency --- EventDriven/.live_forward_compatibility.txt | 3 + EventDriven/dataclasses/orders.py | 2 + EventDriven/dataclasses/states.py | 6 + EventDriven/new_portfolio.py | 63 ++++++- EventDriven/riskmanager/position/analyzer.py | 66 +++++++- EventDriven/riskmanager/position/base.py | 14 ++ .../position/cogs/analyze_utils.py | 9 +- .../riskmanager/position/cogs/limits.py | 47 ++++- .../riskmanager/position/cogs/pnl_monitor.py | 160 ++++++++++++++++++ EventDriven/trade.py | 56 +++++- EventDriven/tradeLedger.py | 21 ++- module_test/test_trade_pnl_consistency.py | 98 +++++++++++ 12 files changed, 516 insertions(+), 29 deletions(-) create mode 100644 EventDriven/.live_forward_compatibility.txt create mode 100644 EventDriven/riskmanager/position/cogs/pnl_monitor.py create mode 100644 module_test/test_trade_pnl_consistency.py diff --git a/EventDriven/.live_forward_compatibility.txt b/EventDriven/.live_forward_compatibility.txt new file mode 100644 index 0000000..8afab57 --- /dev/null +++ b/EventDriven/.live_forward_compatibility.txt @@ -0,0 +1,3 @@ +1. Load Trade Map into portfolio. +2. Ensure limits metadata is saved & retrieved from db +3. \ No newline at end of file diff --git a/EventDriven/dataclasses/orders.py b/EventDriven/dataclasses/orders.py index b1196b4..caf876a 100644 --- a/EventDriven/dataclasses/orders.py +++ b/EventDriven/dataclasses/orders.py @@ -24,4 +24,6 @@ class OrderRequest: chain_spot: numbers.Number = None is_tick_cash_scaled: bool = False delta_lmt: Optional[numbers.Number] = None + signal_total_pnl: Optional[numbers.Number] = None + symbol_total_pnl: Optional[numbers.Number] = None diff --git a/EventDriven/dataclasses/states.py b/EventDriven/dataclasses/states.py index 8da93b6..944fa63 100644 --- a/EventDriven/dataclasses/states.py +++ b/EventDriven/dataclasses/states.py @@ -2,6 +2,7 @@ from pydantic import ConfigDict, Field from typing import Optional, List from datetime import datetime +from EventDriven.trade import Trade from trade.datamanager.market_data import AtIndexResult from EventDriven.types import Order from EventDriven.dataclasses.orders import OrderRequest @@ -51,7 +52,12 @@ class PositionState: current_underlier_data: AtIndexResult pnl: float last_updated: datetime + signal_total_pnl: Optional[float] = None + symbol_total_pnl: Optional[float] = None action: Optional[RMAction] = None + entry_date: Optional[datetime] = None + trades: Optional[Trade] = None + signal_total_pnl: Optional[float] = None def __repr__(self): return f"PositionState(date={self.last_updated}, trade_id={self.trade_id}, quantity={self.quantity}, pnl={self.pnl}, signal_id={self.signal_id})" diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index 4a36b05..20ef1c3 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -5,6 +5,7 @@ # - Position Management: Rolling Options, Hedging, Position sizing # - +from ast import Dict from copy import deepcopy import logging from abc import ABCMeta, abstractmethod @@ -192,8 +193,7 @@ def __init__( self.underlier_list_data = {} self.unprocessed_signals = [] self.allow_multiple_trades = True # allow multiple trades for the same signal_id - self.__equity = None - self.__transactions = [] + # call internal functions to construct key portfolio data self.__construct_all_positions() self.__construct_current_positions() @@ -214,6 +214,11 @@ def __init__( self.eq_strategy = eq_strategy self.using_eq_strategy = eq_strategy is not None + ## Hidden attributes + self.__position_entry_dates = {} + self.__equity = None + self.__transactions = [] + def _is_holiday(self, dt): """ Cache holiday lookups for speed when many signals hit the same date. @@ -673,6 +678,8 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type tick_cash=cash_at_hand, direction=signal_event.signal_type, signal_id=signal_event.signal_id, + signal_total_pnl=self._get_signal_total_pnl(signal_event.signal_id), + symbol_total_pnl=self._get_symbol_total_pnl(signal_event.symbol) ) ) self.position_cache[cache_key] = position_state @@ -700,6 +707,32 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type parent_event=signal_event, ) + def _get_signal_total_pnl(self, signal_id: str): + """ + Helper function to calculate total PnL for a given signal_id across all trades. + """ + total_pnl = 0.0 + tm: Dict[TradeID, Trade] = self.trades_map + for trade in tm.values(): + trade: Trade + trade.update_pnls() # ensure pnl is updated before accessing + if trade.signal_id == signal_id: + total_pnl = total_pnl + trade.total_pnl + return total_pnl + + def _get_symbol_total_pnl(self, symbol: str): + """ + Helper function to calculate total PnL for a given symbol across all trades. + """ + total_pnl = 0.0 + tm: Dict[TradeID, Trade] = self.trades_map + for trade in tm.values(): + trade: Trade + trade.update_pnls() # ensure pnl is updated before accessing + if trade.symbol == symbol: + total_pnl = total_pnl + trade.total_pnl + return total_pnl + def _process_failed_order(self, signal_event: SignalEvent, order_result: Order = None): """ Process a failed order by either rolling it forward or logging it. @@ -867,6 +900,21 @@ def analyze_multiasset_strategy(self, dt: Optional[pd.Timestamp] = None): ) self.eventScheduler.schedule_event(exec_date, signal_event) + def _get_entry_date_for_position(self, trade_id: str, signal_id: str) -> Optional[pd.Timestamp]: + """ + Get the entry date for a position based on the trade_id and signal_id + """ + key = (str(trade_id), str(signal_id)) + if key in self.__position_entry_dates: + return self.__position_entry_dates[key] + trade_obj = self._get_trade_object(trade_id, signal_id) + if trade_obj is not None: + entry_time_str = trade_obj.stats["EntryTime"].values[0] + entry_date = pd.to_datetime(entry_time_str, format="%Y-%m-%d") + self.__position_entry_dates[key] = entry_date + return entry_date + return None + @staticmethod def _construct_position( signal_id: str, @@ -895,6 +943,7 @@ def _construct_position( "quantity": quantity, "entry_price": entry_price, "market_value": close * quantity * 100, + } return position @@ -911,6 +960,8 @@ def _create_ctx(self, date: pd.Timestamp, positions: dict = None) -> PositionAna positions_states = [] for tick, pos_pack in positions.items(): for signal_id, position in pos_pack.items(): + trade_obj = self._get_trade_object(position["position"]["trade_id"], signal_id) + entry_date = self._get_entry_date_for_position(position["position"]["trade_id"], signal_id) trade_id = position["position"]["trade_id"] qty = position["quantity"] entry_price = position["entry_price"] / qty @@ -933,6 +984,10 @@ def _create_ctx(self, date: pd.Timestamp, positions: dict = None) -> PositionAna current_underlier_data=current_underlier_data, pnl=pnl, last_updated=date, + entry_date=entry_date, + trades=trade_obj, + signal_total_pnl=self._get_signal_total_pnl(signal_id), + symbol_total_pnl=self._get_symbol_total_pnl(tick) ) positions_states.append(pos_state) @@ -1022,10 +1077,10 @@ def _get_trade_object(self, trade_id: str, signal_id: str) -> Trade: return self.trades_map.get(self._get_trade_key(trade_id, signal_id)) def _get_trade_key(self, trade_id: str, signal_id: str) -> tuple: - return (trade_id, signal_id) + return (str(trade_id), str(signal_id)) def _set_trade_object(self, trade_id: str, signal_id: str, trade: Trade): - self.trades_map[self._get_trade_key(trade_id, signal_id)] = trade + self.trades_map[self._get_trade_key(str(trade_id), str(signal_id))] = trade def update_positions_on_fill(self, fill_event: FillEvent): """ diff --git a/EventDriven/riskmanager/position/analyzer.py b/EventDriven/riskmanager/position/analyzer.py index bb03d80..687e167 100644 --- a/EventDriven/riskmanager/position/analyzer.py +++ b/EventDriven/riskmanager/position/analyzer.py @@ -211,6 +211,7 @@ ) from EventDriven.configs.core import PositionAnalyzerConfig from EventDriven.dataclasses.states import NewPositionState +from EventDriven.dataclasses.orders import OrderRequest from EventDriven.dataclasses.states import ( PositionAnalysisContext, StrategyChangeMeta, @@ -310,12 +311,21 @@ def analyze(self, context: PositionAnalysisContext) -> StrategyChangeMeta: and just attach the raw opinions. - We'll replace this with the full Cog Process reconciler later. """ - - all_actions: List[PositionState] = [] + all_position_states: Dict[str, PositionState] = {pos_state.trade_id: pos_state for pos_state in context.portfolio.positions} + all_actions: List[RMAction] = [] for cog in self._iter_active_cogs(): + + ## Analyze and collect opinions actions = cog.analyze(context) - all_actions.extend(actions.opinions) + + ## Extract RMAction objects from opinions and log them + rm_actions = [op.action for op in actions.opinions if op.action is not None] + logger.info(f"Cog {cog.name} returned {len(rm_actions)} actions: {[(action.type.value, action.reason) for action in rm_actions]}") + + ## Extend the master list of all actions with this cog's actions + all_actions.extend(rm_actions) + logger.info(f"All actions collected: {all_actions}") ## Get unique trade IDs from all actions unique_trade_ids = set(action.trade_id for action in all_actions) @@ -323,16 +333,56 @@ def analyze(self, context: PositionAnalysisContext) -> StrategyChangeMeta: ## Get the most important action for each trade ID for trade_id in unique_trade_ids: + + ## Filter actions for this trade ID and log them trade_actions = [action for action in all_actions if action.trade_id == trade_id] - trade_actions.sort(key=lambda x: ACTION_PRIORITY.get(x.action.type, float("inf"))) + logger.info(f"Trade ID {trade_id} has actions: {trade_actions}") + + ## Sort actions by priority and log the sorted list + trade_actions.sort(key=lambda x: ACTION_PRIORITY.get(x.type, float("inf"))) + all_trade_id_actions = [(action.type.value, action.reason) for action in trade_actions] + combined_reasons = "; ".join([f"{action.type.value}: {action.reason}" for action in trade_actions]) + logger.info(f"Actions for trade {trade_id}: {all_trade_id_actions}") + + ## Select the most important action (the one with the highest priority) and log it most_important_action = trade_actions[0] - strategy_changes.append(most_important_action) + most_important_action.reason = combined_reasons # Combine reasons for all actions into one string + logger.info(f"Most important action for trade {trade_id}: {most_important_action.type.value} - Combined Reasons: {most_important_action.reason}") + + ## Attach the most important action to the corresponding PositionState + position_state = all_position_states.get(trade_id) + if position_state is None: + logger.warning(f"No position state found for trade ID {trade_id}. Skipping action assignment.") + continue + position_state.action = most_important_action + logger.info(f"Most important action for trade {trade_id}: {most_important_action.type.value} - Reason: {most_important_action.reason}") + + ## Add the position state with the assigned action to the strategy changes list + strategy_changes.append(position_state) return StrategyChangeMeta( date=context.date, actionables=strategy_changes, portfolio_meta=context.portfolio_meta, ) + + def on_new_order_request(self, new_request_state: OrderRequest) -> OrderRequest: + """ + Hook method called when a new order request is generated. + Delegates to all registered cogs. + Args: + new_request_state (OrderRequest): The new order request state containing order details. + Returns: + OrderRequest: The updated order request state after all cogs have processed it. + What this does: + - It iterates through all registered cogs and calls their `on_new_order_request` method. + - Each cog can modify the `new_request_state` as needed (e.g., adjusting max_close based on risk limits). + - Finally, it returns the potentially modified `new_request_state`. + """ + for cog in self._cogs.values(): + if cog.enabled: + cog.on_new_order_request(new_request_state) + return new_request_state def on_new_position(self, new_position_state: NewPositionState) -> NewPositionState: """ @@ -349,7 +399,8 @@ def on_new_position(self, new_position_state: NewPositionState) -> NewPositionSt - Finally, it returns the potentially modified `new_position_state`. """ for cog in self._cogs.values(): - cog.on_new_position(new_position_state) + if cog.enabled: + cog.on_new_position(new_position_state) return new_position_state def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: @@ -359,5 +410,6 @@ def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestam """ lmts = [] for cog in self._cogs.values(): - lmts.append(cog.get_delta_limit(tick_cash=tick_cash, chain_spot=chain_spot, date=date, ticker=ticker)) + if cog.enabled: + lmts.append(cog.get_delta_limit(tick_cash=tick_cash, chain_spot=chain_spot, date=date, ticker=ticker)) return min(lmts) if lmts else float("inf") diff --git a/EventDriven/riskmanager/position/base.py b/EventDriven/riskmanager/position/base.py index 01b0760..9d7631c 100644 --- a/EventDriven/riskmanager/position/base.py +++ b/EventDriven/riskmanager/position/base.py @@ -335,6 +335,20 @@ def on_new_position(self, order: Order, request: OrderRequest) -> None: """ raise NotImplementedError("Subclasses must implement on_new_position().") + def on_new_order_request(self, new_request_state: OrderRequest) -> OrderRequest: + """ + Hook method called when a new order request is generated. + Subclasses can override this to modify the order request before it is processed. + By default, it returns the input unmodified. + - This allows cogs to inject additional logic at the order request stage, + such as adjusting order parameters, adding risk checks, or logging. + - The method receives the new_request_state generated by the signal or position logic. + - Cogs can choose to modify this request (e.g., change quantity, price, etc.) or leave it as is. + - After all cogs have had a chance to process the request, the final (potentially modified) new_request_state is returned and will be used by the Order Picker. + - This provides a powerful extension point for cogs to influence order generation without needing to modify the core signal or position logic. + """ + return new_request_state + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: """ Hook method to provide delta limits for position sizing. diff --git a/EventDriven/riskmanager/position/cogs/analyze_utils.py b/EventDriven/riskmanager/position/cogs/analyze_utils.py index 18fcbe4..3bc265e 100644 --- a/EventDriven/riskmanager/position/cogs/analyze_utils.py +++ b/EventDriven/riskmanager/position/cogs/analyze_utils.py @@ -303,7 +303,7 @@ def greek_check(greek_value: float, greek_threshold: float, qty: int = 1, greate 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) + required_qty = max(int(abs(greek_threshold) // abs(per_greek)), 0) quantity_diff = abs(qty) - abs(required_qty) return _bool, quantity_diff else: @@ -357,7 +357,7 @@ def analyze_position( # Moneyness Check if strategy_enabled_actions.moneyness: if any(abs(m) > abs(moneyness_limit) for m in moneyness_list): # Inline the check - m = min(moneyness_list, key=abs) + m = max(moneyness_list, key=abs) action = ROLL(trade_id=trade_id, action=Changes(quantity_diff=0, new_quantity=qty)) action.reason = f"position is too ITM ({m} exceeds {moneyness_limit})" logger.debug(f"ROLL action for {trade_id} due to moneyness check. Moneyness values: {moneyness_list}, threshold: {moneyness_limit}") @@ -408,13 +408,16 @@ def analyze_position( ## IF new quantity is positive, create ADJUST action if new_qty > 0: + logger.info(f"Calculated new quantity for {trade_id} after adjusting for {greek} breach: {new_qty}") 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. + ## If we have 1 qty, _greek_check always returns True and q_diff == 0. We want to roll instead of adjusting to zero, so create ROLL action instead of ADJUST when q_diff == 0 + # elif q_diff == 0 and qty == 1: elif new_qty == 0: + logger.info(f"Calculated new quantity for {trade_id} is zero after adjusting for {greek} breach. Creating ROLL action instead to avoid closing position.") max_adjust_action = ROLL( trade_id=trade_id, action=Changes(quantity_diff=q_diff, new_quantity=0) ) diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index dd77516..4836af4 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -282,6 +282,7 @@ def __init__( self._sizer_configs: Optional[Union[DefaultSizerConfigs, ZscoreSizerConfigs]] = sizer_configs self.position_limits: Dict[str, PositionLimits] = {} self.position_metadata: Dict[str, _LimitsMetaData] = {} + self.allow_buffer = True # Whether to allow a buffer to the delta limit if the calculated position size is very low (e.g. <=2 contracts). self.underlier_list = list(set(underlier_list if underlier_list is not None else [])) if config is None: config = LimitsEnabledConfig() @@ -388,8 +389,8 @@ def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPosit logger.warning(f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}, and even a single contract cannot be afforded based on the available cash of {tick_cash} and option price of {option_price}. Quantity will remain at 0.") ## Update limits to set to delta + buffer to avoid repeated sizing issues if the issue was with limit calculation - logger.warning(f"Delta limit for trade_id {new_pos_state.order['data']['trade_id']} was calculated as {new_pos_state.limits.delta}, which is below the default delta per contract of {delta}. Setting delta limit to {delta * 1.075} to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation.") - new_pos_state.limits.delta = delta * 1.075 # Set delta limit to 7.5% above the delta of a single contract to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation + logger.warning(f"Delta limit for trade_id {new_pos_state.order['data']['trade_id']} was calculated as {new_pos_state.limits.delta}, which is below the default delta per contract of {delta}. Setting delta limit to {delta * 1.15} to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation.") + new_pos_state.limits.delta = delta * 1.15 # Set delta limit to 15% above the delta of a single contract to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation pos_lmts = PositionLimits( delta=new_pos_state.limits.delta, dte=self.config.default_dte, @@ -399,7 +400,7 @@ def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPosit self._save_position_limits(order["data"]["trade_id"], order["signal_id"], pos_lmts) ## Update metadata as well to reflect the new limits and quantity - metadata = self.position_metadata.get(new_pos_state.order["data"]["trade_id"]) + metadata = self._get_metadata(new_pos_state.order["data"]["trade_id"]) if metadata is not None: metadata.delta_lmt = new_pos_state.limits.delta metadata.new_quantity = new_pos_state.order["data"]["quantity"] @@ -442,7 +443,19 @@ def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: ) logger.info(f"Storing position metadata: {metadata}") - self.position_metadata[order["data"]["trade_id"]] = metadata + self._store_metadata(metadata) + + def _store_metadata(self, metadata: _LimitsMetaData) -> None: + """ + Store the given metadata in the position_metadata dictionary. + """ + self.position_metadata[metadata.trade_id] = metadata + + def _get_metadata(self, trade_id: str) -> Optional[_LimitsMetaData]: + """ + Retrieve metadata for a given trade ID. + """ + return self.position_metadata.get(trade_id, None) def _calculate_limits(self, new_pos_state: NewPositionState) -> float: """ @@ -500,6 +513,32 @@ 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}." ) + + ## Add buffer to delta limit if quantity is <=2 to avoid repeatedly hitting delta limit. + elif q <= 2 and self.allow_buffer: + logger.warning( + f"Calculated position size is {q} for order {order['data']['trade_id']}, which is very low and may indicate that the position is hitting the delta limit. Delta per contract is {delta} and delta limit is {delta_lmt}. Consider reviewing the position or adjusting the delta limit to allow for more flexibility." + ) + if isinstance(self.sizer, ZscoreRVolSizer): + rvol_z = self.sizer.scaler.get_rvol_on_date( + sym=new_position_state.symbol, date=request.date + ) + multiplier = min( + 1 + 0.15 * max(rvol_z, 0), + 1.20 + ) + else: + multiplier = 1.15 + new_delta_lmt = delta_lmt * multiplier + lmts = PositionLimits( + delta=new_delta_lmt, + dte=self.config.default_dte, + moneyness=self.config.default_moneyness, + creation_date=request.date, + ) + self._save_position_limits(order["data"]["trade_id"], order["signal_id"], lmts) + logger.warning(f"Delta limit for order {order['data']['trade_id']} has been increased from {delta_lmt} to {new_delta_lmt} to allow for a larger position size and to avoid repeatedly hitting the delta limit. This adjustment is based on a multiplier of {multiplier} applied to the original delta limit.") + logger.info(f"Updated position quantity to {q} for order {order['data']['trade_id']}.") new_position_state.order = Order.from_dict(order_dict) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py new file mode 100644 index 0000000..623b302 --- /dev/null +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -0,0 +1,160 @@ +from typing import Tuple, Optional +import math +import pandas as pd +from EventDriven.configs.core import BaseCogConfig +from EventDriven.dataclasses.states import NewPositionState +from EventDriven.dataclasses.orders import OrderRequest +from trade.helpers.Logging import setup_logger +from EventDriven.riskmanager.position.base import BaseCog +from EventDriven.dataclasses.states import ( + PositionAnalysisContext, + CogActions, + PositionState +) +from EventDriven.riskmanager.actions import CLOSE, ROLL, HOLD +from pydantic.dataclasses import dataclass as pydantic_dataclass +logger = setup_logger("EventDriven.riskmanager.position.cogs.pnl_monitor", stream_log_level="INFO") + +@pydantic_dataclass +class PnlMonitorConfig(BaseCogConfig): + """ + Configuration dataclass for PnLMonitorCog. + """ + + name: Optional[str] = "PnLMonitorCog" + enabled: bool = True + + +class PnLMonitorCog(BaseCog): + """ + Cog to monitor the PnL of open positions and trigger alerts or actions based on predefined thresholds. + """ + default_config = PnlMonitorConfig() + + def __init__(self, config: Optional[PnlMonitorConfig] = None): + if config is None: + config = PnlMonitorConfig() + super().__init__(config) + self.config = config + + def on_new_position(self, new_position_state: NewPositionState) -> None: + """ + Don't do anything on new position. + """ + pass + + def on_new_order_request(self, new_request_state: OrderRequest) -> None: + """ + Don't do anything on new order request. + """ + logger.info(f"Received new order request for {new_request_state.symbol} with signal ID {new_request_state.signal_id}. Monitoring PnL for this request.") + + + pnl = new_request_state.symbol_total_pnl + tick_cash = new_request_state.tick_cash + + ## Scale to dollar amount for easier interpretation + tick_cash = tick_cash * 100 if not new_request_state.is_tick_cash_scaled else tick_cash + + ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. + if pnl is not None and pnl > 0: + additional_tick_cash = (pnl * 0.25) ## Add 25% of the profits to the tick cash to scale up the position and lock in profits. + + ## Undo pnl from tick cash to avoid double counting, then add the additional tick cash to lock in profits. + undone_pnl_tick_cash = tick_cash - pnl + new_tick_cash = undone_pnl_tick_cash + additional_tick_cash + logger.info(f"Details: PnL: {pnl:.2f}, Original Tick Cash: {tick_cash:.2f}, Undone PnL Tick Cash: {undone_pnl_tick_cash:.2f}, Additional Tick Cash to Lock in Profits: {additional_tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}") + logger.info(f"Adding {additional_tick_cash:.2f} to tick cash for {new_request_state.symbol} to lock in profits. Original Tick Cash: {tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}, PnL: {pnl:.2f}") + new_request_state.tick_cash = new_tick_cash + new_request_state.is_tick_cash_scaled = True + + else: + logger.info(f"No profits to lock in for {new_request_state.symbol}. Tick Cash remains at {tick_cash:.2f}. PnL: {pnl:.2f}") + + def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: + """ + Analyze the current position context and return any actions to be taken. + For this PnL monitor, we will not take any specific actions, but we could log or trigger alerts if needed. + """ + logger.info(f"PnLMonitor received position analysis context {portfolio_context}. Analyzing PnL for open positions.") + opinions = [] + + ## Rules: + ## 1. If PnL is greater than 50% of entry price, close half the position if quantity > 1 to lock in profits. + ## 2. If quantity is 1 and PnL is greater than 150% of entry price, ROLL the position. + ## 2a. If have closed before, and remainding quantity is > 1, ROLL when PnL is greater than 150% of entry price to lock in profits. + positions = portfolio_context.portfolio.positions + bkt_info = portfolio_context.portfolio_meta + t_plus_n = bkt_info.t_plus_n + portfolio_state = portfolio_context.portfolio + last_updated = portfolio_state.last_updated + t_plus_n_timedelta = pd.Timedelta(days=t_plus_n) + for pos_state in positions: + pl_pct = pos_state.pnl / (pos_state.entry_price * pos_state.quantity) if pos_state.entry_price * pos_state.quantity != 0 else 0 + if pl_pct > 0.5 and pos_state.quantity > 1: + qdiff = math.ceil(pos_state.quantity / 2) + new_q = pos_state.quantity - qdiff + logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}") + action = CLOSE( + trade_id=pos_state.trade_id, + action={"quantity_diff": qdiff, "new_quantity": new_q} + ) + action.analysis_date = portfolio_context.date + action.reason = f"PnL is {pl_pct:.2%} which is greater than 50% of entry price. Closing half the position to lock in profits." + action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}" + tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day + action.effective_date = last_updated + tplus_n_timedelta + pos_state.action = action + opinions.append(pos_state) + + elif pl_pct > 1.0: + if pos_state.quantity == 1 or (self._has_closed_before(pos_state.trade_id, pos_state) and pos_state.quantity > 1): + logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits.") + action = ROLL( + trade_id=pos_state.trade_id, + action={"quantity_diff": 0, "new_quantity": pos_state.quantity} + ) + action.analysis_date = portfolio_context.date + action.reason = f"PnL is {pl_pct:.2%} which is greater than 150% of entry price. Rolling the position to lock in profits." + action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits." + tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day + action.effective_date = last_updated + tplus_n_timedelta + pos_state.action = action + opinions.append(pos_state) + else: + logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken.") + pos_state.action = HOLD(trade_id=pos_state.trade_id) + pos_state.action.reason = f"PnL is {pl_pct:.2%}. No action taken." + pos_state.action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken." + opinions.append(pos_state) + + + + + + return CogActions(opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name) + + def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[int, int]: + """ + Helper method to aggregate the total open and close quantities for a position based on its trade entries. + """ + entries = pos_state.trades.entries() if pos_state.trades is not None else pd.DataFrame() + if entries.empty: + return 0, 0 + open_qty = entries[entries["direction"] == "BUY"]["quantity"].sum() + close_qty = entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].sum() if not entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].empty else 0 + return open_qty, close_qty + + + def _has_closed_before(self, trade_id: str, portfolio_context: PositionState) -> bool: + """ + Helper method to check if a position has been closed before based on its trade ID. + + True if there has been at least one close transaction (SELL or EXERCISE) for the given trade ID, False otherwise. + False if there are no transactions for the trade ID or if all transactions are BUY. + """ + open_qty, close_qty = self._get_current_close_open_quantity(portfolio_context) + if open_qty == 0: + return False + return close_qty > 0 + diff --git a/EventDriven/trade.py b/EventDriven/trade.py index 47219c2..c6acb97 100644 --- a/EventDriven/trade.py +++ b/EventDriven/trade.py @@ -24,8 +24,18 @@ def __init__(self, trade_id: str, symbol: str, signal_id: str = None): self.entry_date = None self.exit_date = None self.current_price = None - self.stats = None self.signal_id = signal_id + self.closed_pnl = 0.0 + self.unrealized_pnl = 0.0 + self.total_pnl = 0.0 + + @property + def stats(self): + """ + Returns a DataFrame of aggregated statistics for the trade. + Caches the result and only recalculates if the underlying data has changed. + """ + return pd.DataFrame([self.aggregate()]) def __getitem__(self, key): """ @@ -47,6 +57,23 @@ def __setitem__(self, key, value): else: raise KeyError(f"Key '{key}' not found in stats.") + def _calculate_pnl_from_ledgers(self): + """Calculate realized/unrealized/total PnL directly from buy/sell ledgers.""" + entry_qty = self.buy_ledger.quantity + exit_qty = self.sell_ledger.quantity + closed_qty = min(entry_qty, exit_qty) + open_qty = max(entry_qty - exit_qty, 0) + + entry_price = self.buy_ledger.avg_price + exit_price = self.sell_ledger.avg_price + + realized = (exit_price - entry_price) * closed_qty if closed_qty > 0 else 0.0 + unrealized = ( + (self.current_price - entry_price) * open_qty if open_qty > 0 and self.current_price is not None else 0.0 + ) + total = realized + unrealized + return realized, unrealized, total + def update(self, fill_event: FillEvent): """ Update the appropriate ledger based on the fill event direction @@ -62,7 +89,7 @@ def update(self, fill_event: FillEvent): if self.is_closed(): self.exit_date = fill_event.datetime - self.stats = pd.DataFrame([self.aggregate()]) + self.update_pnls() def _update_kw(self, **entry_kwargs): """ @@ -101,7 +128,7 @@ def _update_kw(self, **entry_kwargs): if self.is_closed(): self.exit_date = entry_time - self.stats = pd.DataFrame([self.aggregate()]) + self.update_pnls() def is_closed(self): """ @@ -133,7 +160,7 @@ def aggregate(self): stats["EntrySlippage"] = self.buy_ledger.slippage stats["EntryQuantity"] = self.buy_ledger.quantity stats["EntryAuxilaryCost"] = self.buy_ledger.aux_cost - stats["TotalEntryCost"] = self.buy_ledger.avg_total_cost + stats["TotalEntryCost"] = self.buy_ledger.total_cost # Calculate metrics for sell transactions stats["ExitPrice"] = self.sell_ledger.avg_price @@ -141,7 +168,7 @@ def aggregate(self): stats["ExitSlippage"] = self.sell_ledger.slippage stats["ExitQuantity"] = self.sell_ledger.quantity stats["ExitAuxilaryCost"] = self.sell_ledger.aux_cost - stats["TotalExitCost"] = self.sell_ledger.avg_total_cost + stats["TotalExitCost"] = self.sell_ledger.total_cost stats["Quantity"] = stats["ExitQuantity"] @@ -182,14 +209,31 @@ def aggregate(self): stats["Duration"] = (self.exit_date - self.entry_date).days else: stats["Duration"] = None - + return stats + + def update_pnls(self): + """ + Helper method to update the closed, unrealized, and total PnL for the trade. + Should be called after any update to the ledgers or current price. + """ + self.closed_pnl, self.unrealized_pnl, self.total_pnl = self._calculate_pnl_from_ledgers() def update_current_price(self, price): """ Update the current market price for calculating unrealized PnL """ self.current_price = price + self.update_pnls() + + def get_current_pnl(self): + """Return current realized/unrealized/total PnL computed from ledgers.""" + self.update_pnls() + return { + "realized": self.closed_pnl, + "unrealized": self.unrealized_pnl, + "total": self.total_pnl, + } def entries(self): """ diff --git a/EventDriven/tradeLedger.py b/EventDriven/tradeLedger.py index 07367e2..7b0f3f0 100644 --- a/EventDriven/tradeLedger.py +++ b/EventDriven/tradeLedger.py @@ -25,7 +25,8 @@ class TradeLedger: ledger (dict): A dictionary storing trade entries, with datetime as keys. ledger_df (pd.DataFrame): A DataFrame representation of the ledger for easier analysis. market_value (float): The total market value of the trades. This is a running sum. - avg_total_cost (float): The average total cost of all trades. This is an average value. + total_cost (float): The cumulative total cost across all fills. + avg_total_cost (float): The average fill-level total cost across all fills. aux_cost (float): The total auxiliary costs (commission + slippage) incurred. This is a running sum. """ @@ -38,6 +39,7 @@ def __init__(self, id: str) -> None: self.ledger = [] self.ledger_df = None self.market_value = 0.0 + self.total_cost = 0.0 self.avg_total_cost = 0.0 self.aux_cost = 0.0 @@ -111,6 +113,15 @@ def _add_entry_common( Everything is scaled to dollar amounts and by quantity.""" + if quantity is None or quantity <= 0: + raise ValueError(f"Quantity must be a positive integer. Received: {quantity}") + + def _weighted_avg_price(existing_avg_price, existing_quantity, new_price, new_quantity): + total_quantity = existing_quantity + new_quantity + if total_quantity == 0: + return 0.0 + return ((existing_avg_price * existing_quantity) + (new_price * new_quantity)) / total_quantity + uid = f"{trade_id}_{signal_id}_{entry_time}" # Normalize monetary fields unless explicitly disabled price_val = normalize_dollar_amount(fill_cost / quantity) if normalize else fill_cost / quantity @@ -150,10 +161,10 @@ def _add_entry_common( "direction": direction, } - self.avg_price = ((self.avg_price * self.quantity) + (entry["price"] * quantity)) / (self.quantity + quantity) - self.avg_total_cost = ((self.avg_total_cost * self.quantity) + (entry["total_cost"] * quantity)) / ( - self.quantity + quantity - ) + self.avg_price = _weighted_avg_price(self.avg_price, self.quantity, entry["price"], quantity) + self.total_cost += entry["total_cost"] + fill_count = len(self.ledger) + 1 + self.avg_total_cost = self.total_cost / fill_count self.aux_cost += entry["aux_cost"] self.quantity += entry["quantity"] self.commission += entry["commission"] diff --git a/module_test/test_trade_pnl_consistency.py b/module_test/test_trade_pnl_consistency.py new file mode 100644 index 0000000..3ecacdb --- /dev/null +++ b/module_test/test_trade_pnl_consistency.py @@ -0,0 +1,98 @@ +"""Regression checks for Trade and TradeLedger PnL consistency. + +Validates: +1. total_pnl stays synchronized with closed_pnl + unrealized_pnl +2. market price updates refresh total_pnl +3. ledger rejects zero or negative quantity + +Usage: + /Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/bin/python \ + module_test/test_trade_pnl_consistency.py +""" + +from __future__ import annotations + +from datetime import datetime +from math import isclose + +from EventDriven.trade import Trade +from EventDriven.tradeLedger import TradeLedger + + +def test_trade_pnl_consistency() -> None: + trade = Trade(trade_id="T1", symbol="AAPL", signal_id="S1") + + # Buy 2 contracts at 100 each. + trade._update_kw( + entry_time=datetime(2026, 1, 2), + direction="BUY", + fill_cost=200.0, + quantity=2, + symbol="AAPL", + commission=0.0, + market_value=200.0, + slippage=0.0, + normalize=False, + ) + trade.update_current_price(120.0) + + assert isclose(trade.closed_pnl, 0.0) + assert isclose(trade.unrealized_pnl, 40.0) + assert isclose(trade.total_pnl, 40.0) + assert isclose(trade.total_pnl, trade.closed_pnl + trade.unrealized_pnl) + + # Close 1 contract at 130. + trade._update_kw( + entry_time=datetime(2026, 1, 3), + direction="SELL", + fill_cost=130.0, + quantity=1, + symbol="AAPL", + commission=0.0, + market_value=130.0, + slippage=0.0, + normalize=False, + ) + + assert isclose(trade.closed_pnl, 30.0) + assert isclose(trade.unrealized_pnl, 20.0) + assert isclose(trade.total_pnl, 50.0) + assert isclose(trade.total_pnl, trade.closed_pnl + trade.unrealized_pnl) + + # Mark remaining open position down to 90. + trade.update_current_price(90.0) + + assert isclose(trade.closed_pnl, 30.0) + assert isclose(trade.unrealized_pnl, -10.0) + assert isclose(trade.total_pnl, 20.0) + assert isclose(trade.total_pnl, trade.closed_pnl + trade.unrealized_pnl) + + +def test_ledger_positive_quantity_guard() -> None: + ledger = TradeLedger("guard_test") + + raised = False + try: + ledger._add_entry_kw( + entry_time=datetime(2026, 1, 2), + trade_id="T2", + signal_id="S2", + fill_cost=0.0, + quantity=0, + symbol="AAPL", + commission=0.0, + market_value=0.0, + slippage=0.0, + direction="BUY", + normalize=False, + ) + except ValueError: + raised = True + + assert raised, "TradeLedger must reject non-positive quantity entries" + + +if __name__ == "__main__": + test_trade_pnl_consistency() + test_ledger_positive_quantity_guard() + print("All trade consistency tests passed.") From 97bba3f666ecab32b98d519390a7d765a4f2b656 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:30 -0400 Subject: [PATCH 37/81] feat: enhance order picker scoring and fallback selection --- EventDriven/riskmanager/picker/builder.py | 114 +++++-------- EventDriven/riskmanager/picker/iv_helper.py | 56 ++++--- .../riskmanager/picker/naked_option.py | 29 ++-- .../riskmanager/picker/order_picker.py | 152 +++++++++++++----- EventDriven/riskmanager/picker/utils.py | 61 ++++++- .../riskmanager/picker/vertical_spread.py | 23 ++- 6 files changed, 270 insertions(+), 165 deletions(-) diff --git a/EventDriven/riskmanager/picker/builder.py b/EventDriven/riskmanager/picker/builder.py index 0b37d3d..360fce3 100644 --- a/EventDriven/riskmanager/picker/builder.py +++ b/EventDriven/riskmanager/picker/builder.py @@ -1,33 +1,32 @@ - import numpy as np from typing import Optional -from trade.helpers.helper import parse_option_tick -from EventDriven.riskmanager.picker import filter_contracts, OrderSchema +from EventDriven.riskmanager.picker import filter_contracts from EventDriven.configs.core import ScoringConfigs import pandas as pd from trade.helpers.Logging import setup_logger +from trade.helpers.helper import parse_option_tick, to_datetime from EventDriven.riskmanager.utils import populate_cache_with_chain from EventDriven.dataclasses.orders import OrderRequest -from .vertical_spread import _extract_order_for_vertical_spread, vertical_spread_order_builder, vertical_spread_pairer_by_exp -from .naked_option import _extract_order_for_naked_option, naked_option_order_builder, naked_option_by_exp +from .vertical_spread import _extract_order_for_vertical_spread, vertical_spread_pairer_by_exp +from .naked_option import _extract_order_for_naked_option, naked_option_by_exp from ...types import ResultsEnum, OrderData from .iv_helper import _add_greeks_and_iv_to_chain from .chain_scoring import _score_chain - logger = setup_logger("EventDriven.riskmanager.picker.builder", stream_log_level="WARNING") -BUILDER_FACTORY = { - "vertical": vertical_spread_order_builder, - "naked": naked_option_order_builder, -} - def validate_order(order: dict, date: pd.Timestamp, spot: Optional[float] = None) -> bool: - """ - Validates the order dictionary structure and contents. - Raises ValueError if validation fails. + """Validate constructed order payload and derive basic risk metrics. + + Args: + order: The order dictionary returned by order extraction helpers. + date: Trade date used for DTE computation. + spot: Optional underlying spot used for moneyness computation. + + Returns: + True when validation passes. """ assert "result" in order, "Order must have a 'result' key." assert order["result"] in [e.value for e in ResultsEnum], f"Invalid result value: {order['result']}." @@ -55,21 +54,23 @@ def validate_order(order: dict, date: pd.Timestamp, spot: Optional[float] = None ## Add min_dte to order data = order.get("data", {}) - ## No data to validate, so we can skip DTE calculation. + ## No data to validate, so we can skip DTE calculation. if not data: return True - + ## If there is data, we want to calculate DTE for risk management purposes. trade_id = order["data"].get("trade_id", None) dt = [] moneyness = [] if trade_id is not None and hasattr(trade_id, "meta"): + date_dt = to_datetime(date) for _, meta in trade_id.meta.items(): for m in meta: - dt.append((pd.to_datetime(m["exp_date"]) - pd.to_datetime(date)).days) + dt.append((to_datetime(m["exp_date"]) - date_dt).days) if spot is not None: moneyness.append(m["strike"] / spot if m["put_call"].lower() == "p" else spot / m["strike"]) + order.setdefault("metrics", {}) order["metrics"]["min_dte"] = min(dt) if dt else None order["metrics"]["max_dte"] = max(dt) if dt else None order["metrics"]["min_moneyness"] = min(moneyness) if moneyness else None @@ -78,63 +79,23 @@ def validate_order(order: dict, date: pd.Timestamp, spot: Optional[float] = None def order_builder( - unfiltered_chain: pd.DataFrame = None, - schema: OrderSchema = None, - spot: float = None, - date: pd.Timestamp = None, - delta_lmt: Optional[float] = None, - *, - req: Optional[OrderRequest] = None, - configs: Optional[ScoringConfigs] = None, + req: OrderRequest, + configs: ScoringConfigs, ) -> OrderData: - """ - Build an order based on the unfiltered option chain and the provided schema. + """Build an order from an OrderRequest using scoring-based selection. + Args: - unfiltered_chain (pd.DataFrame): The unfiltered option chain DataFrame. - schema (OrderSchema): The order schema containing parameters for building the order. + req: The order request containing symbol, date, option type, and pricing constraints. + configs: Scoring configuration defining moneyness, DTE, spread, and pricing targets. + Returns: OrderData: Detailed trade execution data including positions and pricing. """ - if req is not None and configs is not None: - return _order_builder_with_scoring(req=req, configs=configs) - - if unfiltered_chain is None or schema is None or spot is None or date is None: - raise ValueError("unfiltered_chain, schema, spot, and date must all be provided if req and configs are not used.") - # Step 1: Filter contracts based on schema - filtered_chain = filter_contracts( - df=unfiltered_chain, - schema=schema, - spot=spot, - ) - logger.info(f"Recieved {len(unfiltered_chain)} contracts from data source. Delta limit for this order: {delta_lmt}") - if not filtered_chain.empty: - filtered_chain = _add_greeks_and_iv_to_chain(filtered_chain, date, spot) - - - logger.info(f"Filtered chain size: {len(filtered_chain)} contracts after applying schema filters.") - - # Step 2: Build order using the appropriate builder function - structure_type = schema.get("strategy") - if structure_type not in BUILDER_FACTORY: - raise ValueError( - f"Unsupported structure type: {structure_type}. Supported types are: {list(BUILDER_FACTORY.keys())}" - ) - - builder_function = BUILDER_FACTORY[structure_type] - order = builder_function( - filtered_chain=filtered_chain, - schema=schema, - delta_lmt=delta_lmt, - ) - - # Step 3: Validate the constructed order - try: - validate_order(order, date, spot) - except AssertionError as e: - raise ValueError(f"Order validation failed: {e}") from e - + order = _order_builder_with_scoring(req=req, configs=configs) + validate_order(order, date=req.date, spot=req.chain_spot) return order + def _order_builder_with_scoring( req: OrderRequest, configs: ScoringConfigs, @@ -147,11 +108,13 @@ def _order_builder_with_scoring( ## Step 1: Build scored chain scored_chain = build_scored_chain(req=req, configs=configs) if scored_chain.empty: - logger.warning(f"Following are used for filtering but resulted in empty chain: min_moneyness={configs.min_moneyness}, max_moneyness={configs.max_moneyness}, target_dte={configs.target_dte}, dte_tolerance={configs.dte_tolerance}, spread_ratio_max={configs.pct_spread_max}, mid_price_range=({configs.mid_lower_limit}, {configs.mid_upper_limit})") - order = _extract_order_with_scoring_config(chain_row=pd.Series(), configs=configs) + logger.warning( + f"Following are used for filtering but resulted in empty chain: min_moneyness={configs.min_moneyness}, max_moneyness={configs.max_moneyness}, target_dte={configs.target_dte}, dte_tolerance={configs.dte_tolerance}, spread_ratio_max={configs.pct_spread_max}, mid_price_range=({configs.mid_lower_limit}, {configs.mid_upper_limit})" + ) + order = _extract_order_with_scoring_config(chain_row=pd.Series(), configs=configs) order["result"] = ResultsEnum.NO_CONTRACTS_FOUND.value return order - + ## Step 2: Extract top scored row and build order from it top_scored_row = scored_chain.iloc[0] @@ -160,6 +123,7 @@ def _order_builder_with_scoring( validate_order(order, date=req.date, spot=req.chain_spot) return order + def build_scored_chain(req: OrderRequest, configs: ScoringConfigs) -> pd.DataFrame: """ Build a scored option chain based on the request and scoring configurations. This involves fetching the chain, filtering it, adding Greeks and IV, pairing contracts if necessary, and then applying the scoring functions to rank the contracts. @@ -179,7 +143,6 @@ def build_scored_chain(req: OrderRequest, configs: ScoringConfigs) -> pd.DataFra Returns: pd.DataFrame: A DataFrame containing the scored option chain, with additional columns for each """ - ## Step 1: Get chain chain = populate_cache_with_chain(tick=req.symbol, date=req.date, chain_spot=req.chain_spot) @@ -194,12 +157,15 @@ def build_scored_chain(req: OrderRequest, configs: ScoringConfigs) -> pd.DataFra dte_tol=configs.dte_tolerance, ) if filtered.empty: + logger.warning( + f"Filtering resulted in empty chain for {req.symbol} on {req.date}. Parameters used for filtering: option_type={req.option_type}, min_moneyness={configs.min_moneyness}, max_moneyness={configs.max_moneyness}, target_dte={configs.target_dte}, dte_tolerance={configs.dte_tolerance}" + ) return filtered ## Step 3: Add Greeks and IV filtered = _add_greeks_and_iv_to_chain(filtered=filtered, date=req.date, chain_spot=req.chain_spot) - ## Step 4: Pair vertical spreads + ## Step 4: Pair vertical spreads or naked options is_call = req.option_type.lower() == "c" filtered = filtered.sort_values(by="strike", ascending=is_call).reset_index(drop=True) vertical_chain = ( @@ -239,7 +205,8 @@ def build_scored_chain(req: OrderRequest, configs: ScoringConfigs) -> pd.DataFra ].sort_values("total_score", ascending=False) return scored_chain -def _extract_order_with_scoring_config(chain_row: pd.Series, configs: ScoringConfigs) -> OrderSchema: + +def _extract_order_with_scoring_config(chain_row: pd.Series, configs: ScoringConfigs) -> OrderData: """ Extract order schema from a scored chain row, applying any necessary adjustments based on scoring configs. """ @@ -250,4 +217,3 @@ def _extract_order_with_scoring_config(chain_row: pd.Series, configs: ScoringCon if configs.strategy == "naked" else _extract_order_for_vertical_spread(chain_row, schema=schema) ) - diff --git a/EventDriven/riskmanager/picker/iv_helper.py b/EventDriven/riskmanager/picker/iv_helper.py index 66d807b..c6091f9 100644 --- a/EventDriven/riskmanager/picker/iv_helper.py +++ b/EventDriven/riskmanager/picker/iv_helper.py @@ -1,4 +1,3 @@ - from EventDriven._vars import load_riskmanager_cache from trade.optionlib.assets.forward import vectorized_forward_continuous from trade.helpers.helper import time_distance_helper @@ -14,8 +13,6 @@ rates_cache = {} - - def get_rates_on_date(date): string_date = pd.to_datetime(date).strftime("%Y-%m-%d") if string_date in rates_cache: @@ -35,13 +32,22 @@ def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chai ## Filter for contracts with NaN iv to start adding greeks and iv nan_iv_chain = filtered[filtered["iv"].isna()] + if nan_iv_chain.empty: + return filtered ## Check cache for existing iv and greeks before calculating cached_data = {} - for idx, row in nan_iv_chain.iterrows(): - contract_key = (row["root"], row["expiration"], row["strike"], row["right"], date) - if contract_key in CHAIN_GREEKS_CACHE: - cached_data[idx] = CHAIN_GREEKS_CACHE[contract_key] + cache_get = CHAIN_GREEKS_CACHE.get + for idx, root, expiration, strike, right in zip( + nan_iv_chain.index, + nan_iv_chain["root"].values, + nan_iv_chain["expiration"].values, + nan_iv_chain["strike"].values, + nan_iv_chain["right"].values, + ): + cached_value = cache_get((root, expiration, strike, right, date)) + if cached_value is not None: + cached_data[idx] = cached_value ## If there are cached values, add them to the filtered DataFrame and return early if cached_data: @@ -57,12 +63,17 @@ def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chai ## Filter out the contracts that were found in the cache to avoid redundant calculations if cached_data: nan_iv_chain = nan_iv_chain[~nan_iv_chain.index.isin(cached_data.keys())] + if nan_iv_chain.empty: + return filtered + ## Get dividend data div_dm = DividendDataManager(filtered["root"].iloc[0]) # Assuming all contracts in the chain have the same root q = div_dm.get_schedule(date, date, DivType.CONTINUOUS).timeseries.values[0] ## Calculate forward price for the contracts with NaN iv - t = np.array(time_distance_helper([date] * len(nan_iv_chain["expiration"].values), nan_iv_chain["expiration"].values)) + t = np.array( + time_distance_helper([date] * len(nan_iv_chain["expiration"].values), nan_iv_chain["expiration"].values) + ) q_factor = np.exp(-q * t) f = vectorized_forward_continuous( S=[chain_spot] * len(nan_iv_chain["expiration"].values), @@ -101,17 +112,22 @@ def _add_greeks_and_iv_to_chain(filtered: pd.DataFrame, date: pd.Timestamp, chai filtered.loc[nan_iv_chain.index, "rho"] = greeks_df["rho"] filtered.loc[nan_iv_chain.index, "volga"] = greeks_df["volga"] - ## Store the calculated iv and greeks in the CHAIN_GREEKS_CACHE - for _, row in filtered.iterrows(): - contract_key = (row["root"], row["expiration"], row["strike"], row["right"], date) - CHAIN_GREEKS_CACHE[contract_key] = { - "iv": row["iv"], - "delta": row["delta"], - "gamma": row["gamma"], - "vega": row["vega"], - "theta": row["theta"], - "rho": row["rho"], - "volga": row["volga"], + ## Store only newly computed iv and greeks in the CHAIN_GREEKS_CACHE + for idx, root, expiration, strike, right in zip( + nan_iv_chain.index, + nan_iv_chain["root"].values, + nan_iv_chain["expiration"].values, + nan_iv_chain["strike"].values, + nan_iv_chain["right"].values, + ): + CHAIN_GREEKS_CACHE[(root, expiration, strike, right, date)] = { + "iv": filtered.at[idx, "iv"], + "delta": filtered.at[idx, "delta"], + "gamma": filtered.at[idx, "gamma"], + "vega": filtered.at[idx, "vega"], + "theta": filtered.at[idx, "theta"], + "rho": filtered.at[idx, "rho"], + "volga": filtered.at[idx, "volga"], } - return filtered \ No newline at end of file + return filtered diff --git a/EventDriven/riskmanager/picker/naked_option.py b/EventDriven/riskmanager/picker/naked_option.py index b0da851..1e32d71 100644 --- a/EventDriven/riskmanager/picker/naked_option.py +++ b/EventDriven/riskmanager/picker/naked_option.py @@ -5,7 +5,7 @@ from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema from trade.helpers.Logging import setup_logger -from .utils import _verify_delta_in_chain, _delta_lmt, _build_common_pair_mask +from .utils import _verify_delta_in_chain, _delta_lmt, _build_common_pair_masks, _finalize_paired_output logger = setup_logger("EventDriven.riskmanager.picker.naked_option") @@ -18,6 +18,7 @@ def naked_option_by_exp( max_pct_width: float = np.inf, min_oi: int = 0, delta_lmt: Optional[float] = None, + return_mask: bool = False, **kwargs, ) -> pd.DataFrame: """ @@ -101,7 +102,7 @@ def naked_option_by_exp( ] shrunk_delta_lmt = round(_delta_lmt(delta_lmt) * 0.95, 2) - full_mask = _build_common_pair_mask( + mask_df = _build_common_pair_masks( spread_mid=paired_opttick["spread_mid"], spread_bid=paired_opttick["spread_bid"], spread_ask=paired_opttick["spread_ask"], @@ -114,25 +115,17 @@ def naked_option_by_exp( min_oi=min_oi, delta_lmt=shrunk_delta_lmt, ) - - mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) - spread_oi_mask = paired_opttick["spread_oi"] >= min_oi - pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width - spread_bid_mask = paired_opttick["spread_bid"] > 0 - spread_ask_mask = paired_opttick["spread_ask"] > 0 - delta_mask = ( - paired_opttick["spread_delta"].abs() <= abs(shrunk_delta_lmt) - ) # Manually shrinking the delta limit by 10% to be more conservative. And avoid issues in picking options that are right on the edge of the delta limit + full_mask = mask_df["full_mask"] logger.debug( f"Number of naked options after applying all filters: {full_mask.sum()}. Number before filtering: {len(paired_opttick)}" ) ## DEBUG - logger.debug(f"mid_mask: {mid_mask.sum()} (between {min_total_price} and {max_total_price}), ") - logger.debug(f"spread_oi_mask: {spread_oi_mask.sum()} (>= {min_oi}), ") - logger.debug(f"pct_width_mask: {pct_width_mask.sum()} (<= {max_pct_width}), ") - logger.debug(f"spread_bid_mask: {spread_bid_mask.sum()}, (>0) ") - logger.debug(f"spread_ask_mask: {spread_ask_mask.sum()}, (>0) ") - logger.debug(f"delta_mask: {delta_mask.sum()} (<= {abs(shrunk_delta_lmt)})") ## DEBUG - return paired_opttick[full_mask].reset_index(drop=True) + logger.debug(f"mid_mask: {mask_df['mid_mask'].sum()} (between {min_total_price} and {max_total_price}), ") + logger.debug(f"spread_oi_mask: {mask_df['spread_oi_mask'].sum()} (>= {min_oi}), ") + logger.debug(f"pct_width_mask: {mask_df['pct_width_mask'].sum()} (<= {max_pct_width}), ") + logger.debug(f"spread_bid_mask: {mask_df['spread_bid_mask'].sum()}, (>0) ") + logger.debug(f"spread_ask_mask: {mask_df['spread_ask_mask'].sum()}, (>0) ") + logger.debug(f"delta_mask: {mask_df['delta_mask'].sum()} (<= {abs(shrunk_delta_lmt)})") ## DEBUG + return _finalize_paired_output(paired_opttick=paired_opttick, mask_df=mask_df, return_mask=return_mask) ## Finder function to identify the best naked diff --git a/EventDriven/riskmanager/picker/order_picker.py b/EventDriven/riskmanager/picker/order_picker.py index bf707e0..7976a0c 100644 --- a/EventDriven/riskmanager/picker/order_picker.py +++ b/EventDriven/riskmanager/picker/order_picker.py @@ -25,7 +25,6 @@ from copy import deepcopy from datetime import datetime -from trade.datamanager.market_data import Optional from ..utils import ( LOOKBACKS, precompute_lookbacks, @@ -40,7 +39,13 @@ from EventDriven.dataclasses.orders import OrderRequest from EventDriven.types import Order import numpy as np +import pandas as pd from trade.helpers.helper import to_datetime +from EventDriven.riskmanager.utils import populate_cache_with_chain +from .iv_helper import _add_greeks_and_iv_to_chain +from .naked_option import naked_option_by_exp +from .vertical_spread import vertical_spread_pairer_by_exp +from . import filter_contracts logger = setup_logger("EventDriven.riskmanager.picker.order_picker") @@ -145,34 +150,7 @@ def lookback(self, value): precompute_lookbacks("2000-01-01", "2030-12-31", _range=[value]) self.__lookback = value - def get_order_new( - self, - schema: OrderSchema, - date: str | datetime, - spot, - chain_spot: float = None, - print_url: bool = False, - delta_lmt: Optional[float] = None, - ): - raise AttributeError( - "OrderPicker.get_order_new is deprecated and has been removed. " - "Use OrderPicker.get_order(request=OrderRequest(...)) instead." - ) - # @dynamic_memoize - def _get_order( - self, - schema: tuple, - date: str | datetime, - spot: float, - chain_spot: float = None, - print_url: bool = False, - delta_lmt: Optional[float] = None, - ) -> dict: - raise AttributeError( - "OrderPicker._get_order is deprecated and has been removed. " - "Use OrderPicker.get_order(request=OrderRequest(...)) instead." - ) # @timeit def get_order(self, request: OrderRequest) -> Order: @@ -198,9 +176,40 @@ def get_order(self, request: OrderRequest) -> Order: ## Add necessary tags for identification order["signal_id"] = request.signal_id order["map_signal_id"] = request.signal_id + + ## L1 Resolution with optional hybrid fallback if order_failed(order): logger.warning(f"Order failed to resolve for request: {request}") - return Order.from_dict(order) + + ## If hybrid strategy is disabled, return the failed order immediately without attempting fallback. + ## This allows the system to recognize the failure and handle it according to its design. + if not self._scoring_config.hybrid_strategy_enabled: + logger.warning("Hybrid strategy is disabled. Returning failed order without fallback.") + return Order.from_dict(order) + + ## If hybrid strategy is enabled, attempt a single fallback with the reverse strategy before giving up + ## This provides a safety net for cases where the primary strategy fails, while still avoiding infinite fallback loops. + ## Reverse strategy is determined based on the original strategy: if the original is "vertical", the fallback will be "naked", and vice versa. + else: + reverse_strategy = "naked" if self._scoring_config.strategy == "vertical" else "vertical" + original_strategy = self._scoring_config.strategy + self._scoring_config.strategy = reverse_strategy + logger.warning(f"Primary strategy '{original_strategy}' failed. Attempting fallback with reverse strategy '{reverse_strategy}'.") + order = self._get_order_with_scoring(request) + self._scoring_config.strategy = original_strategy ## Reset strategy to original after fallback attempt + order["signal_id"] = request.signal_id + order["map_signal_id"] = request.signal_id + + ## If the fallback also fails, log a warning and return the original failed order to ensure the system can recognize the failure state. + if order_failed(order): + logger.warning(f"Fallback order also failed for request: {request}. Returning original failed order.") + return Order.from_dict(order) + + ## If the fallback succeeds, log a warning indicating that the fallback was successful and return the fallback order + else: + logger.warning(f"Fallback order succeeded for request: {request}. Returning fallback order.") + + ## Final post order gen processing order["data"]["quantity"] = 1 order["date"] = to_datetime(request.date).date() order = Order.from_dict(order) @@ -213,12 +222,81 @@ def construct_inputs(self, request: OrderRequest, schema: OrderSchema) -> None: "Use OrderPicker.get_order(request=OrderRequest(...)) instead." ) + def display_chain( + self, + req: OrderRequest, + filter_chain: bool = True, + return_mask: bool = True, + ) -> pd.DataFrame: + """Return the paired option chain for inspection without scoring. + + Executes steps 1-4 of the scored chain pipeline: fetch chain, + enrich with Greeks/IV, pair contracts by expiration, then optionally + filter paired candidates using scoring-config thresholds. Useful for + debugging contract selection or reviewing available spreads before committing + to an order. + + Args: + req: Order request supplying the symbol, date, option type, and chain spot. + filter_chain: If True, applies spread-level moneyness, DTE, and pricing + constraints from the scoring config after pairing. If False, returns + all paired candidates. + + Returns: + pd.DataFrame: Paired contracts (vertical spreads or naked options indexed + by expiration), with Greeks and IV columns added. Empty DataFrame if no + contracts survive filtering or pairing. + + Examples: + >>> picker = OrderPicker(start_date="2025-01-01", end_date="2025-12-31") + >>> req = OrderRequest(symbol="AAPL", date="2025-06-01", option_type="P", + ... chain_spot=190.0, tick_cash=3.0) + >>> df = picker.display_chain(req) + >>> df = picker.display_chain(req, filter_chain=False) # full chain + """ + configs = self._scoring_config + configs = deepcopy(configs) # Avoid mutating shared config state, especially if we adjust mid price limits for display purposes + configs.mid_upper_limit = req.tick_cash / 100 if req.is_tick_cash_scaled else req.tick_cash + + ## Step 1: Fetch chain + chain = populate_cache_with_chain(tick=req.symbol, date=req.date, chain_spot=req.chain_spot) + + + ## Step 2: Filter chain + chain = filter_contracts( + df=chain, + option_type=req.option_type, + spot=req.chain_spot, + min_moneyness=configs.min_moneyness, + max_moneyness=configs.max_moneyness, + target_dte=configs.target_dte, + dte_tol=configs.dte_tolerance, + ) + + if chain.empty: + print("No contracts available after initial filtering. Returning empty DataFrame.") + return chain + + ## Step 3: Add Greeks and IV + chain = _add_greeks_and_iv_to_chain(filtered=chain, date=req.date, chain_spot=req.chain_spot) + + ## Step 4: Pair contracts by expiration + is_call = req.option_type.lower() == "c" + chain = chain.sort_values(by="strike", ascending=is_call).reset_index(drop=True) + pairer = naked_option_by_exp if configs.strategy == "naked" else vertical_spread_pairer_by_exp + paired = ( + chain.groupby("expiration") + .apply( + pairer, + spread_tick=configs.spread_ticks, + min_total_price=configs.mid_lower_limit, + max_total_price=configs.mid_upper_limit, + max_pct_width=configs.pct_spread_max if filter_chain else np.inf, + min_oi=-25 if filter_chain else -np.inf, + delta_lmt=np.inf, + return_mask=return_mask, + ) + .reset_index(drop=True) + ) -def _get_open_order_backtest( - picker: OrderPicker, - request: OrderRequest, -) -> Order: - raise AttributeError( - "_get_open_order_backtest is deprecated and has been removed. " - "Use OrderPicker.get_order(request=OrderRequest(...)) instead." - ) + return paired diff --git a/EventDriven/riskmanager/picker/utils.py b/EventDriven/riskmanager/picker/utils.py index 4ee0f15..d33dd1a 100644 --- a/EventDriven/riskmanager/picker/utils.py +++ b/EventDriven/riskmanager/picker/utils.py @@ -49,7 +49,7 @@ def _delta_lmt(f: float) -> Union[float, np.float64]: return np.float64(f) -def _build_common_pair_mask( +def _build_common_pair_masks( spread_mid: pd.Series, spread_bid: pd.Series, spread_ask: pd.Series, @@ -61,12 +61,65 @@ def _build_common_pair_mask( max_pct_width: float, min_oi: int, delta_lmt: Union[float, np.float64], -) -> pd.Series: - """Build a shared mask used by pairers for spread contract filtering.""" +) -> pd.DataFrame: + """Build shared mask columns used by pairers for spread contract filtering.""" mid_mask = spread_mid.between(min_total_price, max_total_price) spread_oi_mask = spread_oi >= min_oi pct_width_mask = spread_pct_ratio <= max_pct_width spread_bid_mask = spread_bid > 0 spread_ask_mask = spread_ask > 0 delta_mask = spread_delta.abs() <= abs(delta_lmt) - return mid_mask & spread_oi_mask & pct_width_mask & spread_bid_mask & spread_ask_mask & delta_mask + mask_df = pd.DataFrame( + { + "mid_mask": mid_mask, + "spread_oi_mask": spread_oi_mask, + "pct_width_mask": pct_width_mask, + "spread_bid_mask": spread_bid_mask, + "spread_ask_mask": spread_ask_mask, + "delta_mask": delta_mask, + } + ) + mask_df["full_mask"] = mask_df.all(axis=1) + return mask_df + + +def _build_common_pair_mask( + spread_mid: pd.Series, + spread_bid: pd.Series, + spread_ask: pd.Series, + spread_pct_ratio: pd.Series, + spread_oi: pd.Series, + spread_delta: pd.Series, + min_total_price: float, + max_total_price: float, + max_pct_width: float, + min_oi: int, + delta_lmt: Union[float, np.float64], +) -> pd.Series: + """Build the combined pair-selection mask used by pairers for spread contract filtering.""" + return _build_common_pair_masks( + spread_mid=spread_mid, + spread_bid=spread_bid, + spread_ask=spread_ask, + spread_pct_ratio=spread_pct_ratio, + spread_oi=spread_oi, + spread_delta=spread_delta, + min_total_price=min_total_price, + max_total_price=max_total_price, + max_pct_width=max_pct_width, + min_oi=min_oi, + delta_lmt=delta_lmt, + )["full_mask"] + + +def _finalize_paired_output( + paired_opttick: pd.DataFrame, + mask_df: pd.DataFrame, + return_mask: bool = False, +) -> pd.DataFrame: + """Return filtered paired output, optionally including mask columns.""" + paired_with_masks = pd.concat((paired_opttick.reset_index(drop=True), mask_df.reset_index(drop=True)), axis=1) + filtered_output = paired_with_masks[paired_with_masks["full_mask"]].reset_index(drop=True) + if return_mask: + return filtered_output + return filtered_output[paired_opttick.columns.tolist()] diff --git a/EventDriven/riskmanager/picker/vertical_spread.py b/EventDriven/riskmanager/picker/vertical_spread.py index ea90c14..7906854 100644 --- a/EventDriven/riskmanager/picker/vertical_spread.py +++ b/EventDriven/riskmanager/picker/vertical_spread.py @@ -2,7 +2,12 @@ import pandas as pd from typing import Optional from EventDriven.riskmanager.picker import _order_formatting, create_trade_id -from EventDriven.riskmanager.picker.utils import _delta_lmt, _verify_delta_in_chain, _build_common_pair_mask +from EventDriven.riskmanager.picker.utils import ( + _delta_lmt, + _verify_delta_in_chain, + _build_common_pair_masks, + _finalize_paired_output, +) from EventDriven.types import ResultsEnum, OrderDict from EventDriven.riskmanager.picker import OrderSchema from trade.helpers.Logging import setup_logger @@ -18,6 +23,7 @@ def vertical_spread_pairer_by_exp( max_pct_width: float = np.inf, min_oi: int = 0, delta_lmt: Optional[float] = None, + return_mask: bool = False, ) -> pd.DataFrame: """ For a given row (option contract), find the corresponding leg of the vertical spread based on the spread_tick. @@ -79,7 +85,6 @@ def vertical_spread_pairer_by_exp( spread_moneyness, dte, spread_theta, - ), axis=1, ) @@ -101,7 +106,7 @@ def vertical_spread_pairer_by_exp( shrunk_delta_lmt = round(_delta_lmt(delta_lmt) * 0.95, 2) - full_mask = _build_common_pair_mask( + mask_df = _build_common_pair_masks( spread_mid=paired_opttick["spread_mid"], spread_bid=paired_opttick["spread_bid"], spread_ask=paired_opttick["spread_ask"], @@ -114,18 +119,12 @@ def vertical_spread_pairer_by_exp( min_oi=min_oi, delta_lmt=shrunk_delta_lmt, ) - - mid_mask = paired_opttick["spread_mid"].between(min_total_price, max_total_price) - spread_oi_mask = paired_opttick["spread_oi"] >= min_oi - pct_width_mask = paired_opttick["spread_pct_ratio"] <= max_pct_width - spread_bid_mask = paired_opttick["spread_bid"] > 0 - spread_ask_mask = paired_opttick["spread_ask"] > 0 - delta_mask = paired_opttick["spread_delta"].abs() <= abs(shrunk_delta_lmt) + full_mask = mask_df["full_mask"] logger.debug(f"Number of spreads after applying all filters: {full_mask.sum()}") ## DEBUG logger.debug( - f"mid_mask: {mid_mask.sum()} (between {min_total_price} and {max_total_price}), spread_oi_mask: {spread_oi_mask.sum()}, pct_width_mask: {pct_width_mask.sum()}, spread_bid_mask: {spread_bid_mask.sum()}, spread_ask_mask: {spread_ask_mask.sum()}, delta_mask: {delta_mask.sum()}" + f"mid_mask: {mask_df['mid_mask'].sum()} (between {min_total_price} and {max_total_price}), spread_oi_mask: {mask_df['spread_oi_mask'].sum()}, pct_width_mask: {mask_df['pct_width_mask'].sum()}, spread_bid_mask: {mask_df['spread_bid_mask'].sum()}, spread_ask_mask: {mask_df['spread_ask_mask'].sum()}, delta_mask: {mask_df['delta_mask'].sum()}" ) ## DEBUG - return paired_opttick[full_mask].reset_index(drop=True) + return _finalize_paired_output(paired_opttick=paired_opttick, mask_df=mask_df, return_mask=return_mask) def _vertical_spread_pairer( From caa00758735052481cdda92f6dd604e421a969b3 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:34 -0400 Subject: [PATCH 38/81] feat: support payload-driven option attribution loading --- EventDriven/attribution.py | 79 ++++++- module_test/test_xmultiply_dual_mode.py | 213 +++++++++++++++++ trade/assets/calculate/data_classes.py | 10 +- trade/assets/calculate/xmultiply_attr_v2.py | 239 ++++++++++++++------ 4 files changed, 458 insertions(+), 83 deletions(-) create mode 100644 module_test/test_xmultiply_dual_mode.py diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index 6926909..bf24a9d 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -27,9 +27,10 @@ >>> daily_attr = result.attribution """ -from trade.helpers.helper import change_to_last_busday + +from trade.helpers.helper import change_to_last_busday, to_datetime from pandas.tseries.offsets import BDay -from typing import Callable +from typing import Callable, Union import pandas as pd from dataclasses import dataclass from functools import partial @@ -37,13 +38,14 @@ from EventDriven.types import TradeID, SignalID from trade.helpers.helper_types import FrozenValidated from EventDriven.trade import Trade -from trade.assets.calculate.xmultiply_attr_v2 import load_option_pnl_data +from trade.assets.calculate.xmultiply_attr_v2 import load_option_pnl_data, OptionPnlPayload from trade.assets.calculate.xmultiply_attr import load_option_pnl_data as load_option_pnl_data_v1 from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.market_timeseries import BacktestTimeseries from EventDriven.new_portfolio import OptionSignalPortfolio from typing import Tuple, Dict from tqdm import tqdm +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE logger = setup_logger("EventDriven.attribution") @@ -112,8 +114,8 @@ class BacktestPositionAttribution(FrozenValidated): :func:`compute_position_attribution` for column definitions). """ - trade_id: TradeID - signal_id: SignalID + trade_id: Union[TradeID, str] + signal_id: Union[SignalID, str] qty: QuantityTimeSeries attribution: pd.DataFrame @@ -162,12 +164,41 @@ def _get_trade_quantity_time_series( ] individual_trades_df = individual_trades_df[cols] individual_trades_df.columns = new_col + + ## Aggregate trades table + def _aggregate_trade_group(group): + total_qty = group["qty_change"].sum() + + if total_qty == 0: + weighted_fill_price = 0 + else: + weighted_fill_price = (group["fill_price"] * group["qty_change"]).sum() / total_qty + + return pd.Series({ + "qty_change": total_qty, + "per_unit_slippage": group["per_unit_slippage"].sum(), + "per_unit_commission": group["per_unit_commission"].sum(), + "per_unit_market_value": group["per_unit_market_value"].sum(), + "direction": group["direction"].iloc[0], + "fill_price": weighted_fill_price, + }) + + individual_trades_df = ( + individual_trades_df + .groupby("fill_ts", group_keys=False) + .apply(_aggregate_trade_group) + .sort_index() + .reset_index() + ) + individual_trades_df["qty_change"] = individual_trades_df.apply( lambda row: row["qty_change"] if row["direction"] == "BUY" else -abs(row["qty_change"]), axis=1 ) trade_entry = individual_trades_df["fill_ts"].min() trade_exit = individual_trades_df["fill_ts"].max() + + ## Between entry and exit, extract daily quantity and quantiy change date_range = pd.date_range(start=trade_entry, end=trade_exit, freq="B") qty_frame = individual_trades_df.set_index("fill_ts").reindex(date_range).fillna(0) @@ -190,7 +221,11 @@ def _get_trade_quantity_time_series( def create_position_attribution( - trade_id: TradeID, entry_date: DATE_HINT, exit_date: DATE_HINT, v1: bool = False + trade_id: TradeID, + entry_date: DATE_HINT, + exit_date: DATE_HINT, + v1: bool = False, + portfolio: OptionSignalPortfolio = None, ) -> pd.DataFrame: """Create a position attribution DataFrame for a given trade ID. @@ -206,15 +241,35 @@ def create_position_attribution( Returns: A DataFrame containing the position attribution for the given trade ID. """ + def _get_payload(opttick: str) -> OptionPnlPayload: + """Helper function to load the option PnL payload with risk data for a given option tick.""" + if v1: + return None + else: + pay_load = OptionPnlPayload( + opttick=opttick, + date=to_datetime(entry_date), + ) + opt_data = portfolio.risk_manager.market_data.generate_option_data_for_trade(opttick=opttick, check_date=entry_date) + pay_load.vol = opt_data["vol"] + + greeks = opt_data[["Delta", "Gamma", "Vega", "Theta", "Rho", "Volga"]] + greeks.columns = ["delta", "gamma", "vega", "theta", "rho", "volga"] + option_spot = opt_data["Midpoint"] + pay_load.greeks = greeks + pay_load.spot = option_spot + return pay_load legs = trade_id.legs attribution_frames = [] - entry_padding = pd.to_datetime(entry_date) - pd.Timedelta(days=3) + entry_padding = max(pd.to_datetime(entry_date) - pd.Timedelta(days=3), to_datetime(OPTION_TIMESERIES_START_DATE)) exit_padding = pd.to_datetime(exit_date) + pd.Timedelta(days=3) for direction, opttick in legs: if v1: attribution = load_option_pnl_data_v1(yesterday=entry_padding, today=exit_padding, opttick=opttick) else: - attribution = load_option_pnl_data(yesterday=entry_padding, today=exit_padding, opttick=opttick) + payload = _get_payload(opttick) + payload.date = to_datetime(exit_padding) + attribution = load_option_pnl_data(yesterday=entry_padding, today=exit_padding, opttick=opttick, payload=payload) if direction == "S": attribution.attribution *= -1 attribution_frames.append(attribution.attribution) @@ -275,6 +330,8 @@ def compute_position_attribution( ## Extract series from qty_ts for easier access daily_qty = qty_ts.daily_qty quantity_change = qty_ts.quantity_change + + ## Exec price is per unit market value exec_price = qty_ts.exec_price attribution = attribution.copy() commission = qty_ts.commission @@ -282,9 +339,9 @@ def compute_position_attribution( ## Ensure attribution has necessary columns, if not create them with default values if "commission_cost" not in attribution.columns: - attribution["commission_cost"] = commission.fillna(0) + attribution["commission_cost"] = 0#commission.fillna(0) if "slippage_cost" not in attribution.columns: - attribution["slippage_cost"] = slippage.fillna(0) + attribution["slippage_cost"] = 0#slippage.fillna(0) if "trade_pnl_adjustment" not in attribution.columns: attribution["trade_pnl_adjustment"] = 0.0 if "total_pnl" not in attribution.columns: @@ -413,7 +470,7 @@ def compute_backtest_position_attribution( # Create initial attribution for the position) trade_entry = qty_ts.trade_entry trade_exit = qty_ts.trade_exit - attr = create_position_attribution(trade_id=trade_id, entry_date=trade_entry, exit_date=trade_exit, v1=False) + attr = create_position_attribution(trade_id=trade_id, entry_date=trade_entry, exit_date=trade_exit, v1=False, portfolio=portfolio) attr = attr.loc[trade_entry:trade_exit] # Make partial function for getting position price with market data from the portfolio's risk manager diff --git a/module_test/test_xmultiply_dual_mode.py b/module_test/test_xmultiply_dual_mode.py new file mode 100644 index 0000000..97b95b1 --- /dev/null +++ b/module_test/test_xmultiply_dual_mode.py @@ -0,0 +1,213 @@ +"""Run side-by-side evidence tests for xmultiply attribution payload paths. + +This script runs two test formats for the same option/date inputs: +1) Direct mode: call load_option_pnl_data with opttick only. +2) Payload mode: preload all required data via trade.datamanager managers, + build OptionPnlPayload, then pass payload into load_option_pnl_data. + +Usage: + /Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/bin/python \ + module_test/test_xmultiply_dual_mode.py \ + --opttick HD20250620C500 \ + --start 2024-12-03 \ + --end 2024-12-31 +""" + +from __future__ import annotations + +import argparse +import json +import traceback +from datetime import datetime +from typing import Any, Dict + +from pandas.tseries.offsets import BDay + +from trade.helpers.helper import parse_option_tick, change_to_last_busday +from trade.assets.calculate.data_classes import OptionPnlPayload, SymbolPayload +from trade.assets.calculate.xmultiply_attr_v2 import load_option_pnl_data +from trade.datamanager import ( + SpotDataManager, + RatesDataManager, + VolDataManager, + OptionSpotDataManager, + GreekDataManager, +) + + +def _summarize_output(mode: str, payload: OptionPnlPayload) -> Dict[str, Any]: + """Build a compact evidence payload from function output.""" + attribution = payload.attribution + dod_change = payload.dod_change + summary: Dict[str, Any] = { + "mode": mode, + "status": "SUCCESS", + "attribution_rows": 0 if attribution is None else len(attribution), + "dod_rows": 0 if dod_change is None else len(dod_change), + "attribution_cols": [], + "attribution_index_min": None, + "attribution_index_max": None, + "aggregates": {}, + } + + if attribution is not None and not attribution.empty: + summary["attribution_cols"] = list(attribution.columns) + summary["attribution_index_min"] = str(attribution.index.min()) + summary["attribution_index_max"] = str(attribution.index.max()) + + aggregate_columns = [ + "opt_dod_change", + "total_pnl_excl_trade_pnl", + "trade_pnl_adjustment", + "total_pnl", + "unexplained_pnl", + ] + aggregates = {} + for col in aggregate_columns: + if col in attribution.columns: + aggregates[col] = float(attribution[col].sum()) + summary["aggregates"] = aggregates + + return summary + + +def run_direct(opttick: str, start: datetime, end: datetime) -> Dict[str, Any]: + """Run direct mode (no prebuilt payload).""" + out = load_option_pnl_data(yesterday=start, today=end, opttick=opttick) + return _summarize_output(mode="direct", payload=out) + + +def run_payload(opttick: str, start: datetime, end: datetime) -> Dict[str, Any]: + """Run payload mode with all factors loaded from dedicated data managers.""" + meta = parse_option_tick(opttick) + query_start = change_to_last_busday(start - BDay(1)) + + symbol = meta["ticker"] + exp = meta["exp_date"] + strike = meta["strike"] + right = meta["put_call"] + + spot_mgr = SpotDataManager(symbol) + rates_mgr = RatesDataManager() + vol_mgr = VolDataManager(symbol) + opt_spot_mgr = OptionSpotDataManager(symbol) + greeks_mgr = GreekDataManager(symbol) + + asset_spot = spot_mgr.get_spot_timeseries( + start_date=query_start, + end_date=end, + undo_adjust=True, + ).timeseries + asset_spot.name = "spot" + + rates_spot = rates_mgr.get_risk_free_rate_timeseries( + start_date=query_start, + end_date=end, + ).timeseries + rates_spot.name = "rates" + + vol_data = vol_mgr.get_implied_volatility_timeseries( + start_date=query_start, + end_date=end, + expiration=exp, + strike=strike, + right=right, + ).timeseries + vol_data.name = "vol" + + option_spot_result = opt_spot_mgr.get_option_spot_timeseries( + start_date=query_start, + end_date=end, + expiration=exp, + strike=strike, + right=right, + ) + option_spot = option_spot_result.price + option_spot.name = "spot" + + greeks_data = greeks_mgr.get_greeks_timeseries( + start_date=query_start, + end_date=end, + expiration=exp, + strike=strike, + right=right, + ).timeseries + + preloaded_payload = OptionPnlPayload( + opttick=opttick, + date=end, + vol=vol_data, + spot=option_spot, + greeks=greeks_data, + asset_payload=SymbolPayload(symbol=symbol, datetime=end, spot=asset_spot), + rates_payload=SymbolPayload(symbol="RATES_USD", datetime=end, spot=rates_spot), + ) + + out = load_option_pnl_data( + yesterday=start, + today=end, + opttick=opttick, + payload=preloaded_payload, + ) + return _summarize_output(mode="payload_datamanagers", payload=out) + + +def _run_and_capture(label: str, fn, *args): + """Run a test mode and capture success/error in a JSON-serializable form.""" + print(f"\n===== {label} =====") + try: + result = fn(*args) + print(json.dumps(result, indent=2)) + return result + except Exception as exc: # pragma: no cover + error = { + "mode": label, + "status": "ERROR", + "error_type": type(exc).__name__, + "error_message": str(exc), + "traceback": traceback.format_exc(), + } + print(json.dumps(error, indent=2)) + return error + + +def main() -> None: + parser = argparse.ArgumentParser(description="Dual-mode evidence test for load_option_pnl_data") + parser.add_argument("--opttick", required=True, help="Option ticker, e.g. HD20250620C500") + parser.add_argument("--start", required=True, help="Start date YYYY-MM-DD") + parser.add_argument("--end", required=True, help="End date YYYY-MM-DD") + args = parser.parse_args() + + start = datetime.strptime(args.start, "%Y-%m-%d") + end = datetime.strptime(args.end, "%Y-%m-%d") + + print("Running dual-mode evidence test") + print(f"opttick={args.opttick}, start={start.date()}, end={end.date()}") + + direct_result = _run_and_capture("direct", run_direct, args.opttick, start, end) + payload_result = _run_and_capture("payload_datamanagers", run_payload, args.opttick, start, end) + + print("\n===== summary =====") + summary = { + "direct_status": direct_result.get("status"), + "payload_status": payload_result.get("status"), + "direct_rows": direct_result.get("attribution_rows"), + "payload_rows": payload_result.get("attribution_rows"), + "direct_aggregates": direct_result.get("aggregates"), + "payload_aggregates": payload_result.get("aggregates"), + "aggregate_differences": {}, + } + + direct_aggregates = summary.get("direct_aggregates") or {} + payload_aggregates = summary.get("payload_aggregates") or {} + all_keys = sorted(set(direct_aggregates.keys()) | set(payload_aggregates.keys())) + for key in all_keys: + direct_value = direct_aggregates.get(key, 0.0) + payload_value = payload_aggregates.get(key, 0.0) + summary["aggregate_differences"][key] = float(direct_value - payload_value) + + print(json.dumps(summary, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/trade/assets/calculate/data_classes.py b/trade/assets/calculate/data_classes.py index e576c45..b129527 100644 --- a/trade/assets/calculate/data_classes.py +++ b/trade/assets/calculate/data_classes.py @@ -83,11 +83,11 @@ class OptionPnlPayload: """ opttick: str date: datetime - vol: pd.DataFrame | pd.Series - spot: pd.DataFrame | pd.Series - greeks: pd.DataFrame - asset_payload: SymbolPayload - rates_payload: SymbolPayload + vol: Optional[pd.DataFrame | pd.Series] = None + spot: Optional[pd.DataFrame | pd.Series] = None + greeks: Optional[pd.DataFrame] = None + asset_payload: Optional[SymbolPayload] = None + rates_payload: Optional[SymbolPayload] = None attribution_model: AttributionModel = AttributionModel.UNDEFINED dod_change: Optional[pd.DataFrame] = Field(default_factory=pd.DataFrame) attribution: Optional[pd.DataFrame] = Field(default_factory=pd.DataFrame) diff --git a/trade/assets/calculate/xmultiply_attr_v2.py b/trade/assets/calculate/xmultiply_attr_v2.py index 413ff18..05e5471 100644 --- a/trade/assets/calculate/xmultiply_attr_v2.py +++ b/trade/assets/calculate/xmultiply_attr_v2.py @@ -2,7 +2,7 @@ from typing import Optional, List from pandas.tseries.offsets import BDay import pandas as pd -from pydantic import validate_call, ConfigDict +from pydantic import validate_call, ConfigDict # noqa from trade.assets.calculate.data_classes import SymbolPayload, OptionPnlPayload, TradePnlInfo, SYMBOL_PAYLOADS from trade.assets.calculate.enums import AttributionModel from trade.assets.calculate.adjustments import trade_pnl_adjustment @@ -10,17 +10,69 @@ parse_option_tick, retrieve_timeseries, # noqa change_to_last_busday, + get_missing_dates, + to_datetime, ) from trade.helpers.Logging import setup_logger -from trade.helpers.decorators import log_time +from trade.helpers.decorators import log_time # noqa from module_test.raw_code.DataManagers.DataManagers import OptionDataManager, set_skip_mysql_query from trade.datamanager.timeseries import TimeseriesDataManager # noqa +from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE ##TODO: Take this out once DataManagers has been optimized set_skip_mysql_query(True) logger = setup_logger("trade.assets.calculate.xmultiply_attr") +def _is_missing_data(data: Optional[pd.Series | pd.DataFrame]) -> bool: + """Return True if a pandas object is missing or empty.""" + return data is None or data.empty + + +def _validate_timeseries_dates( + field_name: str, + data: pd.Series | pd.DataFrame, + start_date: datetime, + end_date: datetime, +) -> None: + """Validate index dtype and business-date coverage for provided timeseries.""" + if not isinstance(data.index, pd.DatetimeIndex): + raise ValueError(f"Provided '{field_name}' must have a DatetimeIndex.") + if data.empty: + raise ValueError(f"Provided '{field_name}' is empty.") + + missing_dates = get_missing_dates(data, _start=start_date, _end=end_date) + if missing_dates: + missing_str = [d.strftime("%Y-%m-%d") for d in missing_dates] + logger.warning(f"Provided '{field_name}' is missing expected business dates: {missing_str}") + + +def _validate_expected_greeks_columns(greeks_data: pd.DataFrame) -> None: + """Validate that greeks include columns required by calculate_pnl_decomposition.""" + required_greeks_cols = {"delta", "gamma", "vega", "theta", "rho", "volga"} + missing_cols = required_greeks_cols - set(greeks_data.columns) + if missing_cols: + raise ValueError(f"Provided 'greeks' is missing required columns: {sorted(missing_cols)}") + + +def _validate_symbol_payload( + field_name: str, + symbol_payload: SymbolPayload, + expected_symbol: str, + start_date: datetime, + end_date: datetime, +) -> None: + """Validate symbol payload metadata and date coverage.""" + if symbol_payload.symbol != expected_symbol: + raise ValueError(f"Provided '{field_name}.symbol' must be '{expected_symbol}', got '{symbol_payload.symbol}'.") + _validate_timeseries_dates( + field_name=f"{field_name}.spot", + data=symbol_payload.spot, + start_date=start_date, + end_date=end_date, + ) + + def get_symbol_timeseries(symbol) -> TimeseriesDataManager: """ Get a timeseries data manager for a given symbol. @@ -32,8 +84,8 @@ def get_symbol_timeseries(symbol) -> TimeseriesDataManager: return TimeseriesDataManager(symbol=symbol) -@log_time() -@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +# @log_time() +# @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def load_symbol_payload(symbol: str, today: datetime, yesterday: datetime) -> SymbolPayload: """ Load symbol payload data for a given symbol between yesterday @@ -45,15 +97,14 @@ def load_symbol_payload(symbol: str, today: datetime, yesterday: datetime) -> Sy Returns: SymbolPayload: The loaded symbol data. """ - - spot_series = get_symbol_timeseries(symbol).spot.get_timeseries(start_date=yesterday, end_date=today).timeseries + spot_series = get_symbol_timeseries(symbol).spot.get_timeseries(start_date=yesterday, end_date=today, undo_adjust=False).timeseries spot_series.name = "spot" return SymbolPayload(symbol=symbol, datetime=today, spot=spot_series) -@log_time() -@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +# @log_time() +# @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def load_rate_payload(today: datetime, yesterday: datetime) -> SymbolPayload: """ Load rate payload data for USD rates between yesterday @@ -73,8 +124,8 @@ def load_rate_payload(today: datetime, yesterday: datetime) -> SymbolPayload: return SymbolPayload(symbol="RATES_USD", datetime=today, spot=rates_series) -@log_time() -@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +# @log_time() +# @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def add_dod_change(payload: OptionPnlPayload, yesterday: datetime, today: datetime) -> OptionPnlPayload: """ Add day-over-day change data to the option payload. @@ -115,79 +166,135 @@ def add_dod_change(payload: OptionPnlPayload, yesterday: datetime, today: dateti return payload -@log_time() -@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +# @log_time() +# @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def load_option_pnl_data( - yesterday: datetime, today: datetime, *, dm: OptionDataManager = None, opttick: str = None + yesterday: datetime, + today: datetime, + *, + dm: OptionDataManager = None, + opttick: str = None, + payload: Optional[OptionPnlPayload] = None, ) -> OptionPnlPayload: """ Load option data for a given option data manager between yesterday and today. Args: dm (OptionDataManager): The option data manager to load data from. + opttick (str): The option ticker symbol. + payload (Optional[OptionPnlPayload]): Optional payload containing any subset + of expected fields. Provided fields are validated; missing fields are loaded. yesterday (datetime): The start date for the data. today (datetime): The end date for the data. Returns: OptionPnlPayload: The loaded option data. """ - if dm is None and opttick is None: - raise ValueError("Either 'dm' or 'opttick' must be provided.") + if dm is None and opttick is None and payload is None: + raise ValueError("One of 'opttick' or 'payload' must be provided.") if dm is not None: raise ValueError("Data manager input is currently not supported. Please provide 'opttick' directly.") - option_meta = parse_option_tick(opttick) + + payload_opttick = payload.opttick if payload is not None else None + effective_opttick = opttick or payload_opttick + if effective_opttick is None: + raise ValueError("Could not resolve option ticker. Provide 'opttick' or payload.opttick.") + if opttick is not None and payload_opttick is not None and opttick != payload_opttick: + raise ValueError( + "Provided 'opttick' does not match 'payload.opttick'. " + f"Got opttick='{opttick}', payload.opttick='{payload_opttick}'." + ) + + if payload is not None and payload.date != today: + raise ValueError(f"Provided 'payload.date' must equal 'today'. Got payload.date={payload.date}, today={today}.") + + option_meta = parse_option_tick(effective_opttick) ## Back up yesterday by 1BDAY to ensure inclusive data retrieval - yesterday = change_to_last_busday(yesterday - BDay(1)) + yesterday = max(change_to_last_busday(yesterday - BDay(1)), to_datetime(OPTION_TIMESERIES_START_DATE)) ts = get_symbol_timeseries(option_meta["ticker"]) - ## Query Vol Data - vol_req = ts.vol.get_timeseries( - start_date=yesterday, - end_date=today, - expiration=option_meta["exp_date"], - strike=option_meta["strike"], - right=option_meta["put_call"], - ) - vol_data = vol_req.timeseries - vol_data.name = "vol" - - ## Query Option Spot Data - spot_req = ts.option_spot.get_timeseries( - start_date=yesterday, - end_date=today, - expiration=option_meta["exp_date"], - strike=option_meta["strike"], - right=option_meta["put_call"], - ) - spot_data = spot_req.price - spot_data.name = "spot" - - ## Query Greeks Data - greeks_req = ts.greeks.get_timeseries( - start_date=yesterday, - end_date=today, - expiration=option_meta["exp_date"], - strike=option_meta["strike"], - right=option_meta["put_call"], - ) - greeks_data = greeks_req.timeseries - - ## Load Symbol Payload - sym_payload = SYMBOL_PAYLOADS.get((option_meta["ticker"], yesterday, today)) - if sym_payload is None: - sym_payload = load_symbol_payload(symbol=option_meta["ticker"], today=today, yesterday=yesterday) - SYMBOL_PAYLOADS[(option_meta["ticker"], yesterday, today)] = sym_payload - - ## Load Rates Payload - rates_payload = SYMBOL_PAYLOADS.get(("RATES_USD", yesterday, today)) - if rates_payload is None: - rates_payload = load_rate_payload( - today=today, - yesterday=yesterday, + provided_vol = payload.vol if payload is not None else None + if _is_missing_data(provided_vol): + vol_req = ts.vol.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], ) - SYMBOL_PAYLOADS[("RATES_USD", yesterday, today)] = rates_payload + vol_data = vol_req.timeseries + vol_data.name = "vol" + else: + _validate_timeseries_dates("vol", provided_vol, yesterday, today) + vol_data = provided_vol + + provided_spot = payload.spot if payload is not None else None + if _is_missing_data(provided_spot): + spot_req = ts.option_spot.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + ) + spot_data = spot_req.price + spot_data.name = "spot" + else: + _validate_timeseries_dates("spot", provided_spot, yesterday, today) + spot_data = provided_spot + + provided_greeks = payload.greeks if payload is not None else None + if _is_missing_data(provided_greeks): + greeks_req = ts.greeks.get_timeseries( + start_date=yesterday, + end_date=today, + expiration=option_meta["exp_date"], + strike=option_meta["strike"], + right=option_meta["put_call"], + ) + greeks_data = greeks_req.timeseries + _validate_expected_greeks_columns(greeks_data) + else: + _validate_timeseries_dates("greeks", provided_greeks, yesterday, today) + _validate_expected_greeks_columns(provided_greeks) + greeks_data = provided_greeks + + provided_asset_payload = payload.asset_payload if payload is not None else None + if provided_asset_payload is None or _is_missing_data(provided_asset_payload.spot): + sym_payload = SYMBOL_PAYLOADS.get((option_meta["ticker"], yesterday, today)) + if sym_payload is None: + sym_payload = load_symbol_payload(symbol=option_meta["ticker"], today=today, yesterday=yesterday) + SYMBOL_PAYLOADS[(option_meta["ticker"], yesterday, today)] = sym_payload + else: + _validate_symbol_payload( + field_name="asset_payload", + symbol_payload=provided_asset_payload, + expected_symbol=option_meta["ticker"], + start_date=yesterday, + end_date=today, + ) + sym_payload = provided_asset_payload + + provided_rates_payload = payload.rates_payload if payload is not None else None + if provided_rates_payload is None or _is_missing_data(provided_rates_payload.spot): + rates_payload = SYMBOL_PAYLOADS.get(("RATES_USD", yesterday, today)) + if rates_payload is None: + rates_payload = load_rate_payload( + today=today, + yesterday=yesterday, + ) + SYMBOL_PAYLOADS[("RATES_USD", yesterday, today)] = rates_payload + else: + _validate_symbol_payload( + field_name="rates_payload", + symbol_payload=provided_rates_payload, + expected_symbol="RATES_USD", + start_date=yesterday, + end_date=today, + ) + rates_payload = provided_rates_payload payload = OptionPnlPayload( - opttick=opttick, + opttick=effective_opttick, date=today, vol=vol_data, spot=spot_data, @@ -201,8 +308,8 @@ def load_option_pnl_data( return calculate_pnl_decomposition(payload) -@log_time() -@validate_call(config=ConfigDict(arbitrary_types_allowed=True)) +# @log_time() +# @validate_call(config=ConfigDict(arbitrary_types_allowed=True)) def calculate_pnl_decomposition( payload: OptionPnlPayload, trade_pnl_entries: Optional[List[TradePnlInfo]] = None ) -> OptionPnlPayload: @@ -251,8 +358,6 @@ def calculate_pnl_decomposition( rho_pnl = (dod_change["rates_change"] * greeks["rho"] * 100).dropna() rho_pnl.name = "rho_pnl" - - opt_change = dod_change["opt_change"].dropna() opt_change.name = "opt_dod_change" From f5bc40e1cdbccb9aacc123c6794baacc872ecb24 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:38 -0400 Subject: [PATCH 39/81] refactor: tighten cache handling and date synchronization --- EventDriven/_vars.py | 31 +- EventDriven/riskmanager/utils.py | 57 ++-- module_test/test_position_cache.py | 118 ++++++++ trade/datamanager/base.py | 16 +- trade/datamanager/dividend.py | 8 +- trade/datamanager/utils/date.py | 9 +- trade/helpers/helper.py | 440 +++++++++++++++++++++-------- 7 files changed, 517 insertions(+), 162 deletions(-) create mode 100644 module_test/test_position_cache.py diff --git a/EventDriven/_vars.py b/EventDriven/_vars.py index af62f52..79c55dd 100644 --- a/EventDriven/_vars.py +++ b/EventDriven/_vars.py @@ -77,25 +77,26 @@ def load_riskmanager_cache(target: str = None, """ Load the risk manager cache based on the USE_TEMP_CACHE setting. """ + size_limit = kwargs.get("size_limit", 2**31) # Default to 2gb if not provided 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", clear_on_exit=True) - 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) - adjusted_strike_cache = CustomCache(BASE/"temp", fname = "rm_adjusted_strike_cache", expire_days=100) + spot_timeseries = CustomCache(BASE/"temp", fname = "rm_spot_timeseries", expire_days=100, size_limit=size_limit) + chain_spot_timeseries = CustomCache(BASE/"temp", fname = "rm_chain_spot_timeseries", expire_days=100, size_limit=size_limit) ## This is used for pricing, to account option strikes for splits + processed_option_data = CustomCache(BASE/"temp", fname = "rm_processed_option_data", clear_on_exit=True, size_limit=size_limit) + position_data = CustomCache(BASE/"temp", fname = "rm_position_data", clear_on_exit=True, size_limit=size_limit) + dividend_timeseries = CustomCache(BASE/"temp", fname = "rm_dividend_timeseries", expire_days=100, size_limit=size_limit) + adjusted_strike_cache = CustomCache(BASE/"temp", fname = "rm_adjusted_strike_cache", expire_days=100, size_limit=size_limit) 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) - adjusted_strike_cache = CustomCache(BASE, fname = "rm_adjusted_strike_cache", expire_days=100) + spot_timeseries = CustomCache(BASE, fname = "rm_spot_timeseries", expire_days=100, size_limit=size_limit) + chain_spot_timeseries = CustomCache(BASE, fname = "rm_chain_spot_timeseries", expire_days=100, size_limit=size_limit) ## This is used for pricing, to account option strikes for splits + processed_option_data = CustomCache(BASE, fname = "rm_processed_option_data", expire_days=100, size_limit=size_limit) + position_data = CustomCache(BASE, fname = "rm_position_data", clear_on_exit=True, size_limit=size_limit) + dividend_timeseries = CustomCache(BASE, fname = "rm_dividend_timeseries", expire_days=100, size_limit=size_limit) + adjusted_strike_cache = CustomCache(BASE, fname = "rm_adjusted_strike_cache", expire_days=100, size_limit=size_limit) ## 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 + splits_raw =CustomCache(HOME_BASE, fname = "split_names_dates", expire_days = 1000, size_limit=size_limit) ## Cache for raw splits data to handle splits adjustments, separate from dividend_timeseries which is used for dividend adjustments. This allows us to keep a long history of splits data without bloating the dividend cache which is more frequently accessed and updated. + special_dividend = CustomCache(HOME_BASE, fname = 'special_dividend', expire_days=1000, size_limit=size_limit) ## Special dividend cache for handling special dividends special_dividend['COST'] = { '2020-12-01': 10, '2023-12-27': 15 @@ -124,7 +125,7 @@ def load_riskmanager_cache(target: str = None, if create_on_missing: logger.warning(f"Creating new cache for unknown target: {target}") clear_on_exit = kwargs.get('clear_on_exit', True) - return CustomCache(BASE, fname = f"extra_{target}", clear_on_exit=clear_on_exit) + return CustomCache(BASE, fname = f"extra_{target}", clear_on_exit=clear_on_exit, size_limit=size_limit) raise ValueError(f"Unknown target: {target}") return (spot_timeseries, diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index e8cdbb6..9ae51e8 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -225,7 +225,7 @@ ## Caches _TEMP_CACHE = CustomCache(location / "temp", fname="temp_cache", clear_on_exit=True) -_PERSISTENT_CACHE = CustomCache(location, fname="persistent_cache", expire_days=30) +_PERSISTENT_CACHE = CustomCache(location, fname="persistent_cache", expire_days=30, size_limit=10e9) ## 10 GB size limit for persistent cache spot_cache = CustomCache(BASE, fname="spot", expire_days=45) ## Flags @@ -437,9 +437,15 @@ def populate_cache_with_chain( key = (tick, pd.to_datetime(date, format="%Y-%m-%d").strftime("%Y-%m-%d")) if key in get_persistent_cache(): chain_clipped = get_persistent_cache()[key] + chain_clipped.columns = chain_clipped.columns.str.capitalize() + chain_clipped.rename(columns={"Opttick": "opttick"}, inplace=True) + drops = ["Datetime", "Dte", "Moneyness"] + for col in drops: + if col in chain_clipped.columns: + chain_clipped.drop(columns=col, inplace=True) else: - chain = retrieve_chain_bulk(tick, "", date, date, "16:00", "C", print_url=False) + chain = retrieve_chain_bulk(symbol=tick, start_date=date, end_date=date, end_time="16:00", option_type="C", print_url=False, exp = None) logger.info(f"Retrieved chain for {tick} on {date}") ## Retrieve OI @@ -449,38 +455,50 @@ def populate_cache_with_chain( prev = change_to_last_busday((pd.to_datetime(date) - BDay(1))).strftime("%Y-%m-%d") oi = retrieve_bulk_open_interest(symbol=tick, exp=0, start_date=prev, end_date=prev, print_url=False) + + ## Clip Chain - chain_clipped = chain.reset_index() # [['datetime', 'Root', 'Strike', 'Right', 'Expiration', 'Midpoint']] + chain_clipped = ( + chain.reset_index() + ) + chain_clipped = chain_clipped.merge( oi[["Root", "Expiration", "Strike", "Right", "Open_interest"]], on=["Root", "Expiration", "Strike", "Right"], how="left", ) + if PATCH_TICKERS: chain_clipped["Root"] = chain_clipped["Root"].apply(swap_ticker) - - ## Create ID - id_params = chain_clipped[["Root", "Right", "Expiration", "Strike"]].T.to_numpy() - ids = runThreads(generate_option_tick_new, id_params) - chain_clipped["opttick"] = ids + _PERSISTENT_CACHE[key] = chain_clipped ## Cache the chain data to avoid redundant API calls in the future + chain_clipped.columns = chain_clipped.columns.str.capitalize() + + ## Create ID + id_params = chain_clipped[ + ["Root", "Right", "Expiration", "Strike"] + ].T.to_numpy() + ids = runThreads(generate_option_tick_new, id_params) + chain_clipped["opttick"] = ids + filter_opt = get_avoid_opticks(tick) + chain_clipped["datetime"] = pd.to_datetime(date) chain_clipped = chain_clipped[~chain_clipped["opttick"].isin(filter_opt)] ## Optticks to avoid chain_clipped["chain_id"] = chain_clipped["opttick"] + "_" + chain_clipped["datetime"].astype(str) chain_clipped["dte"] = ( pd.to_datetime(chain_clipped["Expiration"]) - pd.to_datetime(chain_clipped["datetime"]) ).dt.days - ## Save to cache - def save_to_cache(id, date, spot): - date = pd.to_datetime(date).strftime("%Y-%m-%d") - save_id = f"{id}_{date}" - if save_id not in get_cache("spot"): - spot_cache[save_id] = spot + # ## Save to cache + # def save_to_cache(id, date, spot): + # date = pd.to_datetime(date).strftime("%Y-%m-%d") + # save_id = f"{id}_{date}" + # if save_id not in get_cache("spot"): + # spot_cache[save_id] = spot - save_params = chain_clipped[["opttick", "datetime", "Midpoint"]].T.to_numpy() - runThreads(save_to_cache, save_params) + # save_params = chain_clipped[["opttick", "datetime", "Midpoint"]].T.to_numpy() + # runThreads(save_to_cache, save_params) if chain_spot: chain_clipped["spot"] = chain_spot @@ -501,6 +519,7 @@ def save_to_cache(id, date, spot): chain_clipped[["iv", "delta", "gamma", "vega", "theta", "rho", "volga"]] = ( np.nan ) # Placeholder for Greeks, to be filled in later when we have the data + return chain_clipped @@ -545,16 +564,18 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat s = new_data.spot.timeseries y = new_data.dividend.timeseries r = new_data.rates.timeseries - greeks, option_spot, s, y, r = sync_date_index(greeks, option_spot, s, y, r) + vol = new_data.vol.timeseries + greeks, option_spot, s, y, r, vol = sync_date_index(greeks, option_spot, s, y, r, vol) ## set names properly start_time = time.time() s.name = "s" y.name = "y" r.name = "r" + vol.name = "vol" data = greeks.join(option_spot[["midpoint", "closeask", "closebid"]]) data.columns = data.columns.str.capitalize() - data = data.join(s).join(y).join(r) + data = data.join(s).join(y).join(r).join(vol) logger.info(f"Data processing for {opttick} took {time.time() - start_time:.2f} seconds") processed_option_data[opttick] = data return data diff --git a/module_test/test_position_cache.py b/module_test/test_position_cache.py new file mode 100644 index 0000000..aafd5ae --- /dev/null +++ b/module_test/test_position_cache.py @@ -0,0 +1,118 @@ +"""Test position data caching in BacktestTimeseries. + +Validates that: +1. Position data is correctly cached after first calculation +2. Subsequent calls return cached data (early return) +3. Skip columns adjustment is only applied once +4. Cache retrieval methods work correctly + +Usage: + /Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/bin/python \ + module_test/test_position_cache.py +""" + +from __future__ import annotations + +import time +from datetime import datetime + +from EventDriven.riskmanager.market_timeseries import BacktestTimeseries + + +def test_position_caching(): + """Test that position data caching works correctly.""" + + print("=" * 70) + print("Position Data Caching Test") + print("=" * 70) + + # Setup + bt = BacktestTimeseries(_start="2024-12-03", _end="2024-12-31") + pos_id = "&L:HD20250620C500" + test_date = "2024-12-10" + + # Test 1: Initial state + print("\n[Test 1] Initial State") + print(f" Position '{pos_id}' in cache: {pos_id in bt.position_data_cache}") + assert pos_id not in bt.position_data_cache, "Cache should be empty initially" + print(" ✓ Cache is empty as expected") + + # Test 2: First calculation (should calculate and cache) + print("\n[Test 2] First calculate_option_data call (will calculate greeks)") + print(" Note: This will take ~5 minutes for greek calculation...") + start_time = time.time() + result1 = bt.calculate_option_data(position_id=pos_id, date=test_date) + calc_time = time.time() - start_time + print(f" Calculation time: {calc_time:.2f} seconds") + print(f" Position now in cache: {pos_id in bt.position_data_cache}") + print(f" Result shape: {result1.shape}") + print(f" Result columns: {len(result1.columns)}") + assert pos_id in bt.position_data_cache, "Position should be cached after calculation" + assert not result1.empty, "Result should not be empty" + print(" ✓ Data calculated and cached successfully") + + # Test 3: Second calculation (should use cache - early return) + print("\n[Test 3] Second calculate_option_data call (should return cached data)") + start_time = time.time() + result2 = bt.calculate_option_data(position_id=pos_id, date=test_date) + cache_time = time.time() - start_time + print(f" Cache retrieval time: {cache_time:.2f} seconds") + print(f" Result shape: {result2.shape}") + assert cache_time < 1.0, f"Cache retrieval should be instant, took {cache_time:.2f}s" + assert result1.shape == result2.shape, "Cached result should match original" + print(f" ✓ Cache hit! Retrieved in {cache_time:.4f}s (vs {calc_time:.2f}s for calculation)") + print(f" ✓ Speedup: {calc_time / cache_time:.0f}x faster") + + # Test 4: get_position_data retrieval + print("\n[Test 4] get_position_data retrieval") + cached_df = bt.get_position_data(pos_id) + print(f" Retrieved shape: {cached_df.shape}") + print(f" Is empty: {cached_df.empty}") + assert not cached_df.empty, "Retrieved data should not be empty" + assert cached_df.shape == result1.shape, "Retrieved data should match cached data" + print(" ✓ Direct cache retrieval successful") + + # Test 5: get_at_time_position_data + print("\n[Test 5] get_at_time_position_data at specific date") + at_time = bt.get_at_time_position_data(pos_id, test_date) + print(f" Result is None: {at_time is None}") + if at_time: + print(f" Position ID: {at_time.position_id}") + print(f" Date: {at_time.date}") + print(f" Midpoint: {at_time.midpoint:.4f}") + print(f" Delta: {at_time.delta:.4f}") + assert at_time.position_id == pos_id + print(" ✓ Point-in-time retrieval successful") + else: + print(" ✗ FAILED: at_time is None (should have data)") + + # Test 6: Verify data integrity + print("\n[Test 6] Data Integrity Check") + print(f" Index type: {type(cached_df.index).__name__}") + print(f" Index range: {cached_df.index.min()} to {cached_df.index.max()}") + print(f" Number of rows: {len(cached_df)}") + print(f" Has skip columns: {'Midpoint_skip_day' in cached_df.columns}") + assert "Midpoint" in cached_df.columns, "Missing Midpoint column" + assert "Delta" in cached_df.columns, "Missing Delta column" + print(" ✓ Data structure is valid") + + # Summary + print("\n" + "=" * 70) + print("SUMMARY: All caching tests passed!") + print("=" * 70) + print(f" • Position data cached correctly") + print(f" • Cache hit provides {calc_time / cache_time:.0f}x speedup") + print(f" • Skip columns applied once (not re-applied on cache hit)") + print(f" • All retrieval methods working") + print() + + +if __name__ == "__main__": + try: + test_position_caching() + except AssertionError as e: + print(f"\n✗ TEST FAILED: {e}") + raise + except Exception as e: + print(f"\n✗ TEST ERROR: {type(e).__name__}: {e}") + raise diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py index 97d3a74..5db78f6 100644 --- a/trade/datamanager/base.py +++ b/trade/datamanager/base.py @@ -78,12 +78,12 @@ def __init_subclass__(cls, **kwargs: Any) -> None: # 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__}." - # ) + 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 @@ -139,9 +139,13 @@ def clear_all_caches(cls) -> None: """Clears caches for all registered DataManager subclasses.""" from .market_data import MarketTimeseries from .market_data_helpers.dividends import DIVIDEND_CACHE + from .utils.date import LIST_DATE_CACHE + from .dividend import DIV_TEMP_CACHE MarketTimeseries.clear_caches() DIVIDEND_CACHE.clear() + LIST_DATE_CACHE.clear() + DIV_TEMP_CACHE.clear() 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() diff --git a/trade/datamanager/dividend.py b/trade/datamanager/dividend.py index b0a1c8b..cbef24f 100644 --- a/trade/datamanager/dividend.py +++ b/trade/datamanager/dividend.py @@ -40,6 +40,10 @@ logger = setup_logger("trade.datamanager.dividend", stream_log_level=get_logging_level()) TS = get_times_series() +DIV_TEMP_CACHE = CustomCache( + location=DM_GEN_PATH.as_posix(), fname="dividend_temp_cache", expire_days=1, clear_on_exit=True + ) + class DividendDataManager(BaseDataManager): """Manages dividend data retrieval, caching, and schedule construction for a specific symbol. @@ -127,9 +131,7 @@ def __init__( 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 - ) + self.temp_cache: CustomCache = DIV_TEMP_CACHE ## General caching logic def cache_it(self, key: str, value: Any, *, expire: Optional[int] = None, _type: str = "discrete") -> None: diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index f1eea13..5a8de7b 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -183,6 +183,8 @@ def _get_max_date(requested_end: datetime) -> datetime: is_market_hrs = is_market_hours_today() today = ny_now() + timestamp_today = pd.Timestamp(today.date()) + timestamp_exp = pd.Timestamp(to_datetime(expiration).date()) prev_busday = (today - BDay(1)).to_pydatetime() start_date = to_datetime(start_date) end_date = to_datetime(end_date) @@ -198,12 +200,12 @@ def _get_max_date(requested_end: datetime) -> datetime: return min(start_date, end_date), max(start_date, end_date) logger.info(f"Fetching date range from Thetadata for {opttick}") - dates = list_dates( + dates = list(list_dates( symbol=symbol, exp=expiration, right=right, strike=strike, - ) + )) if not dates: raise ValueError(f"No trading dates found for {opttick}") @@ -215,8 +217,9 @@ def _get_max_date(requested_end: datetime) -> datetime: max_date = max(dates) start_date = max(min_date, start_date) end_date = min(_get_max_date(end_date), max_date) + max_allowable = min(timestamp_today, timestamp_exp) if endpoint_source == OptionSpotEndpointSource.EOD else timestamp_exp - LIST_DATE_CACHE.set(key=opttick, value={"min_date": min_date, "max_date": end_date}, expire=None) + LIST_DATE_CACHE.set(key=opttick, value={"min_date": pd.Timestamp(min_date), "max_date": pd.Timestamp(max_allowable)}, expire=None) return to_datetime(min(start_date, end_date)), to_datetime(max(start_date, end_date)) diff --git a/trade/helpers/helper.py b/trade/helpers/helper.py index 1111c8d..21e1393 100644 --- a/trade/helpers/helper.py +++ b/trade/helpers/helper.py @@ -15,6 +15,7 @@ from typing import Any, Dict import pstats import warnings +import pytz from pandas.tseries.offsets import BDay from typing import Union from trade.helpers.Configuration import ConfigProxy @@ -224,21 +225,43 @@ def __init__( # 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._register_location = f'{os.environ["WORK_DIR"]}/trade/helpers/clear_dirs.json' + 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") + 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" + ) self.__log_path = log_path os.makedirs(dir, exist_ok=True) @@ -277,7 +300,9 @@ def __getstate__(self): fname=self.fname, 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, + expire_days=( + pd.to_datetime(self.expiry_date).date() - datetime.today().date() + ).days, size_limit=self.size_limit, cull_limit=self.cull_limit, data=dict(self.items()), @@ -413,11 +438,15 @@ def filter_keys(self, x): def __repr__(self): sample_keys = list(self)[:10] - return f"" + return ( + f"" + ) def __str__(self): sample = dict(list(self.items())[:10]) - return f"" + return ( + f"" + ) def setdefault(self, key, default): if key not in self: @@ -466,7 +495,9 @@ def str_to_bool(value: str) -> bool: elif value.lower() in ["false", "0", "no"]: return False else: - raise ValueError("Invalid boolean string. Expected 'True', 'False', '1', '0', 'yes', or 'no'.") + raise ValueError( + "Invalid boolean string. Expected 'True', 'False', '1', '0', 'yes', or 'no'." + ) def check_all_days_available(x, _start, _end): @@ -482,7 +513,9 @@ def check_all_days_available(x, _start, _end): """ 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_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) @@ -498,18 +531,28 @@ def check_missing_dates(x, _start, _end): list: List of missing business days in the range. """ if "Datetime" not in x.columns: - logger.warning(f"DataFrame does not contain 'Datetime' column. Will default to index") + 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") 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] + 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") return missing_dates_fourth_check -def get_missing_dates(x: pd.Series | pd.DataFrame, _start: datetime, _end: datetime) -> List[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 @@ -520,7 +563,9 @@ def get_missing_dates(x: pd.Series | pd.DataFrame, _start: datetime, _end: datet Returns: list: List of missing business days in the range. """ - assert isinstance(x.index, pd.DatetimeIndex), "DataFrame index must be a DatetimeIndex" + assert isinstance(x.index, pd.DatetimeIndex), ( + "DataFrame index must be a DatetimeIndex" + ) date_range = bus_range(_start, _end, freq="1B") dates_available = x.index @@ -572,14 +617,24 @@ 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(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(flag, str), f"Recieved '{type(flag)}' for flag. Expected 'str'" if sigma <= 0: @@ -599,7 +654,14 @@ def vol_backout_errors(sigma, K, S0, T, r, q, market_price, flag): 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): + 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.") @@ -607,13 +669,17 @@ 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") 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: + with open( + f"{os.environ['WORK_DIR']}/trade/helpers/vol_resolve_{agg}.json", "w" + ) as f: json.dump(data, f) @@ -621,7 +687,7 @@ 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) @@ -632,7 +698,7 @@ def save_option_keys(key, info): 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: + with open(f"{os.environ['WORK_DIR']}/trade/assets/option_key.json", "w") as f: json.dump(option_keys, f) @@ -658,7 +724,9 @@ def filter_zeros(data): return data.ffill() -@backoff.on_exception(backoff.expo, (OpenBBEmptyData, YFinanceEmptyData), max_tries=5, logger=logger) +@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: @@ -679,7 +747,9 @@ def retrieve_timeseries( if spot_type == "chain_price" or spot_type == "chain_spot": df = retrieve_timeseries( tick, - end=(change_to_last_busday(datetime.today()) + BDay(1)).strftime("%Y-%m-%d"), + end=(change_to_last_busday(datetime.today()) + BDay(1)).strftime( + "%Y-%m-%d" + ), start="1960-01-01", interval=interval, provider=provider, @@ -699,10 +769,21 @@ def retrieve_timeseries( 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 + 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.rename(columns={"Stock Splits": "split_ratio", "Dividends": "dividends"}, inplace=True) - data = data.loc[:, ~data.columns.duplicated()] ## For some reason columns are duplicated sometimes + data = data.loc[ + :, ~data.columns.duplicated() + ] ## For some reason columns are duplicated sometimes data.columns = data.columns.str.lower() return data @@ -710,7 +791,9 @@ def query_data(start, end, tick, 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") + 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: @@ -727,7 +810,9 @@ def query_data(start, end, tick, interval): ## Retry up to 3 times while retry_counter < 3: - data = query_data(start=start, end=end, tick=tick, interval=interval) + data = query_data( + start=start, end=end, tick=tick, interval=interval + ) ## If found, break if "split_ratio" in data.columns: @@ -746,9 +831,14 @@ def query_data(start, end, tick, interval): ## 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()) + & ( + 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. + 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) @@ -796,32 +886,47 @@ def query_data(start, end, tick, interval): 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()}" + 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()}" + 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()}" + 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}"' + assert string in TIMEFRAMES_VALUES.keys(), ( + f'Available timeframes are {TIMEFRAMES_VALUES.keys()}, recieved "{string}"' + ) return integer * TIMEFRAMES_VALUES[string] @@ -836,9 +941,9 @@ 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): @@ -901,7 +1006,9 @@ 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. """ @@ -977,7 +1084,11 @@ def find_split_dates_within_range(tick: str, start: str, end: str): """ 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): @@ -1009,18 +1120,27 @@ def assert_equal_length(*args, names: list = None): 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}") + 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}") + 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]] + 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) + 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] @@ -1039,8 +1159,6 @@ def time_distance_helper( 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 @@ -1079,7 +1197,9 @@ def binomial( today = datetime.today() start = today.strftime("%Y-%m-%d") if tick is not None: - logger.info(f"This is no longer supported. Please pass in S0 and y directly. Ticker passed: {tick}") + logger.info( + f"This is no longer supported. Please pass in S0 and y directly. Ticker passed: {tick}" + ) # if y is None: # y = stock.div_yield() # if S0 is None: @@ -1128,7 +1248,9 @@ def binomial( 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 @@ -1155,7 +1277,16 @@ def implied_vol_bs_helper(S0, K, T, r, market_price, flag="c", tol=1e-3, exp_dat def implied_vol_bt( - S0, K, r, market_price, exp_date: str, flag="c", tol=0.000000000001, y=None, start=None, break_time=60 + 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 @@ -1183,7 +1314,16 @@ def implied_vol_bt( 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 C = bs_price - market_price @@ -1222,7 +1362,9 @@ def volga(S, K, r, T, sigma, flag, q): else: 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.") + raise ValueError( + "Invalid Option Type. Only 'C' for Call and 'P' for Put are available." + ) return volga @@ -1239,7 +1381,9 @@ def vanna(S, K, r, T, sigma, flag, q): else: 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.") + raise ValueError( + "Invalid Option Type. Only 'C' for Call and 'P' for Put are available." + ) return vanna @@ -1392,11 +1536,15 @@ def optionPV_helper( ) # Black-Scholes-Merton Process (with dividend yield) - bsm_process = ql.BlackScholesMertonProcess(spot_handle, dividend_ts, risk_free_ts, volatility_ts) + bsm_process = ql.BlackScholesMertonProcess( + spot_handle, dividend_ts, risk_free_ts, volatility_ts + ) if model == "mcs": # Monte Carlo Pricing (Longstaff-Schwartz) - monte_carlo_engine = ql.MCAmericanEngine(bsm_process, "PseudoRandom", timeSteps=250, requiredSamples=10000) + 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() @@ -1497,7 +1645,9 @@ def IV_handler(*args, **kwargs): return 0.0 -def binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, dividend_yield): +def binomial_implied_vol( + price, S, K, r, exp_date, option_type, pricing_date, dividend_yield +): """ Calculate the implied volatility of an option using the binomial tree model. @@ -1569,9 +1719,13 @@ def binomial_implied_vol(price, S, K, r, exp_date, option_type, pricing_date, di def generate_option_tick(symbol, right, exp, strike): exp = to_datetime(exp).strftime("%Y-%m-%d") - assert right.upper() in ["P", "C"], f"Recieved '{right}' for right. Expected 'P' or 'C'" + 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'" + 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": @@ -1616,15 +1770,26 @@ def parse_option_tick(tick: str) -> OptionTickComponents: 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'" + 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": @@ -1649,7 +1814,10 @@ def wait_for_response(wait_time, condition_func, interval): def to_datetime( - date_input: str | datetime | pd.Series | list, format: Optional[str] = None + date_input: str | datetime | pd.Series | list, + format: Optional[str] = None, + ny_utc: bool = False, + custom_tz: Optional[str] = None, ) -> datetime | pd.DatetimeIndex: """ Convert a string or iterable to datetime object(s). @@ -1666,45 +1834,60 @@ def to_datetime( Raises: ValueError: If conversion fails with all attempted methods. """ - # Return datetime objects as-is - if isinstance(date_input, (datetime)): - return date_input + if ny_utc and custom_tz: + raise ValueError("Pass only one of 'ny_utc' or 'custom_tz'.") - elif isinstance(date_input, pd.Timestamp): - return date_input.to_pydatetime() + is_scalar = isinstance( + date_input, (str, datetime, date, pd.Timestamp, np.datetime64) + ) - elif isinstance(date_input, np.datetime64): - return pd.to_datetime(date_input).to_pydatetime() + if not is_scalar: + if format: + dt = pd.to_datetime(date_input, format=format) + else: + try: + dt = pd.to_datetime(date_input, format="%Y-%m-%d") + except (ValueError, TypeError): + dt = pd.to_datetime(date_input) - elif isinstance(date_input, date): - return datetime(date_input.year, date_input.month, date_input.day) + if ny_utc or custom_tz: + target_tz = "America/New_York" if ny_utc else custom_tz + if getattr(dt, "tz", None) is None: + dt = dt.tz_localize("UTC") + dt = dt.tz_convert(target_tz) + return dt - # Handle iterables (list, tuple, pd.Series, etc.) - if hasattr(date_input, "__iter__") and not isinstance(date_input, str): + if isinstance(date_input, datetime): + dt = date_input + elif isinstance(date_input, pd.Timestamp): + dt = date_input.to_pydatetime() + elif isinstance(date_input, np.datetime64): + dt = pd.to_datetime(date_input).to_pydatetime() + elif isinstance(date_input, date): + dt = datetime(date_input.year, date_input.month, date_input.day) + elif isinstance(date_input, str): if format: - return pd.to_datetime(date_input, format=format) + dt = datetime.strptime(date_input, 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) + dt = datetime.strptime(date_input, "%Y-%m-%d") + except ValueError: + result = pd.to_datetime(date_input) + dt = ( + result.to_pydatetime() + if isinstance(result, pd.Timestamp) + else result + ) + else: + raise TypeError(f"Unsupported date_input type: {type(date_input)}") - # Handle single string input - if format: - return datetime.strptime(date_input, format) + if ny_utc or custom_tz: + target_tz = NY if ny_utc else pytz.timezone(custom_tz) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=pytz.UTC) + dt = dt.astimezone(target_tz) - # 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 + return dt def is_busday(date): @@ -1751,7 +1934,10 @@ def not_trading_day(date: str | datetime, time_aware: bool = False) -> bool: ## Time Check only if time != 00:00:00 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: + if ( + pd.to_datetime(date).time() < open_time + or pd.to_datetime(date).time() > close_time + ): ret_bool = True else: ret_bool = False @@ -1827,7 +2013,10 @@ def not_trading_day(date: str | datetime, time_aware: bool = False) -> bool: def change_to_last_busday( - end: Union[str, datetime], offset: int = 1, eod_time: bool = True, time_of_day_aware: bool = True + end: Union[str, datetime], + offset: int = 1, + eod_time: bool = True, + time_of_day_aware: bool = True, ) -> datetime: """ Adjust date to a valid business day, handling weekends, holidays, and market hours. @@ -1897,7 +2086,12 @@ def change_to_last_busday( # If no time specified (midnight), default to market close if current_time == pd.Timestamp("00:00:00").time(): - end_dt = end_dt.replace(hour=market_close.hour, minute=market_close.minute, second=0, microsecond=0) + end_dt = end_dt.replace( + hour=market_close.hour, + minute=market_close.minute, + second=0, + microsecond=0, + ) # Before market open - move to previous business day elif current_time < market_open: # Determine direction based on offset @@ -1905,17 +2099,29 @@ def change_to_last_busday( end_dt = end_dt - BDay(1) else: end_dt = end_dt + BDay(abs(offset)) - end_dt = end_dt.replace(hour=market_close.hour, minute=market_close.minute, second=0, microsecond=0) + end_dt = end_dt.replace( + hour=market_close.hour, + minute=market_close.minute, + second=0, + microsecond=0, + ) # After or at market close - cap at close time else: - end_dt = end_dt.replace(hour=market_close.hour, minute=market_close.minute, second=0, microsecond=0) + end_dt = end_dt.replace( + hour=market_close.hour, + minute=market_close.minute, + second=0, + microsecond=0, + ) # Step 2: Ensure we're on a valid business day (not weekend or holiday) # Use a single consolidated loop to handle both weekends and holidays max_iterations = 10 # Prevent infinite loops iterations = 0 - while (not is_busday(end_dt) or is_USholiday(end_dt)) and iterations < max_iterations: + while ( + not is_busday(end_dt) or is_USholiday(end_dt) + ) and iterations < max_iterations: if offset == 0: # Try to find nearest business day (prefer backward) end_dt = end_dt - BDay(1) From 5f8d8ea3c67e9a452d49507fbf784ed8dbd9bd4d Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 26 Apr 2026 21:33:44 -0400 Subject: [PATCH 40/81] chore: update editor and plotting defaults --- .vscode/settings.json | 1 + trade/__init__.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index a7c6353..4a2e5d3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "editor.defaultFormatter": "vscode.json-language-features" }, "editor.formatOnSave": false, + "python.terminal.useEnvFile": true, // Ruff: Linting only (style, code quality) "ruff.enable": true, diff --git a/trade/__init__.py b/trade/__init__.py index fd41121..563fef5 100644 --- a/trade/__init__.py +++ b/trade/__init__.py @@ -12,7 +12,9 @@ from .helpers.Logging import setup_logger from pathlib import Path + warnings.filterwarnings("ignore") +pd.options.plotting.backend = "plotly" # type: ignore # Load .env file first before accessing any environment variables load_dotenv() From 4d46740dbcf512f107b797049dbfa624991b63ed Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:24 -0400 Subject: [PATCH 41/81] fix(datamanager): disable duplicate CACHE_NAME registry check --- trade/datamanager/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py index 5db78f6..29bc344 100644 --- a/trade/datamanager/base.py +++ b/trade/datamanager/base.py @@ -77,13 +77,13 @@ def __init_subclass__(cls, **kwargs: Any) -> None: 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__}." - ) + # 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 From 0c605d2629877f51af0fc637103a68ba194c87b6 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:31 -0400 Subject: [PATCH 42/81] feat(datamanager): add negative-cache support for option spot gaps --- trade/datamanager/option_spot.py | 130 +++++++--- trade/datamanager/utils/cache.py | 284 ++++++++++++++++------ trade/datamanager/utils/classification.py | 120 +++++++++ trade/datamanager/utils/data_structure.py | 4 + trade/datamanager/utils/date.py | 173 ++++++++----- 5 files changed, 537 insertions(+), 174 deletions(-) create mode 100644 trade/datamanager/utils/classification.py diff --git a/trade/datamanager/option_spot.py b/trade/datamanager/option_spot.py index 7185db1..df95562 100644 --- a/trade/datamanager/option_spot.py +++ b/trade/datamanager/option_spot.py @@ -26,15 +26,18 @@ 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.utils.classification import classify_option_spot_dates 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.DataAPI.ThetaExceptions import ThetaDataNotFound 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. @@ -105,7 +108,7 @@ def _sync_date( strike: Optional[float] = None, expiration: Optional[Union[datetime, str]] = None, right: Optional[str] = None, - endpoint_source: Optional[OptionSpotEndpointSource] = OptionSpotEndpointSource.EOD + endpoint_source: Optional[OptionSpotEndpointSource] = OptionSpotEndpointSource.EOD, ) -> Tuple[DATE_HINT, DATE_HINT]: """Synchronizes requested dates with available data range from Thetadata. @@ -145,9 +148,9 @@ def _sync_date( strike=strike, expiration=expiration, right=right, - endpoint_source=endpoint_source + endpoint_source=endpoint_source, ) - + def get_option_spot( self, date: Union[datetime, str], @@ -288,7 +291,6 @@ def get_option_spot_timeseries( result.rt = False result.model_price = model_price or self.CONFIG.model_price - strike, right, symbol, expiration = _handle_opttick_param( strike=strike, right=right, @@ -356,15 +358,66 @@ def get_option_spot_timeseries( right=right, ) + # Ensure a DatetimeIndex even when the API returned an empty DataFrame. + if fetched_data.empty and not isinstance(fetched_data.index, pd.DatetimeIndex): + fetched_data.index = pd.DatetimeIndex([]) + + # Classify what the API returned: observed rows vs confirmed-missing dates. + # start_date/end_date are already sync'd to the valid window by _sync_date. + classification = classify_option_spot_dates( + fetched=fetched_data, + valid_start=start_date, + valid_end=end_date, + ) + logger.info( + f"Option spot date classification for key {key}: " + f"{len(classification.observed_dates)} observed, " + f"{len(classification.checked_missing_dates)} checked-missing." + ) + + # Keep only rows that had at least one real value. + if not fetched_data.empty: + fetched_data = fetched_data.loc[fetched_data.index.isin(classification.observed_dates)] + # 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) + if not fetched_data.empty: + fetched_data.index = default_timestamp(fetched_data.index) + + # If the requested window has only checked-missing dates, add placeholder + # NaN rows so sanitization and later cache hits can shape the output range. + if classification.checked_missing_dates: + checked_missing_idx = pd.DatetimeIndex(to_datetime(classification.checked_missing_dates)) + checked_missing_idx = default_timestamp(checked_missing_idx) + + has_requested_rows = False + if not fetched_data.empty: + has_requested_rows = fetched_data.index.isin(checked_missing_idx).any() + + if not has_requested_rows: + placeholder_cols = ( + fetched_data.columns.tolist() + if isinstance(fetched_data, pd.DataFrame) and len(fetched_data.columns) > 0 + else ["open", "high", "low", "close", "volume", "count"] + ) + placeholder_df = pd.DataFrame(index=checked_missing_idx, columns=placeholder_cols, dtype=float) + + if fetched_data.empty: + fetched_data = placeholder_df + else: + merged = pd.concat([fetched_data, placeholder_df]) + fetched_data = merged[~merged.index.duplicated(keep="last")].sort_index() + + # Cache both real rows and checked-missing coverage. + _data_structure_cache_it( + self, + key, + fetched_data, + checked_missing_dates=classification.checked_missing_dates, + ) # Sanitize before returning fetched_data = _data_structure_sanitize( @@ -426,30 +479,36 @@ def _query_thetadata_api( - 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, + try: + 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, + ) + except ThetaDataNotFound: + logger.warning( + f"ThetaData returned no data for {self.symbol} {strike}{right} exp={expiration} " + f"from {start_date} to {end_date}. Returning empty DataFrame." ) + return pd.DataFrame() - 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, @@ -470,13 +529,11 @@ def rt( 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." - ) + 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, @@ -497,8 +554,8 @@ def rt( result.daily_option_spot = rt result.key = self.make_key( symbol=self.symbol, - time = datetime.now().time(), - date = datetime.now(), + time=datetime.now().time(), + date=datetime.now(), artifact_type=ArtifactType.OPTION_SPOT, series_id=SeriesId.AT_TIME, endpoint_source=OptionSpotEndpointSource.QUOTE, @@ -510,4 +567,3 @@ def rt( result.endpoint_source = OptionSpotEndpointSource.QUOTE result.rt = True return result - \ No newline at end of file diff --git a/trade/datamanager/utils/cache.py b/trade/datamanager/utils/cache.py index 6a8478f..a91d854 100644 --- a/trade/datamanager/utils/cache.py +++ b/trade/datamanager/utils/cache.py @@ -8,58 +8,98 @@ from .date import _should_save_today, DATE_HINT from ..base import BaseDataManager from .data_structure import _data_structure_sanitize +from trade.datamanager.exceptions import EmptyDataException from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME + logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) MARKET_OPEN = pd.Timestamp(get_pricing_config()["MARKET_OPEN_TIME"]).time() MARKET_CLOSE = pd.Timestamp(get_pricing_config()["MARKET_CLOSE_TIME"]).time() + + @dataclass class _CachedData: """ Represents cached timeseries data along with metadata about its date coverage and missing dates. The goal of this class is to encapsulate the cached data and provide utility methods to check if it fully covers a requested date range. This allows the cache checking logic to be more efficient by avoiding repeated calculations of missing dates and date range coverage. + + checked_missing_dates: Business dates that were explicitly requested from the source and confirmed + to have no data. These count as covered so they are never re-fetched, even though the data rows + for those dates are NaN. """ + key: str data: Union[pd.Series, pd.DataFrame] data_start_date: Optional[DATE_HINT] = None data_end_date: Optional[DATE_HINT] = None missing_dates_within_range: List[DATE_HINT] = None + checked_missing_dates: List[DATE_HINT] = None def __post_init__(self): if not isinstance(self.data, (pd.Series, pd.DataFrame)): raise TypeError(f"Expected pd.Series or pd.DataFrame for cached data, got {type(self.data)}") if not isinstance(self.data.index, pd.DatetimeIndex): raise TypeError("Expected DatetimeIndex for cached timeseries data.") - + + if self.checked_missing_dates is None: + self.checked_missing_dates = [] + self.data_start_date = self.data.index.min().date() if not self.data.empty else None self.data_end_date = self.data.index.max().date() if not self.data.empty else None if self.data_start_date and self.data_end_date: missing = get_missing_dates(self.data, _start=self.data_start_date, _end=self.data_end_date) - self.missing_dates_within_range = [to_datetime(d).date() for d in missing] + # A date that is confirmed checked-missing is not a gap to re-fetch. + checked_missing_set = {to_datetime(d).date() for d in (self.checked_missing_dates or [])} + self.missing_dates_within_range = [ + to_datetime(d).date() for d in missing if to_datetime(d).date() not in checked_missing_set + ] else: self.missing_dates_within_range = [] def is_fully_covered(self, start_dt: DATE_HINT, end_dt: DATE_HINT) -> bool: - """Checks if the cached data fully covers the requested date range.""" - if self.data.empty: - logger.info(f"Cached data is empty for key: {self.key}.") + """Checks if the cached data fully covers the requested date range. + + A date is considered covered if it has a real row in the cached DataFrame + OR if it was explicitly confirmed as having no data (checked_missing_dates). + """ + start_d = to_datetime(start_dt).date() + end_d = to_datetime(end_dt).date() + + checked_missing_set = {to_datetime(d).date() for d in (self.checked_missing_dates or [])} + + # Determine the outer boundary spanned by real data and checked-missing dates combined. + all_covered_dates = set() + if not self.data.empty: + all_covered_dates.update(self.data.index.date) + all_covered_dates.update(checked_missing_set) + + if not all_covered_dates: + logger.info(f"Cached data is empty and no checked-missing dates for key: {self.key}.") return False - if self.data_start_date > to_datetime(start_dt).date() or self.data_end_date < to_datetime(end_dt).date(): - logger.info(f"Cached data date range {self.data_start_date} to {self.data_end_date} does not cover requested range {to_datetime(start_dt).date()} to {to_datetime(end_dt).date()}.") + + effective_start = min(all_covered_dates) + effective_end = max(all_covered_dates) + + if effective_start > start_d or effective_end < end_d: + logger.info( + f"Covered range {effective_start} to {effective_end} does not span " + f"requested range {start_d} to {end_d} for key: {self.key}." + ) return False - if not self.missing_dates_within_range: - logger.info(f"No missing dates within cached data range for key: {self.key}.") - return True - - missing_in_range = [to_datetime(d).date() for d in self.missing_dates_within_range if to_datetime(start_dt).date() <= d <= to_datetime(end_dt).date()] - _bool = len(missing_in_range) == 0 + # Any index gap that is not in checked_missing_dates is a real gap. + unfilled_gaps = [d for d in (self.missing_dates_within_range or []) if start_d <= d <= end_d] + _bool = len(unfilled_gaps) == 0 if not _bool: - logger.info(f"Missing dates within requested range for key {self.key}: {missing_in_range}. Fully covered: {_bool}") + logger.info( + f"Unfilled gaps within requested range for key {self.key}: {unfilled_gaps}. Fully covered: {_bool}" + ) else: - logger.info(f"No missing dates within requested range for key {self.key}. Fully covered: {_bool}") + logger.info(f"No unfilled gaps within requested range for key {self.key}. Fully covered: {_bool}") return _bool - - def _comprehensive_cache_check(self, start_dt: DATE_HINT, end_dt: DATE_HINT) -> Tuple[Optional[Union[pd.Series, pd.DataFrame]], bool, DATE_HINT, DATE_HINT]: + + def _comprehensive_cache_check( + self, start_dt: DATE_HINT, end_dt: DATE_HINT + ) -> Tuple[Optional[Union[pd.Series, pd.DataFrame]], bool, DATE_HINT, DATE_HINT]: """ Performs a comprehensive check to determine if the cached data fully covers the requested date range, and identifies any missing dates. Return args order: @@ -69,48 +109,109 @@ def _comprehensive_cache_check(self, start_dt: DATE_HINT, end_dt: DATE_HINT) -> - missing_end_date: The latest missing date if partially present, else end_dt """ if to_datetime(end_dt).date() == date.today() and datetime.now().time() < MARKET_OPEN: - logger.info(f"Requested end date {end_dt} is today but market has not opened yet. Adjusting end date to last business day.") + logger.info( + f"Requested end date {end_dt} is today but market has not opened yet. Adjusting end date to last business day." + ) end_dt = to_datetime(end_dt) - pd.tseries.offsets.BDay(1) if self.is_fully_covered(start_dt, end_dt): logger.info(f"Cache hit for timeseries data structure key: {self.key}") - sanitized_data = _data_structure_sanitize( - self.data, - start=start_dt, - end=end_dt, - source_name=f"cached timeseries for key {self.key}", + start_d = to_datetime(start_dt).date() + end_d = to_datetime(end_dt).date() + checked_missing_in_range = sorted( + [ + to_datetime(d) + for d in (self.checked_missing_dates or []) + if start_d <= to_datetime(d).date() <= end_d + ] ) + + try: + sanitized_data = _data_structure_sanitize( + self.data, + start=start_dt, + end=end_dt, + source_name=f"cached timeseries for key {self.key}", + ) + except EmptyDataException: + # The whole window is checked-missing with no real rows at all. + if not checked_missing_in_range: + raise + if isinstance(self.data, pd.Series): + sanitized_data = pd.Series( + index=pd.DatetimeIndex(checked_missing_in_range), dtype=float, name=self.data.name + ) + else: + cols = self.data.columns.tolist() if len(self.data.columns) > 0 else ["close"] + sanitized_data = pd.DataFrame( + index=pd.DatetimeIndex(checked_missing_in_range), columns=cols, dtype=float + ) + sanitized_data = _data_structure_sanitize( + sanitized_data, + start=start_dt, + end=end_dt, + source_name=f"cached checked-missing placeholder for key {self.key}", + ) + else: + # Real rows exist but checked-missing dates in range may be absent. + # Back-fill NaN rows for those dates so the caller sees the full window. + if checked_missing_in_range: + existing_dates = set(sanitized_data.index.normalize()) + absent = [d for d in checked_missing_in_range if pd.Timestamp(d).normalize() not in existing_dates] + if absent: + absent_idx = pd.DatetimeIndex(absent) + if isinstance(sanitized_data, pd.Series): + gap_rows = pd.Series(index=absent_idx, dtype=sanitized_data.dtype, name=sanitized_data.name) + else: + gap_rows = pd.DataFrame(index=absent_idx, columns=sanitized_data.columns, dtype=float) + sanitized_data = pd.concat([sanitized_data, gap_rows]).sort_index() + sanitized_data.index.name = "datetime" return sanitized_data, False, to_datetime(start_dt), to_datetime(end_dt) - + + checked_missing_set = {to_datetime(d).date() for d in (self.checked_missing_dates or [])} + + # Unfilled gaps: index gaps not explained by checked_missing_dates. + raw_missing = get_missing_dates(self.data, _start=start_dt, _end=end_dt) + unfilled = [d for d in raw_missing if to_datetime(d).date() not in checked_missing_set] + logger.info( f"Cache partially covers requested date range for timeseries data structure. " - f"Key: {self.key}. Fetching missing dates within range: {[d for d in self.missing_dates_within_range if to_datetime(start_dt).date() <= d <= to_datetime(end_dt).date()]}" + f"Key: {self.key}. Dates still needed: {unfilled}" ) - - missing_in_range = get_missing_dates(self.data, _start=start_dt, _end=end_dt) - missing_start_date = to_datetime(min(missing_in_range) if missing_in_range else start_dt) - missing_end_date = to_datetime(max(missing_in_range) if missing_in_range else end_dt) + + missing_start_date = to_datetime(min(unfilled) if unfilled else start_dt) + missing_end_date = to_datetime(max(unfilled) if unfilled else end_dt) return self.data, True, missing_start_date, missing_end_date + def _simple_extract_from_cache(key: str, cache: CustomCache) -> Optional[Union[pd.Series, pd.DataFrame]]: """Simple helper to extract cached data, handling the _CachedData wrapper.""" cached = cache.get(key, default=None) cached = _extract_data(cached) return cached + def _extract_data(data: Union[pd.Series, pd.DataFrame, _CachedData]) -> Union[pd.Series, pd.DataFrame]: """Extracts the actual data from a _CachedData object or returns it directly if it's already a Series/DataFrame.""" if isinstance(data, _CachedData): return data.data return data + def _data_structure_cache_it( - self: BaseDataManager, - key: str, - value: Union[pd.Series, pd.DataFrame], - *, + self: BaseDataManager, + key: str, + value: Union[pd.Series, pd.DataFrame], + *, expire: Optional[int] = None, + checked_missing_dates: Optional[List[DATE_HINT]] = None, ): - """Merges and caches rate timeseries, excluding today's partial data.""" + """Merges and caches rate timeseries, excluding today's partial data. + + Args: + checked_missing_dates: Business dates that were explicitly queried but + returned no data. Stored in cache metadata so they are never + re-fetched, even when all price values are NaN. + """ 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)}") @@ -120,7 +221,7 @@ def _data_structure_cache_it( 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, @@ -128,8 +229,10 @@ def _data_structure_cache_it( value=value, expire=expire, cache=self, + checked_missing_dates=checked_missing_dates, ) - + + def _cache_it_timeseries_data_structure( existing: Union[pd.Series, pd.DataFrame], key: str, @@ -137,58 +240,76 @@ def _cache_it_timeseries_data_structure( expire: Optional[int] = None, cache: CustomCache = None, skip_today_check: bool = False, + checked_missing_dates: Optional[List[DATE_HINT]] = None, ): - """Caches a timeseries data structure, merging with existing data and handling today's data.""" + """Caches a timeseries data structure, merging with existing data and handling today's data. + + checked_missing_dates are merged with any already-stored checked-missing dates on + the existing cache entry, so coverage knowledge accumulates across writes. + """ + # Extract existing checked-missing dates before unwrapping _CachedData. + existing_checked_missing: List[DATE_HINT] = [] if isinstance(existing, _CachedData): + existing_checked_missing = list(existing.checked_missing_dates or []) existing = existing.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)}" + + 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)}" + ) + + # Merge all checked-missing dates (existing + new), keeping unique date values. + merged_checked_missing: List[DATE_HINT] = list( + {to_datetime(d).date() for d in (existing_checked_missing + (checked_missing_dates or []))} + ) ## 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 - - max_date = value.index.max().date() - max_is_today = max_date == date.today() - - ## Really only makes sense to remove today's data if max date is today. if not just skip the check and save whatever we have since it won't be partial day data. - ## This also avoids the overhead of checking today's date and time for every cache entry that has a max date in the past. + max_date = value.index.max().date() if not value.empty else None + max_is_today = max_date == date.today() if max_date is not None else False + if max_is_today: 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())] - else: + elif max_date is not None: logger.info(f"Max date {max_date} for key: {key} is not today. Skipping today's data check.") ## 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: + skip_cache = False + + ## 1) If after removing today's data, there is no data left AND no checked-missing dates + if value.empty and not merged_checked_missing: + skip_cache = True + logger.info(f"No data and no checked-missing dates to cache for key: {key}") + + ## 2) All-NaN is only rejected when there are no checked-missing dates to preserve. + ## If checked-missing dates are present the empty rows are intentional padding. + if not value.empty and value.isna().all().all() and not merged_checked_missing: + skip_cache = True + logger.info(f"All data points are NaN and no checked-missing dates for key: {key}. Not caching.") + + if skip_cache: return - value.sort_index(inplace=True) - cache_data = _CachedData(key=key, data=value) - logger.info(f"Caching timeseries data structure for key: {key} with date range {cache_data.data_start_date} to {cache_data.data_end_date}, missing dates within range: {cache_data.missing_dates_within_range}") - - cache.set(key, cache_data, expire=expire) + new_cache_data = _CachedData( + key=key, + data=value, + checked_missing_dates=merged_checked_missing, + ) + logger.info( + f"Caching timeseries data structure for key: {key} with date range " + f"{new_cache_data.data_start_date} to {new_cache_data.data_end_date}, " + f"missing dates within range: {new_cache_data.missing_dates_within_range}, " + f"checked-missing dates: {new_cache_data.checked_missing_dates}" + ) + cache.set(key, new_cache_data, expire=expire) def _simple_list_cache_it(self: BaseDataManager, key: str, value: List[Any], *, expire: Optional[int] = None): @@ -202,6 +323,7 @@ def _simple_list_cache_it(self: BaseDataManager, key: str, value: List[Any], *, existing = sorted(list(set(existing))) self.set(key, existing, expire=expire) + def _check_cache_for_timeseries_data_structure( self: BaseDataManager, key: str, @@ -217,28 +339,30 @@ def _check_cache_for_timeseries_data_structure( - 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 isinstance(cached_data, _CachedData): - cached_data = cached_data.data + cached_entry = 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))): - logger.info(f"Cache entry for key: {key} is not a pd.Series, pd.DataFrame, or None. Found type: {type(cached_data)}. Ignoring cache entry.") + # _CachedData is passed through intact so checked_missing_dates is preserved. + if cached_entry is None: + logger.info(f"No cache entry found for key: {key}") return None, False, start_dt, end_dt - if cached_data is None: - logger.info(f"No cache entry found for key: {key}") + if not isinstance(cached_entry, (_CachedData, pd.Series, pd.DataFrame)): + logger.info( + f"Cache entry for key: {key} is not a recognised type. Found type: {type(cached_entry)}. Ignoring cache entry." + ) return None, False, start_dt, end_dt - + return _data_structure_cache_check_missing( - cached_data=cached_data, + cached_data=cached_entry, key=key, start_dt=start_dt, end_dt=end_dt, ) - + + def _data_structure_cache_check_missing( cached_data: Union[pd.Series, pd.DataFrame, _CachedData], key: str, @@ -253,7 +377,7 @@ def _data_structure_cache_check_missing( - 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 """ - ## Firstly we want to ensure backward compatibility with old cache data structure which is just the raw pd.Series or pd.DataFrame. + ## Firstly we want to ensure backward compatibility with old cache data structure which is just the raw pd.Series or pd.DataFrame. ## We will convert it to the new cache data structure and save it back to cache for future use. This way we can also populate the missing dates info for old cache entries. if isinstance(cached_data, (pd.Series, pd.DataFrame)): diff --git a/trade/datamanager/utils/classification.py b/trade/datamanager/utils/classification.py new file mode 100644 index 0000000..e45764a --- /dev/null +++ b/trade/datamanager/utils/classification.py @@ -0,0 +1,120 @@ +"""Option spot date classification for gap-aware caching. + +Classifies fetched option date slices into observed rows versus confirmed-missing +dates so the cache layer knows which dates to never re-request from the API. + +Core Dataclasses: + DateClassification: Output container for the three date buckets. + +Core Functions: + classify_option_spot_dates: Splits a fetched DataFrame into observed and + checked-missing dates given the requested valid window. + +Processing Flow: + 1. Build the full set of expected business dates from the valid window. + 2. Compare against what the API actually returned. + 3. Dates absent from the response are classified as checked-missing. + 4. Dates present with at least one non-NaN price value are observed. + +Usage: + >>> from trade.datamanager.utils.classification import classify_option_spot_dates + >>> result = classify_option_spot_dates( + ... fetched=df, + ... valid_start="2026-01-05", + ... valid_end="2026-01-09", + ... ) + >>> result.observed_dates + DatetimeIndex(['2026-01-05', '2026-01-06', '2026-01-07'], dtype='datetime64[ns]', freq=None) + >>> result.checked_missing_dates + [datetime.date(2026, 1, 8), datetime.date(2026, 1, 9)] +""" + +from dataclasses import dataclass, field +from datetime import datetime +from typing import List, Union + +import pandas as pd + +from trade.helpers.helper import get_missing_dates, to_datetime +from trade import HOLIDAY_SET + + +@dataclass +class DateClassification: + """Output of the option spot date classifier. + + Attributes: + observed_dates: Index of dates where the API returned at least one + non-NaN price value. + checked_missing_dates: Business dates inside the valid window that the + API was asked about but returned no usable data. These should be + stored in the cache so they are never re-requested. + """ + + observed_dates: pd.DatetimeIndex = field(default_factory=pd.DatetimeIndex) + checked_missing_dates: List[datetime] = field(default_factory=list) + + +def classify_option_spot_dates( + fetched: pd.DataFrame, + valid_start: Union[datetime, str], + valid_end: Union[datetime, str], +) -> DateClassification: + """Classifies fetched option dates into observed rows and confirmed-missing dates. + + Compares the dates present in the API response against the full set of + expected business days in the valid window. Dates absent from the response + or present with all-NaN values are marked as checked-missing so they are + never re-fetched. + + Args: + fetched: DataFrame returned by the API, indexed by DatetimeIndex. + May be empty or contain all-NaN rows for some dates. + valid_start: First date of the valid query window (already synced by + _sync_date). + valid_end: Last date of the valid query window (already synced by + _sync_date). + + Returns: + DateClassification with observed_dates and checked_missing_dates populated. + + Examples: + >>> import pandas as pd + >>> import numpy as np + >>> idx = pd.bdate_range("2026-01-05", "2026-01-09") + >>> df = pd.DataFrame({"close": [10.0, np.nan, 12.0, np.nan, np.nan]}, index=idx) + >>> result = classify_option_spot_dates(df, "2026-01-05", "2026-01-09") + >>> list(result.observed_dates.strftime("%Y-%m-%d")) + ['2026-01-05', '2026-01-07'] + >>> sorted(str(d) for d in result.checked_missing_dates) + ['2026-01-06', '2026-01-08', '2026-01-09'] + """ + valid_start_dt = to_datetime(valid_start) + valid_end_dt = to_datetime(valid_end) + + # Full set of expected business days in the valid window, excluding holidays. + expected_bus_days = pd.bdate_range(start=valid_start_dt, end=valid_end_dt) + expected_bus_days = pd.DatetimeIndex([d for d in expected_bus_days if d.strftime("%Y-%m-%d") not in HOLIDAY_SET]) + + if fetched is None or fetched.empty: + return DateClassification( + observed_dates=pd.DatetimeIndex([]), + checked_missing_dates=list(expected_bus_days.date), + ) + + if not isinstance(fetched.index, pd.DatetimeIndex): + fetched = fetched.copy() + fetched.index = to_datetime(fetched.index) + + # Dates where at least one column has a real value. + has_data_mask = ~fetched.isna().all(axis=1) + observed_idx = fetched.index[has_data_mask] + + # All expected dates not in the observed set are checked-missing. + observed_set = set(observed_idx.normalize()) + checked_missing = [d.date() for d in expected_bus_days if d.normalize() not in observed_set] + + return DateClassification( + observed_dates=observed_idx, + checked_missing_dates=checked_missing, + ) diff --git a/trade/datamanager/utils/data_structure.py b/trade/datamanager/utils/data_structure.py index 88a8d76..05e2898 100644 --- a/trade/datamanager/utils/data_structure.py +++ b/trade/datamanager/utils/data_structure.py @@ -11,6 +11,10 @@ logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) PANDAS_DATA_HINT = Union[pd.Series, pd.DataFrame] +class EmptyFloat(float): + """""" + + def _data_structure_sanitize( df: Union[pd.Series, pd.DataFrame], start: Union[datetime, str], diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index 5a8de7b..c0d7806 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -4,22 +4,24 @@ 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.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 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.helper_types import is_iterable # noqa from trade.helpers.Logging import setup_logger -from trade.optionlib.utils.format import assert_equal_length # noqa +from trade.optionlib.utils.format import assert_equal_length # noqa from dbase.DataAPI.ThetaData import list_dates +from dbase.DataAPI.ThetaExceptions import ThetaDataNotFound 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 from trade import MARKET_CLOSE, MARKET_OPEN + logger = setup_logger(UTILS_LOGGER_NAME, stream_log_level=get_logging_level()) PATH = Path(os.environ["GEN_CACHE_PATH"]) / "dm_gen_cache" @@ -28,7 +30,6 @@ ## This is to avoid calling API all the time - LIST_DATE_CACHE = CustomCache( location=PATH.as_posix(), fname="list_date_cache", @@ -36,6 +37,7 @@ expire_days=365, ) + def _convert_expiration_to_datetime(expiration: Union[str, datetime]) -> datetime: """ Converts an expiration date to a datetime object. If the input is already a datetime, it is returned as is. @@ -46,10 +48,13 @@ def _convert_expiration_to_datetime(expiration: Union[str, datetime]) -> datetim A datetime object representing the expiration date. """ if isinstance(expiration, datetime): - return expiration.replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) # Set to end of day for accurate T calculation + return expiration.replace( + hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute + ) # Set to end of day for accurate T calculation else: return to_datetime(expiration).replace(hour=MARKET_CLOSE.hour, minute=MARKET_CLOSE.minute) - + + def _convert_dates_to_datetime(dates: List[Union[str, datetime]]) -> List[datetime]: """ Converts a list of dates to datetime objects. If an element is already a datetime, it is returned as is. @@ -62,11 +67,14 @@ def _convert_dates_to_datetime(dates: List[Union[str, datetime]]) -> List[dateti converted_dates = [] for d in dates: if isinstance(d, datetime): - converted_dates.append(d.replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute)) # Set to market open time for accurate T calculation + converted_dates.append( + d.replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute) + ) # Set to market open time for accurate T calculation else: converted_dates.append(to_datetime(d).replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute)) return converted_dates + def _convert_date_to_datetime(date_input: Union[str, datetime]) -> datetime: """ Converts a date to a datetime object. If the input is already a datetime, it is returned as is. @@ -77,10 +85,13 @@ def _convert_date_to_datetime(date_input: Union[str, datetime]) -> datetime: A datetime object representing the input date. """ if isinstance(date_input, datetime): - return date_input.replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute) # Set to market open time for accurate T calculation + return date_input.replace( + hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute + ) # Set to market open time for accurate T calculation else: return to_datetime(date_input).replace(hour=MARKET_OPEN.hour, minute=MARKET_OPEN.minute) + 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): @@ -97,8 +108,6 @@ def sync_date_index(*args) -> List[Union[pd.Series, pd.DataFrame]]: return synced_series - - def _sync_date( symbol: str, start_date: DATE_HINT, @@ -153,33 +162,41 @@ def _sync_date( 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 + def _guard_rail_dates( + start_date: datetime, + end_date: datetime, + min_trade_date: datetime, + max_trade_date: datetime, + dates: Optional[list] = None, + ) -> Tuple[datetime, datetime]: + """Ensures start_date and end_date are within min_trade_date and max_trade_date.""" + if start_date < min_trade_date: + logger.warning( + f"Requested start_date {start_date.date()} is before available data. Adjusting to {min_trade_date.date()}." + ) + start_date = min_trade_date + if end_date > max_trade_date: + logger.warning( + f"Requested end_date {end_date.date()} is after available data. Adjusting to {max_trade_date.date()}." + ) + end_date = max_trade_date + if start_date > end_date: + start_date = end_date + + ## Check if date range is present in the list of dates, if provided + if dates is not None: + available_dates = set(to_datetime(dates)) + if start_date not in available_dates: + logger.warning( + f"Adjusted start_date {start_date.date()} is not in available dates. Adjusting to nearest available date." + ) + start_date = min(available_dates, key=lambda d: abs(d - start_date)) + if end_date not in available_dates: + logger.warning( + f"Adjusted end_date {end_date.date()} is not in available dates. Adjusting to nearest available date." + ) + end_date = min(available_dates, key=lambda d: abs(d - end_date)) + return start_date, end_date is_market_hrs = is_market_hours_today() today = ny_now() @@ -188,40 +205,82 @@ def _get_max_date(requested_end: datetime) -> datetime: prev_busday = (today - BDay(1)).to_pydatetime() start_date = to_datetime(start_date) end_date = to_datetime(end_date) + is_expired = timestamp_exp < timestamp_today + + def _compute_max_allowable() -> pd.Timestamp: + """Returns the latest date we should allow for this request at runtime.""" + if not is_expired: + if endpoint_source == OptionSpotEndpointSource.EOD: + max_allowed_today = prev_busday if is_market_hrs else today + else: + max_allowed_today = today + return min(timestamp_exp, pd.Timestamp(max_allowed_today.date())) + + # For expired contracts, expiration day is the latest safe upper bound + # unless we fetched an explicit last-trade date from list_dates below. + return timestamp_exp 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 = cached_dates["max_date"] + min_date = to_datetime(cached_dates["min_date"]) + max_date_raw = cached_dates.get("max_date") + max_date = to_datetime(max_date_raw) if max_date_raw is not None else _compute_max_allowable() 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) + return _guard_rail_dates(start_date, end_date, min_date, max_date) logger.info(f"Fetching date range from Thetadata for {opttick}") - dates = list(list_dates( - symbol=symbol, - exp=expiration, - right=right, - strike=strike, - )) + try: + dates = list( + list_dates( + symbol=symbol, + exp=expiration, + right=right, + strike=strike, + ) + ) + except ThetaDataNotFound: + logger.warning(f"ThetaData returned no data for {opttick}. Returning original requested date range.") + return to_datetime(start_date), to_datetime(end_date) if not dates: - raise ValueError(f"No trading dates found for {opttick}") + logger.warning(f"No trading dates found for {opttick}. Returning original requested date range.") + return to_datetime(start_date), to_datetime(end_date) dates = to_datetime(dates) + max_trade_date = pd.Timestamp(max(dates)) + min_trade_date = pd.Timestamp(min(dates)) ## Adjust start date to min - min_date = min(dates) - max_date = max(dates) - start_date = max(min_date, start_date) - end_date = min(_get_max_date(end_date), max_date) - max_allowable = min(timestamp_today, timestamp_exp) if endpoint_source == OptionSpotEndpointSource.EOD else timestamp_exp - - LIST_DATE_CACHE.set(key=opttick, value={"min_date": pd.Timestamp(min_date), "max_date": pd.Timestamp(max_allowable)}, expire=None) + start_date = max(min_trade_date, start_date) + logger.info(f"Calculated date range for option spot timeseries: {start_date} to {end_date}") + + # This is how far into the future we can get data for. + # For non-expired options, max changes with time, so do not persist it. + if is_expired: + max_allowable = max_trade_date + LIST_DATE_CACHE.set( + key=opttick, + value={ + "min_date": pd.Timestamp(min_trade_date), + "max_date": pd.Timestamp(max_allowable), + "range": dates, + }, + expire=None, + ) + else: + max_allowable = _compute_max_allowable() + LIST_DATE_CACHE.set( + key=opttick, + value={ + "min_date": pd.Timestamp(min_trade_date), + }, + expire=None, + ) - return to_datetime(min(start_date, end_date)), to_datetime(max(start_date, end_date)) + return _guard_rail_dates(start_date, end_date, min_trade_date, max_allowable, dates=dates) @dataclass(slots=True) @@ -277,6 +336,6 @@ def is_available_on_date(date: date) -> bool: ## 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 From 4685c3f4fd48f3bc76c0f460d602f7811f6c5f70 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:34 -0400 Subject: [PATCH 43/81] fix(datamanager): make filter_specials param optional in get_div_schedule --- trade/datamanager/market_data_helpers/dividends.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/trade/datamanager/market_data_helpers/dividends.py b/trade/datamanager/market_data_helpers/dividends.py index a8d1584..5e01f76 100644 --- a/trade/datamanager/market_data_helpers/dividends.py +++ b/trade/datamanager/market_data_helpers/dividends.py @@ -60,7 +60,10 @@ def resample_dividends_to_daily(div_series: pd.Series, buffer: int = 30) -> pd.S resampled.sort_index(inplace=True) return resampled -def get_div_schedule(ticker: str): +def get_div_schedule( + ticker: str, + filter_specials: bool = None, + ) -> pd.Series: """ 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. @@ -77,7 +80,8 @@ def get_div_schedule(ticker: str): ## 4. Return the dividend schedule DataFrame # Check if ticker is in cache - filter_specials = OptionDataConfig().filter_out_special_dividends + if filter_specials is None: + filter_specials = OptionDataConfig().filter_out_special_dividends key = (ticker, filter_specials) if key not in DIVIDEND_CACHE: try: From 28d86e5104dd7d17ec452396ad14938d39876d3b Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:38 -0400 Subject: [PATCH 44/81] test: add end-to-end test for option spot negative cache --- test_negative_cache.py | 105 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test_negative_cache.py diff --git a/test_negative_cache.py b/test_negative_cache.py new file mode 100644 index 0000000..e08b4f2 --- /dev/null +++ b/test_negative_cache.py @@ -0,0 +1,105 @@ +"""Test script for the negative-cache implementation. + +Runs two consecutive calls for a date range that has no option data, +then reports whether the second call avoids an API hit. +""" + +import os +import logging + +logging.basicConfig(level=logging.INFO, format="%(name)s [%(levelname)s] %(message)s") + +os.environ["THETADATA_USE_V3"] = "true" + +from dbase.DataAPI.ThetaData import list_dates, set_use_proxy, retrieve_chain_bulk, list_contracts, retrieve_eod_ohlc +from trade.datamanager.utils.date import _sync_date, LIST_DATE_CACHE +from trade.datamanager.option_spot import OptionSpotDataManager + +set_use_proxy("") + +# ── Check the actual valid window from the API ────────────────────────────── +print("=" * 60) +print("Step 0: list_dates for QQQ 440C 2024-03-28") +d = list_dates(symbol="QQQ", exp="2024-03-28", strike=440, right="C", print_url=True) +d = list(d) if d is not None else [] +if len(d) > 0: + print(f" first 5 : {d[:5]}") + print(f" min={min(d)} max={max(d)} count={len(d)}") +else: + print(" No dates returned") + +# ── Test params ────────────────────────────────────────────────────────────── +symbol = "QQQ" +start_date = "2023-12-27" +end_date = "2024-02-08" +opt_params = dict(strike=440.00, right="C", expiration="2024-03-28") + +# ── Call 1: should hit the API and cache checked_missing_dates ─────────────── +print("\n" + "=" * 60) +print("Call 1: Fetching from API (expect cache miss)") +result1 = OptionSpotDataManager(symbol=symbol).get_option_spot_timeseries( + start_date=start_date, end_date=end_date, **opt_params +) +print(" daily_option_spot:") +print(result1.daily_option_spot) + +# ── Inspect cache entry directly ───────────────────────────────────────────── +from trade.datamanager.utils.cache import _CachedData + +mgr = OptionSpotDataManager(symbol=symbol) +from trade.datamanager._enums import ArtifactType, Interval, SeriesId, OptionSpotEndpointSource +from trade.datamanager.config import OptionDataConfig +from dbase.DataAPI.ThetaData.utils import _handle_opttick_param + +strike, right, sym, expiration = _handle_opttick_param( + strike=440.0, right="C", symbol=symbol, exp="2024-03-28", opttick=None, enforce_single_option=True +) +key = mgr.make_key( + symbol=symbol, + artifact_type=ArtifactType.OPTION_SPOT, + series_id=SeriesId.HIST, + endpoint_source=OptionSpotEndpointSource.EOD.value, + interval=Interval.EOD, + strike=strike, + right=right, + expiration=expiration, +) +print(f"\nCache key: {key}") +cached = mgr.get(key, default=None) +if isinstance(cached, _CachedData): + print(f" _CachedData found") + print(f" data shape : {cached.data.shape}") + print(f" checked_missing: {cached.checked_missing_dates[:10]}") # first 10 + print(f" len(checked_missing): {len(cached.checked_missing_dates)}") +else: + print(f" Cached type: {type(cached)}") + +# ── Call 2: should return from cache WITHOUT calling the API ───────────────── +print("\n" + "=" * 60) +print("Call 2: Should be a full cache hit (no API call)") +result2 = OptionSpotDataManager(symbol=symbol).get_option_spot_timeseries( + start_date=start_date, end_date=end_date, **opt_params +) +print(" daily_option_spot:") +print(result2.daily_option_spot) +print("\nDone.") + +# ── Call 3: wider window that includes both real data and checked-missing dates ── +print("\n" + "=" * 60) +print("Call 3: Wider range 2023-11-27 to 2024-02-08 (real data + checked-missing)") +result3 = OptionSpotDataManager(symbol=symbol).get_option_spot_timeseries( + start_date="2023-11-27", + end_date="2024-02-08", + **opt_params, +) +df3 = result3.daily_option_spot +print(f" shape: {df3.shape}") +print(f" date range: {df3.index.min().date()} to {df3.index.max().date()}") +real_rows = df3.dropna(how="all") +nan_rows = df3[df3.isna().all(axis=1)] +print(f" real rows : {len(real_rows)}") +print(f" NaN rows : {len(nan_rows)}") +if len(nan_rows): + print(f" first NaN : {nan_rows.index.min().date()}") + print(f" last NaN : {nan_rows.index.max().date()}") +print("\nDone (all calls).") From f6c988e343a5ebb93c0c23c3803cf79bdbe8dda9 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:44 -0400 Subject: [PATCH 45/81] feat(riskmanager): load special dividends from yaml into market_timeseries --- EventDriven/riskmanager/market_timeseries.py | 87 ++++++++++++++++++- .../riskmanager/special_dividends.yaml | 40 +++++++++ 2 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 EventDriven/riskmanager/special_dividends.yaml diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 4ddbd43..977f011 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -157,9 +157,11 @@ from EventDriven.configs.core import SkipCalcConfig, UndlTimeseriesConfig, OptionPriceConfig from trade.assets.rates import get_risk_free_rate_helper_v2 from threading import Lock -from typing import Dict, Any, Union +from typing import Dict, Any, Union, List import pandas as pd import numpy as np +import yaml +from importlib.resources import files from trade.helpers.Logging import setup_logger from trade.helpers.pools import _change_global_stream_level from EventDriven.dataclasses.timeseries import AtTimeOptionData, AtTimePositionData @@ -167,6 +169,17 @@ logger = setup_logger("EventDriven.riskmanager.market_timeseries", stream_log_level="WARNING") logger.info("Changing pools log level to WARNING for market_timeseries module") _change_global_stream_level("WARNING") +SPECIAL_DIVIDENDS: Dict[str, List[Dict[str, Any]]] = {} +NO_SPECIAL_DIVIDENDS: List[str] = [] + +def load_special_divs(): + global SPECIAL_DIVIDENDS, NO_SPECIAL_DIVIDENDS + loc = files("EventDriven.riskmanager").joinpath("special_dividends.yaml") + with open(loc, "r") as f: + info = yaml.safe_load(f) + + SPECIAL_DIVIDENDS = info["special_dividends"] + NO_SPECIAL_DIVIDENDS = info["no_special_dividends"] class BacktestTimeseries(BacktestRunMixin): @@ -183,6 +196,7 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): target="processed_option_data", clear_on_exit=True ) ## Cache to store processed option data, cleared on exit to avoid polluting the cache with potentially large data that might not be needed in future sessions. self.position_data_cache = load_riskmanager_cache(target="position_data") + self.special_dividends = load_riskmanager_cache(target="special_dividend") self.splits = load_riskmanager_cache(target="splits_raw") self.adjusted_strike_cache = load_riskmanager_cache(target="adjusted_strike_cache") @@ -193,6 +207,16 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): self.undl_timeseries_config = UndlTimeseriesConfig() self.option_price_config = OptionPriceConfig() self.lock = Lock() + + ## Private attrs + self._backup_position_data_cache = load_riskmanager_cache( + target="position_data_backup", clear_on_exit=True, create_on_missing=True + ) ## Backup cache to store original position data before any adjustments, cleared on exit to avoid polluting the cache. + self._loaded_special_dividends = {} + + ## Load special dividends info from yaml file into cache. + ## Reloads on initialization to ensure we have the most up-to-date information, and to avoid issues with mutable global state if the module is reloaded during the session. + load_special_divs() def skip(self, position_id: str, date: Union[datetime, str], column: str = "Midpoint") -> bool: """ @@ -213,6 +237,7 @@ def pre_run_setup(self): ## Clear session related caches self.session_loaded_option_cache.clear() self.position_data_cache.clear() + self._backup_position_data_cache.clear() self.adjusted_strike_cache.clear() def set_splits(self, d): @@ -226,6 +251,52 @@ def set_splits(self, d): if compare_dates.inbetween(d[0], self.start_date, self.end_date): splits_dict[k].append(d) return splits_dict + + def get_special_dividends(self, ticker: str) -> List[Dict[str, float]]: + """ + Retrieve special dividend information for a given ticker. + - If the ticker is in the no_special_dividends list, returns an empty list. + - If the ticker is in the special_dividends list, returns the corresponding dividend information. + - If the ticker is not found in either list, raises a ValueError. + """ + if ticker in self._loaded_special_dividends: + return self._loaded_special_dividends[ticker] + + if ticker in NO_SPECIAL_DIVIDENDS: + return {} + elif ticker in SPECIAL_DIVIDENDS: + divs = SPECIAL_DIVIDENDS[ticker] + new_fmt = {} + for d in divs: + new_fmt[to_datetime(d["adjusted_ex_date"])] = d["amount"] + return new_fmt + else: + raise ValueError( + f"Ticker {ticker} not found in either special or no special dividends list. Please update the yaml file accordingly." + ) + + def get_list_of_splits(self, ticker: str) -> List[Dict[str, float]]: + """ + Retrieve list of splits for a given ticker. + """ + t = self.market_timeseries._get_chain_spot_timeseries(ticker, "1990-01-01") + return list((t[t["split_ratio"] != 1]["split_ratio"]).items()) + + + def get_splits(self, ticker: str, bkt_end_date: pd.Timestamp): + """ + Retrieve splits for a given ticker, updating the cache if necessary. + """ + split = self.splits.get(ticker, None) + if split is None: + split = self.get_list_of_splits(ticker) + self.splits[ticker] = {"split": split, "last_updated": pd.Timestamp.now()} + else: + last_updated = self.splits[ticker].get("last_updated", pd.Timestamp(0)) + if pd.Timestamp(bkt_end_date) > last_updated: + split = self.get_list_of_splits(ticker) + self.splits[ticker] = {"split": split, "last_updated": pd.Timestamp.now()} + return self.splits[ticker]["split"] def get_option_data(self, opttick: str) -> pd.DataFrame: """ @@ -243,7 +314,11 @@ def get_position_data(self, position_id: str) -> pd.DataFrame: """ Retrieve position data for a given position ID. """ - return self.position_data_cache.get(position_id, pd.DataFrame()) + d = self.position_data_cache.get(position_id, pd.DataFrame()) + if not d.empty: + logger.info(f"Position data for {position_id} found in position data cache, returning cached data") + d = self._backup_position_data_cache.get(position_id, d) ## Return the backup data if it exists, otherwise return the original data. This allows us to retain the original position data before any adjustments, while still allowing for adjustments to be made to the position data in the main cache. + return d def get_at_time_option_data(self, opttick: str, date: Union[datetime, str]) -> AtTimeOptionData: """ @@ -324,9 +399,10 @@ def calculate_option_data(self, position_id: str, date: Union[datetime, str]) -> ) ## Check cache first - early return if data exists - if position_id in self.position_data_cache: + d = self.get_position_data(position_id) + if not d.empty and pd.to_datetime(date) in d.index: logger.info(f"Position Data for {position_id} already available in cache, returning cached data") - return self.position_data_cache[position_id] + return d ## Data not in cache - perform full calculation logger.critical(f"Position Data for {position_id} not available, calculating greeks. Load time ~5 minutes") @@ -403,6 +479,7 @@ def get_timeseries(_id, direction): ## Cache the position data self.position_data_cache[position_id] = position_data + self._backup_position_data_cache[position_id] = position_data.copy() ## Store a copy of the original position data in the backup cache before any adjustments are made, to ensure we have the original data available for future reference if needed. return position_data @@ -536,7 +613,9 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: ## Check if there's any split/special dividend splits = self.splits.get(meta["ticker"], []) + splits = self.get_splits(meta["ticker"], bkt_end_date=effective_end) dividends = self.special_dividends.get(meta["ticker"], {}) + dividends = self.get_special_dividends(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 diff --git a/EventDriven/riskmanager/special_dividends.yaml b/EventDriven/riskmanager/special_dividends.yaml new file mode 100644 index 0000000..a0ce706 --- /dev/null +++ b/EventDriven/riskmanager/special_dividends.yaml @@ -0,0 +1,40 @@ + +no_special_dividends: + - HD + - TSLA + - NVDA + - AAPL + - SBUX + - META + - AMZN + - AMD + - NFLX + +special_dividends: + COST: + - adjusted_ex_date: "2015-02-04" + amount: 5.0 + payment_date: "2015-02-20" + - adjusted_ex_date: "2017-05-10" + amount: 7.0 + payment_date: "2017-05-26" + - adjusted_ex_date: "2020-12-10" + amount: 10.0 + payment_date: "2020-12-18" + - adjusted_ex_date: "2023-12-28" + amount: 15.0 + payment_date: "2024-01-12" + TGT: + - adjusted_ex_date: "2007-10-10" + amount: 3.0 + payment_date: "2007-10-31" + - adjusted_ex_date: "2009-12-16" + amount: 1.0 + payment_date: "2009-12-28" + QQQ: + - adjusted_ex_date: "2023-12-27" + amount: 0.22 + payment_date: "2024-01-15" + - adjusted_ex_date: "2014-02-27" + amount: 0.37 + payment_date: "2014-03-07" \ No newline at end of file From 690b9cf2209412cac090bbc78fff3d9df675ebfc Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:48 -0400 Subject: [PATCH 46/81] fix(cogs): use business-day arithmetic for action effective_date --- .../riskmanager/position/cogs/limits.py | 78 +++++++----- .../riskmanager/position/cogs/pnl_monitor.py | 115 +++++++++++------- 2 files changed, 119 insertions(+), 74 deletions(-) diff --git a/EventDriven/riskmanager/position/cogs/limits.py b/EventDriven/riskmanager/position/cogs/limits.py index 4836af4..19fbe9f 100644 --- a/EventDriven/riskmanager/position/cogs/limits.py +++ b/EventDriven/riskmanager/position/cogs/limits.py @@ -234,7 +234,7 @@ from typing import List, Optional, Union, Dict from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.position.base import BaseCog -from EventDriven.riskmanager.sizer._sizer import DefaultSizer, BaseSizer, ZscoreRVolSizer, default_delta_limit # noqa +from EventDriven.riskmanager.sizer._sizer import DefaultSizer, BaseSizer, ZscoreRVolSizer, default_delta_limit # noqa from EventDriven.configs.core import ZscoreSizerConfigs, DefaultSizerConfigs from EventDriven.dataclasses.limits import PositionLimits from EventDriven.dataclasses.states import ( @@ -282,7 +282,7 @@ def __init__( self._sizer_configs: Optional[Union[DefaultSizerConfigs, ZscoreSizerConfigs]] = sizer_configs self.position_limits: Dict[str, PositionLimits] = {} self.position_metadata: Dict[str, _LimitsMetaData] = {} - self.allow_buffer = True # Whether to allow a buffer to the delta limit if the calculated position size is very low (e.g. <=2 contracts). + self.allow_buffer = True # Whether to allow a buffer to the delta limit if the calculated position size is very low (e.g. <=2 contracts). self.underlier_list = list(set(underlier_list if underlier_list is not None else [])) if config is None: config = LimitsEnabledConfig() @@ -321,9 +321,9 @@ def sizer_configs(self) -> Union[DefaultSizerConfigs, ZscoreSizerConfigs]: @sizer_configs.setter def sizer_configs(self, value: Union[DefaultSizerConfigs, ZscoreSizerConfigs]) -> None: - assert isinstance( - value, (DefaultSizerConfigs, ZscoreSizerConfigs) - ), "sizer_configs must be of type DefaultSizerConfigs or ZscoreSizerConfigs" + assert isinstance(value, (DefaultSizerConfigs, ZscoreSizerConfigs)), ( + "sizer_configs must be of type DefaultSizerConfigs or ZscoreSizerConfigs" + ) self._sizer_configs = value ## Update config delta limit type to match sizer configs @@ -354,7 +354,9 @@ def on_new_position(self, new_pos_state: NewPositionState) -> NewPositionState: self._calculate_limits(new_pos_state) self._update_position_quantity(new_pos_state) self._create_position_metadata(new_pos_state) - self._on_new_position_failsafe(new_pos_state) # Ensure limits and quantity are set to reasonable values even if there are issues in the main logic + self._on_new_position_failsafe( + new_pos_state + ) # Ensure limits and quantity are set to reasonable values even if there are issues in the main logic return new_pos_state def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPositionState: @@ -376,21 +378,33 @@ def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPosit sizing_lev=self.sizer_configs.sizing_lev, ) if new_pos_state.limits is None: - logger.warning(f"Limits were not set for trade_id {new_pos_state.order['data']['trade_id']}. Setting to default delta limit of {default_delta_lmt}.") - new_pos_state.limits = PositionLimits(delta=default_delta_lmt, dte=self.config.default_dte, moneyness=self.config.default_moneyness) + logger.warning( + f"Limits were not set for trade_id {new_pos_state.order['data']['trade_id']}. Setting to default delta limit of {default_delta_lmt}." + ) + new_pos_state.limits = PositionLimits( + delta=default_delta_lmt, dte=self.config.default_dte, moneyness=self.config.default_moneyness + ) if new_pos_state.order["data"]["quantity"] == 0: max_size_cash_can_buy = abs(math.floor(tick_cash / (option_price * 100))) if max_size_cash_can_buy >= 1: - logger.warning(f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}. However, based on the available cash of {tick_cash} and option price of {option_price}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Setting quantity to 1.") + logger.warning( + f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}. However, based on the available cash of {tick_cash} and option price of {option_price}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Setting quantity to 1." + ) order_dict = new_pos_state.order.to_dict() order_dict["data"]["quantity"] = 1 new_pos_state.order = Order.from_dict(order_dict) else: - logger.warning(f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}, and even a single contract cannot be afforded based on the available cash of {tick_cash} and option price of {option_price}. Quantity will remain at 0.") - + logger.warning( + f"Quantity was calculated as 0 for trade_id {new_pos_state.order['data']['trade_id']}, and even a single contract cannot be afforded based on the available cash of {tick_cash} and option price of {option_price}. Quantity will remain at 0." + ) + ## Update limits to set to delta + buffer to avoid repeated sizing issues if the issue was with limit calculation - logger.warning(f"Delta limit for trade_id {new_pos_state.order['data']['trade_id']} was calculated as {new_pos_state.limits.delta}, which is below the default delta per contract of {delta}. Setting delta limit to {delta * 1.15} to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation.") - new_pos_state.limits.delta = delta * 1.15 # Set delta limit to 15% above the delta of a single contract to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation + logger.warning( + f"Delta limit for trade_id {new_pos_state.order['data']['trade_id']} was calculated as {new_pos_state.limits.delta}, which is below the default delta per contract of {delta}. Setting delta limit to {delta * 1.15} to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation." + ) + new_pos_state.limits.delta = ( + delta * 1.15 + ) # Set delta limit to 15% above the delta of a single contract to allow for at least 1 contract to be sized in the next cycle, and to avoid repeated sizing issues if the issue was with limit calculation pos_lmts = PositionLimits( delta=new_pos_state.limits.delta, dte=self.config.default_dte, @@ -404,11 +418,12 @@ def _on_new_position_failsafe(self, new_pos_state: NewPositionState) -> NewPosit if metadata is not None: metadata.delta_lmt = new_pos_state.limits.delta metadata.new_quantity = new_pos_state.order["data"]["quantity"] - logger.warning(f"Updated metadata for trade_id {new_pos_state.order['data']['trade_id']} to reflect new delta limit of {metadata.delta_lmt} and new quantity of {metadata.new_quantity}.") - + logger.warning( + f"Updated metadata for trade_id {new_pos_state.order['data']['trade_id']} to reflect new delta limit of {metadata.delta_lmt} and new quantity of {metadata.new_quantity}." + ) - return new_pos_state + def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: """ Create and store metadata for the new position. @@ -440,7 +455,6 @@ def _create_position_metadata(self, new_pos_state: NewPositionState) -> None: delta_lmt=new_pos_state.limits.delta, new_quantity=order["data"]["quantity"], rvol=rvol, - ) logger.info(f"Storing position metadata: {metadata}") self._store_metadata(metadata) @@ -472,7 +486,9 @@ def _calculate_limits(self, new_pos_state: NewPositionState) -> float: current_cash=request.tick_cash, underlier_price_at_time=undl_data.chain_spot["close"], ) - logger.info(f"Calculated limits for position {order['data']['trade_id']}: {limits}. Details: cash={request.tick_cash}, undl_price={undl_data.chain_spot['close']}") + logger.info( + f"Calculated limits for position {order['data']['trade_id']}: {limits}. Details: cash={request.tick_cash}, undl_price={undl_data.chain_spot['close']}" + ) pos_lmts = PositionLimits( delta=limits, dte=self.config.default_dte, @@ -520,13 +536,8 @@ def _update_position_quantity(self, new_position_state: NewPositionState) -> Non f"Calculated position size is {q} for order {order['data']['trade_id']}, which is very low and may indicate that the position is hitting the delta limit. Delta per contract is {delta} and delta limit is {delta_lmt}. Consider reviewing the position or adjusting the delta limit to allow for more flexibility." ) if isinstance(self.sizer, ZscoreRVolSizer): - rvol_z = self.sizer.scaler.get_rvol_on_date( - sym=new_position_state.symbol, date=request.date - ) - multiplier = min( - 1 + 0.15 * max(rvol_z, 0), - 1.20 - ) + rvol_z = self.sizer.scaler.get_rvol_on_date(sym=new_position_state.symbol, date=request.date) + multiplier = min(1 + 0.15 * max(rvol_z, 0), 1.20) else: multiplier = 1.15 new_delta_lmt = delta_lmt * multiplier @@ -537,8 +548,10 @@ def _update_position_quantity(self, new_position_state: NewPositionState) -> Non creation_date=request.date, ) self._save_position_limits(order["data"]["trade_id"], order["signal_id"], lmts) - logger.warning(f"Delta limit for order {order['data']['trade_id']} has been increased from {delta_lmt} to {new_delta_lmt} to allow for a larger position size and to avoid repeatedly hitting the delta limit. This adjustment is based on a multiplier of {multiplier} applied to the original delta limit.") - + logger.warning( + f"Delta limit for order {order['data']['trade_id']} has been increased from {delta_lmt} to {new_delta_lmt} to allow for a larger position size and to avoid repeatedly hitting the delta limit. This adjustment is based on a multiplier of {multiplier} applied to the original delta limit." + ) + logger.info(f"Updated position quantity to {q} for order {order['data']['trade_id']}.") new_position_state.order = Order.from_dict(order_dict) @@ -556,7 +569,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction bkt_start_date = bkt_info.start_date t_plus_n = bkt_info.t_plus_n last_updated = portfolio_state.last_updated - t_plus_n_timedelta = pd.Timedelta(days=t_plus_n) + t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) for position in positions: trade_id = position.trade_id @@ -609,8 +622,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction ## This is because analysis is based on EOD data at last_updated. Execution therefore has to start from the next trading day. ## If t_plus_n is 0, then effective date will be the next trading day after last_updated, which is the expected behavior. ## If t_plus_n is > 0, then effective date will be last_updated + t_plus_n, but if that falls on a non-trading day, we need to move it to the next trading day. Therefore, we add a buffer of 1 day to ensure we move to the next trading day if last_updated + t_plus_n falls on a non-trading day. - tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day - action.effective_date = last_updated + tplus_n_timedelta + action.effective_date = last_updated + t_plus_n_bdays ## Only generate verbose_info for non-HOLD actions (Task #4 optimization) if action.action != "HOLD": @@ -624,7 +636,9 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction action.verbose_info = None position.action = action opinions.append(position) - return CogActions(opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name) + return CogActions( + opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name + ) def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: """ @@ -632,4 +646,4 @@ def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestam This can be used by the position manager to enforce sizing constraints. """ - return np.inf # No limit by default, can be overridden by specific logic in cogs like mean reversion cog \ No newline at end of file + return np.inf # No limit by default, can be overridden by specific logic in cogs like mean reversion cog diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 623b302..68449c9 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -6,21 +6,19 @@ from EventDriven.dataclasses.orders import OrderRequest from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.position.base import BaseCog -from EventDriven.dataclasses.states import ( - PositionAnalysisContext, - CogActions, - PositionState -) +from EventDriven.dataclasses.states import PositionAnalysisContext, CogActions, PositionState from EventDriven.riskmanager.actions import CLOSE, ROLL, HOLD from pydantic.dataclasses import dataclass as pydantic_dataclass + logger = setup_logger("EventDriven.riskmanager.position.cogs.pnl_monitor", stream_log_level="INFO") + @pydantic_dataclass class PnlMonitorConfig(BaseCogConfig): """ Configuration dataclass for PnLMonitorCog. """ - + name: Optional[str] = "PnLMonitorCog" enabled: bool = True @@ -29,6 +27,7 @@ class PnLMonitorCog(BaseCog): """ Cog to monitor the PnL of open positions and trigger alerts or actions based on predefined thresholds. """ + default_config = PnlMonitorConfig() def __init__(self, config: Optional[PnlMonitorConfig] = None): @@ -36,6 +35,7 @@ def __init__(self, config: Optional[PnlMonitorConfig] = None): config = PnlMonitorConfig() super().__init__(config) self.config = config + self.enable_stop_loss = False # Enable stop loss by default def on_new_position(self, new_position_state: NewPositionState) -> None: """ @@ -47,8 +47,9 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: """ Don't do anything on new order request. """ - logger.info(f"Received new order request for {new_request_state.symbol} with signal ID {new_request_state.signal_id}. Monitoring PnL for this request.") - + logger.info( + f"Received new order request for {new_request_state.symbol} with signal ID {new_request_state.signal_id}. Monitoring PnL for this request." + ) pnl = new_request_state.symbol_total_pnl tick_cash = new_request_state.tick_cash @@ -58,29 +59,37 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. if pnl is not None and pnl > 0: - additional_tick_cash = (pnl * 0.25) ## Add 25% of the profits to the tick cash to scale up the position and lock in profits. - + additional_tick_cash = ( + pnl * 0.25 + ) ## Add 25% of the profits to the tick cash to scale up the position and lock in profits. + ## Undo pnl from tick cash to avoid double counting, then add the additional tick cash to lock in profits. undone_pnl_tick_cash = tick_cash - pnl new_tick_cash = undone_pnl_tick_cash + additional_tick_cash - logger.info(f"Details: PnL: {pnl:.2f}, Original Tick Cash: {tick_cash:.2f}, Undone PnL Tick Cash: {undone_pnl_tick_cash:.2f}, Additional Tick Cash to Lock in Profits: {additional_tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}") - logger.info(f"Adding {additional_tick_cash:.2f} to tick cash for {new_request_state.symbol} to lock in profits. Original Tick Cash: {tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}, PnL: {pnl:.2f}") + logger.info( + f"Details: PnL: {pnl:.2f}, Original Tick Cash: {tick_cash:.2f}, Undone PnL Tick Cash: {undone_pnl_tick_cash:.2f}, Additional Tick Cash to Lock in Profits: {additional_tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}" + ) + logger.info( + f"Adding {additional_tick_cash:.2f} to tick cash for {new_request_state.symbol} to lock in profits. Original Tick Cash: {tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}, PnL: {pnl:.2f}" + ) new_request_state.tick_cash = new_tick_cash new_request_state.is_tick_cash_scaled = True - + else: - logger.info(f"No profits to lock in for {new_request_state.symbol}. Tick Cash remains at {tick_cash:.2f}. PnL: {pnl:.2f}") + logger.info( + f"No profits to lock in for {new_request_state.symbol}. Tick Cash remains at {tick_cash:.2f}. PnL: {pnl:.2f}" + ) def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: """ Analyze the current position context and return any actions to be taken. For this PnL monitor, we will not take any specific actions, but we could log or trigger alerts if needed. """ - logger.info(f"PnLMonitor received position analysis context {portfolio_context}. Analyzing PnL for open positions.") + # logger.info(f"PnLMonitor received position analysis context {portfolio_context}. Analyzing PnL for open positions.") opinions = [] ## Rules: - ## 1. If PnL is greater than 50% of entry price, close half the position if quantity > 1 to lock in profits. + ## 1. If PnL is greater than 50% of entry price, close half the position if quantity > 1 to lock in profits. ## 2. If quantity is 1 and PnL is greater than 150% of entry price, ROLL the position. ## 2a. If have closed before, and remainding quantity is > 1, ROLL when PnL is greater than 150% of entry price to lock in profits. positions = portfolio_context.portfolio.positions @@ -88,52 +97,72 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction t_plus_n = bkt_info.t_plus_n portfolio_state = portfolio_context.portfolio last_updated = portfolio_state.last_updated - t_plus_n_timedelta = pd.Timedelta(days=t_plus_n) + t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) for pos_state in positions: - pl_pct = pos_state.pnl / (pos_state.entry_price * pos_state.quantity) if pos_state.entry_price * pos_state.quantity != 0 else 0 + pl_pct = ( + pos_state.pnl / (pos_state.entry_price * pos_state.quantity) + if pos_state.entry_price * pos_state.quantity != 0 + else 0 + ) if pl_pct > 0.5 and pos_state.quantity > 1: - qdiff = math.ceil(pos_state.quantity / 2) + qdiff = -math.ceil(pos_state.quantity / 2) new_q = pos_state.quantity - qdiff - logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}") - action = CLOSE( - trade_id=pos_state.trade_id, - action={"quantity_diff": qdiff, "new_quantity": new_q} + logger.info( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}" ) + action = CLOSE(trade_id=pos_state.trade_id, action={"quantity_diff": qdiff, "new_quantity": new_q}) action.analysis_date = portfolio_context.date action.reason = f"PnL is {pl_pct:.2%} which is greater than 50% of entry price. Closing half the position to lock in profits." action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}" - tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day - action.effective_date = last_updated + tplus_n_timedelta + action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action opinions.append(pos_state) - + elif pl_pct > 1.0: - if pos_state.quantity == 1 or (self._has_closed_before(pos_state.trade_id, pos_state) and pos_state.quantity > 1): - logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits.") + if pos_state.quantity == 1 or ( + self._has_closed_before(pos_state.trade_id, pos_state) and pos_state.quantity > 1 + ): + logger.info( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits." + ) action = ROLL( - trade_id=pos_state.trade_id, - action={"quantity_diff": 0, "new_quantity": pos_state.quantity} + trade_id=pos_state.trade_id, action={"quantity_diff": 0, "new_quantity": pos_state.quantity} ) action.analysis_date = portfolio_context.date action.reason = f"PnL is {pl_pct:.2%} which is greater than 150% of entry price. Rolling the position to lock in profits." action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits." - tplus_n_timedelta = max(t_plus_n_timedelta, pd.Timedelta(days=1)) # Ensure at least 1 day is added to move to the next trading day - action.effective_date = last_updated + tplus_n_timedelta + action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action opinions.append(pos_state) + + ## Stop loss branch: close <=-70% + elif pl_pct <= -0.7 and self.enable_stop_loss: + logger.info( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." + ) + action = CLOSE( + trade_id=pos_state.trade_id, action={"quantity_diff": -pos_state.quantity, "new_quantity": 0} + ) + action.analysis_date = portfolio_context.date + action.reason = f"PnL is {pl_pct:.2%} which is less than or equal to -70% of entry price. Closing the position to prevent further losses." + action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." + action.effective_date = last_updated + t_plus_n_bdays + pos_state.action = action + opinions.append(pos_state) + else: logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken.") pos_state.action = HOLD(trade_id=pos_state.trade_id) pos_state.action.reason = f"PnL is {pl_pct:.2%}. No action taken." - pos_state.action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken." + pos_state.action.verbose_info = ( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken." + ) opinions.append(pos_state) - - - - - return CogActions(opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name) - + return CogActions( + opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name + ) + def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[int, int]: """ Helper method to aggregate the total open and close quantities for a position based on its trade entries. @@ -142,10 +171,13 @@ def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[in if entries.empty: return 0, 0 open_qty = entries[entries["direction"] == "BUY"]["quantity"].sum() - close_qty = entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].sum() if not entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].empty else 0 + close_qty = ( + entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].sum() + if not entries[entries["direction"].isin(["SELL", "EXERCISE"])]["quantity"].empty + else 0 + ) return open_qty, close_qty - def _has_closed_before(self, trade_id: str, portfolio_context: PositionState) -> bool: """ Helper method to check if a position has been closed before based on its trade ID. @@ -157,4 +189,3 @@ def _has_closed_before(self, trade_id: str, portfolio_context: PositionState) -> if open_qty == 0: return False return close_qty > 0 - From eb07ab5cd4e18f622f6e5a0ca24a6a86dc3b8112 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:52 -0400 Subject: [PATCH 47/81] fix(execution): guard SELL fallback and log sizing exceptions --- EventDriven/execution.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/EventDriven/execution.py b/EventDriven/execution.py index 1472fe5..a72acf7 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -374,6 +374,7 @@ def execute_order(self, order_event: OrderEvent): # Ensuring cash doesn't go below zero raw_quantity = order_event.quantity + quantity = 0 try: if raw_quantity is not None: @@ -391,7 +392,9 @@ def execute_order(self, order_event: OrderEvent): ## Clamp quantity to ensure we don't exceed available cash while total_cost > order_event.cash: - logger.info(f"Total cost {total_cost} exceeds available cash {order_event.cash}. Reducing quantity.") + logger.info( + f"Total cost {total_cost} exceeds available cash {order_event.cash}. Reducing quantity." + ) quantity -= 1 total_cost = quantity * price + self.commission_rate logger.info( @@ -402,10 +405,18 @@ def execute_order(self, order_event: OrderEvent): # For SELL, we can only sell what we have in the position quantity = raw_quantity else: - # Fall back to normal logic - quantity = math.floor(order_event.cash / (price + self.commission_rate)) - except: - pass + # Fall back to cash-based sizing for BUY orders. + if order_event.direction == "BUY": + quantity = math.floor(order_event.cash / (price + self.commission_rate)) + else: + quantity = 0 + except Exception as exc: # noqa + logger.exception( + f"Failed to determine quantity for signal {order_event.signal_id}. Defaulting quantity to 0. Error: {exc}, cash: {order_event.cash}, price: {price}, commission_rate: {self.commission_rate}, unit_cost: {price + self.commission_rate}" + ) + quantity = 0 + + quantity = max(int(quantity), 0) commission = ( self.commission_rate * quantity * (len(order_event.position.get("trade_id", "&L:").split("&")) - 1) From bdeb25c2f4f2b05b753b579e903e6a9a38271ff3 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:57 -0400 Subject: [PATCH 48/81] fix(attribution): apply direction-aware sign to trade quantity --- EventDriven/attribution.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index bf24a9d..48e24b8 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -143,6 +143,14 @@ def _get_trade_quantity_time_series( individual_trades = trade_obj.buy_ledger.ledger + trade_obj.sell_ledger.ledger individual_trades_df = pd.DataFrame(individual_trades) + ## Monitor if this addition is correct + individual_trades_df["quantity"] = individual_trades_df.apply( + lambda row: ( + row["quantity"] if row["direction"] == "BUY" else -abs(row["quantity"]) + ), + axis=1, + ) + ## Format the individual trades DataFrame for analysis cols = [ "datetime", @@ -190,6 +198,7 @@ def _aggregate_trade_group(group): .sort_index() .reset_index() ) + individual_trades_df["qty_change"] = individual_trades_df.apply( lambda row: row["qty_change"] if row["direction"] == "BUY" else -abs(row["qty_change"]), axis=1 From ec6ff8fd3c1fd3d46109b5c574270a1a1dbc1d21 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 4 May 2026 00:08:57 -0400 Subject: [PATCH 49/81] chore(riskmanager): replace print with logger, fix noqa placement --- EventDriven/riskmanager/new_base.py | 2 +- EventDriven/riskmanager/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index 00093e5..3d65b27 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -450,7 +450,7 @@ def get_order(self, req: OrderRequest) -> NewPositionState: return {"result": ResultsEnum.IS_HOLIDAY.value, "data": None} ## Run through position analyzer first - print(f"Running order request through position analyzer: {req}") + logger.info(f"Running order request through position analyzer: {req}") self.position_analyzer.on_new_order_request(new_request_state=req) ## Investigate if tick cash is scaled diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 9ae51e8..ffe2d3f 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -185,7 +185,7 @@ from trade.datamanager.loaders import load_full_option_data # from trade.datamanager.vars import get_times_series -from trade.datamanager._enums import DivType, OptionPricingModel +from trade.datamanager._enums import DivType, OptionPricingModel # noqa from trade.datamanager.utils.date import sync_date_index 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 85ebf26f985d0d5ebfa366d4a7acf4f5817d062d Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Fri, 8 May 2026 19:35:50 -0500 Subject: [PATCH 50/81] Stuff --- trade/helpers/Logging.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/trade/helpers/Logging.py b/trade/helpers/Logging.py index f98c598..edd110c 100644 --- a/trade/helpers/Logging.py +++ b/trade/helpers/Logging.py @@ -108,6 +108,7 @@ def setup_logger( remove_root=True, custom_logger_name=None, timezone=None, + dir: str | Path = None, ) -> logging.Logger: """ Set up a logger with console and file handlers, with environment-aware configuration. @@ -138,7 +139,7 @@ def setup_logger( FILE_LOG_LEVEL = 'INFO' PROPAGATE_TO_ROOT_LOGGER = 'False' """ - project_root_log_dir = get_logger_base_location() + project_root_log_dir = dir or get_logger_base_location() # If custom logger name is None, use filename: From 9e61434e74bec70380f59b7ddc7335287cdc312f Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 11 May 2026 21:47:31 -0400 Subject: [PATCH 51/81] feat(EventDriven/configs): move PnlMonitorConfig to configs.core and update exports - Add PnlMonitorConfig pydantic dataclass to configs/core.py - Export PnlMonitorConfig from export_configs.py - Add PnlMonitorConfig and related entries to configs/vars.py --- EventDriven/configs/core.py | 10 +++++ EventDriven/configs/export_configs.py | 24 +++++++++++- EventDriven/configs/vars.py | 55 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index f38bfad..28d908c 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -326,3 +326,13 @@ class ExecutionHandlerConfig(BaseConfigs): "randomized" # Whether to use randomized slippage, fixed slippage, slippage as a percentage of the spread, or no slippage. Default is randomized slippage. ) pct_alpha: float = 0.25 # If using spread_pct slippage model, this is the percentage of the spread to use as slippage. For example, if pct_alpha is 0.25 and the spread is $0.20, then the slippage will be $0.05. + + +@pydantic_dataclass +class PnlMonitorConfig(BaseCogConfig): + """ + Configuration dataclass for PnLMonitorCog. + """ + + name: Optional[str] = "PnLMonitorCog" + enabled: bool = True \ No newline at end of file diff --git a/EventDriven/configs/export_configs.py b/EventDriven/configs/export_configs.py index 3bdb79c..b95efc7 100644 --- a/EventDriven/configs/export_configs.py +++ b/EventDriven/configs/export_configs.py @@ -6,6 +6,7 @@ import yaml import logging import importlib +from trade.helpers.Logging import setup_logger if TYPE_CHECKING: from EventDriven.configs.core import ( @@ -24,12 +25,15 @@ LimitsEnabledConfig, PositionAnalyzerConfig, PortfolioManagerConfig, + LiquidityConfig, BacktesterConfig, CashAllocatorConfig, RiskManagerConfig, + MeanReversionSizerConfigs, + ExecutionHandlerConfig, ) -logger = logging.getLogger(__name__) +logger = setup_logger("EventDriven.configs.export_configs") # Type variable for generic config types T = TypeVar("T", bound=BaseConfigs) @@ -65,9 +69,17 @@ class ConfigsDict(TypedDict, total=False): LimitsEnabledConfig: "LimitsEnabledConfig" PositionAnalyzerConfig: "PositionAnalyzerConfig" PortfolioManagerConfig: "PortfolioManagerConfig" + LiquidityConfig: "LiquidityConfig" + LiquidityConfig: "LiquidityConfig" BacktesterConfig: "BacktesterConfig" CashAllocatorConfig: "CashAllocatorConfig" RiskManagerConfig: "RiskManagerConfig" + MeanReversionSizerConfigs: "MeanReversionSizerConfigs" + ScoringConfigs: "ScoringConfigs" + ExecutionHandlerConfig: "ExecutionHandlerConfig" + MeanReversionSizerConfigs: "MeanReversionSizerConfigs" + ScoringConfigs: "ScoringConfigs" + ExecutionHandlerConfig: "ExecutionHandlerConfig" @dataclass @@ -96,12 +108,20 @@ def save_to_yaml(self, filename: str): Args: filename (str): The path to the file where configs will be saved. """ + confs = {} + for label, cfg in self.configs.items(): + + if not isinstance(cfg, dict): + # Convert config objects to dicts for YAML serialization + confs[label] = _sanitize_for_yaml(cfg) + else: + confs[label] = cfg data = { "run_name": self.run_name, "created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else str(self.created_at), - "configs": self.configs, + "configs": confs, "metadata": self.metadata, } # Use safe_dump to avoid Python object tags diff --git a/EventDriven/configs/vars.py b/EventDriven/configs/vars.py index e6432c6..38fbb08 100644 --- a/EventDriven/configs/vars.py +++ b/EventDriven/configs/vars.py @@ -19,6 +19,10 @@ "BacktesterConfig": "Configuration for backtest execution including settlement delays and trade finalization.", "RiskManagerConfig": "Configuration for the risk manager controlling order and analysis caching behavior.", "CashAllocatorConfig": "Threshold-based cash bucket allocator for symbols.", + "LiquidityConfig": "Centralized liquidity control for both risk and execution layers, managing spread and liquidity level.", + "MeanReversionSizerConfigs": "Custom mean reversion position sizer using z-score scaling to adjust sizing dynamically around a target DTE.", + "ScoringConfigs": "Configuration for scoring and selecting options based on moneyness, DTE, mid price, spread, and theta burden targets.", + "ExecutionHandlerConfig": "Configuration for the execution handler controlling slippage model and spread percentage parameters.", } CONFIG_DEFINITIONS = { @@ -132,6 +136,8 @@ "raise_errors": "Flag to raise errors during backtest execution instead of logging them (default False).", "min_slippage_pct": "Minimum slippage percentage applied to trade execution (default 0.075).", "max_slippage_pct": "Maximum slippage percentage applied to trade execution (default 0.15).", + "commission_per_contract_in_units": "Commission charged per contract in dollar units (default 0.0065).", + "liquidity": "LiquidityConfig instance controlling spread and liquidity level for backtest execution.", }, "RiskManagerConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -143,6 +149,55 @@ "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", "thresholds": "(min_alloc, bucket_value) pairs; first pair whose min_alloc is satisfied sets the bucket. Cash is supplied at runtime.", }, + "LiquidityConfig": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "level": "Liquidity enforcement level (0=none, 1=standard, 2=strict). Clamped to [0, 2] on init.", + "max_spread_pct": "Maximum allowable spread as a percentage of mid price (default 0.25).", + }, + "MeanReversionSizerConfigs": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "name": "Name identifier for this sizer cog (default 'custom_mean_reversion_sizer').", + "beta": "Mean reversion speed parameter controlling how aggressively sizing reverts to the mean (default 0.5).", + "min_scale": "Minimum scaling factor applied to base sizing level (default 0.5).", + "max_scale": "Maximum scaling factor applied to base sizing level (default 2.0).", + "sizing_lev": "Base leverage level for position sizing (default 2).", + "default_dte": "Default days to expiration used in scaling calculations (default 10).", + "enabled_limits": "StrategyLimitsEnabled instance controlling which limit types are active for this sizer.", + "min_zscore": "Minimum z-score threshold required to trigger scaling adjustments (default 2.5).", + }, + "ScoringConfigs": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "tilt_strength": "Strength of the directional tilt applied during scoring (default 0.2).", + "spread_ticks": "Number of strike ticks between spread legs (default 2).", + "structure_direction": "Direction of the structure: 'long' or 'short' (default 'long').", + "strategy": "Options strategy type: 'vertical' or 'naked' (default 'vertical').", + "hybrid_strategy_enabled": "Enable hybrid strategy combining multiple structure types (default False).", + "m_target": "Target moneyness for option selection (default 0.8).", + "min_moneyness": "Minimum moneyness allowed for selected options (default 0.45).", + "max_moneyness": "Maximum moneyness allowed for selected options (default 1.05).", + "m_sigma": "Moneyness scoring sigma for gaussian weighting (default 0.2).", + "m_tilt": "Moneyness tilt preference: 'otm', 'itm', or 'atm' (default 'otm').", + "target_dte": "Target days to expiration for scoring (default 200).", + "dte_tolerance": "Allowed DTE deviation from target (default 100).", + "dte_sigma": "DTE scoring sigma for gaussian weighting (default 10).", + "dte_tilt": "DTE tilt preference: 'flat', 'short', or 'long' (default 'short').", + "mid_min": "Minimum acceptable mid price (default 0.5).", + "mid_max": "Maximum acceptable mid price (default 3.0).", + "mid_upper_limit": "Hard upper limit on mid price (default 5).", + "mid_lower_limit": "Hard lower limit on mid price (default 0.25).", + "mid_sigma": "Mid price scoring sigma for gaussian weighting (default 0.25).", + "pct_spread_max": "Maximum allowable spread as a percentage of mid price (default 1.0).", + "target_spread_pct": "Target spread percentage for scoring (default 0.2).", + "pct_spread_sigma": "Spread scoring sigma for gaussian weighting (default 0.10).", + "oi_target": "Target open interest for scoring (default 1000).", + "theta_burden_max": "Maximum theta burden as a fraction of position value (default 0.03).", + "theta_burden_sigma": "Theta burden scoring sigma for gaussian weighting (default 0.02).", + }, + "ExecutionHandlerConfig": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "slippage_model": "Slippage model to apply: 'randomized', 'fixed', 'spread_pct', or 'none' (default 'randomized').", + "pct_alpha": "Fraction of the spread used as slippage when using spread_pct model (default 0.25).", + }, } From 96ddd8be72310fa762159f468b7dfa4dfe820674 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 11 May 2026 21:47:42 -0400 Subject: [PATCH 52/81] refactor(EventDriven/riskmanager): import PnlMonitorConfig from configs.core - Remove duplicate PnlMonitorConfig definition from pnl_monitor.py - Import PnlMonitorConfig from EventDriven.configs.core - Minor fixes to _vars.py, states.py, and market_timeseries.py --- EventDriven/_vars.py | 7 +++++-- EventDriven/dataclasses/states.py | 1 - EventDriven/riskmanager/market_timeseries.py | 3 ++- EventDriven/riskmanager/position/cogs/pnl_monitor.py | 10 +--------- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/EventDriven/_vars.py b/EventDriven/_vars.py index 79c55dd..d8669fd 100644 --- a/EventDriven/_vars.py +++ b/EventDriven/_vars.py @@ -124,8 +124,11 @@ def load_riskmanager_cache(target: str = None, case _: if create_on_missing: logger.warning(f"Creating new cache for unknown target: {target}") - clear_on_exit = kwargs.get('clear_on_exit', True) - return CustomCache(BASE, fname = f"extra_{target}", clear_on_exit=clear_on_exit, size_limit=size_limit) + ## Always clear when using temp cache. If not user must explicitly set clear_on_exit + clear_on_exit = True if get_use_temp_cache() else kwargs.get("clear_on_exit", True) + base = BASE / "temp" if get_use_temp_cache() else BASE + + return CustomCache(base, fname = f"extra_{target}", clear_on_exit=clear_on_exit, size_limit=size_limit) raise ValueError(f"Unknown target: {target}") return (spot_timeseries, diff --git a/EventDriven/dataclasses/states.py b/EventDriven/dataclasses/states.py index 944fa63..3370a7e 100644 --- a/EventDriven/dataclasses/states.py +++ b/EventDriven/dataclasses/states.py @@ -57,7 +57,6 @@ class PositionState: action: Optional[RMAction] = None entry_date: Optional[datetime] = None trades: Optional[Trade] = None - signal_total_pnl: Optional[float] = None def __repr__(self): return f"PositionState(date={self.last_updated}, trade_id={self.trade_id}, quantity={self.quantity}, pnl={self.pnl}, signal_id={self.signal_id})" diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 977f011..e6d5f99 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -166,7 +166,7 @@ from trade.helpers.pools import _change_global_stream_level from EventDriven.dataclasses.timeseries import AtTimeOptionData, AtTimePositionData -logger = setup_logger("EventDriven.riskmanager.market_timeseries", stream_log_level="WARNING") +logger = setup_logger("EventDriven.riskmanager.market_timeseries", stream_log_level="INFO") logger.info("Changing pools log level to WARNING for market_timeseries module") _change_global_stream_level("WARNING") SPECIAL_DIVIDENDS: Dict[str, List[Dict[str, Any]]] = {} @@ -259,6 +259,7 @@ def get_special_dividends(self, ticker: str) -> List[Dict[str, float]]: - If the ticker is in the special_dividends list, returns the corresponding dividend information. - If the ticker is not found in either list, raises a ValueError. """ + load_special_divs() if ticker in self._loaded_special_dividends: return self._loaded_special_dividends[ticker] diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 68449c9..9cff927 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -1,26 +1,18 @@ from typing import Tuple, Optional import math import pandas as pd -from EventDriven.configs.core import BaseCogConfig from EventDriven.dataclasses.states import NewPositionState from EventDriven.dataclasses.orders import OrderRequest from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.position.base import BaseCog from EventDriven.dataclasses.states import PositionAnalysisContext, CogActions, PositionState from EventDriven.riskmanager.actions import CLOSE, ROLL, HOLD -from pydantic.dataclasses import dataclass as pydantic_dataclass +from EventDriven.configs.core import PnlMonitorConfig logger = setup_logger("EventDriven.riskmanager.position.cogs.pnl_monitor", stream_log_level="INFO") -@pydantic_dataclass -class PnlMonitorConfig(BaseCogConfig): - """ - Configuration dataclass for PnLMonitorCog. - """ - name: Optional[str] = "PnLMonitorCog" - enabled: bool = True class PnLMonitorCog(BaseCog): From 22cffc69751c750b047afd2b5c1e0169070c35c5 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 11 May 2026 21:47:53 -0400 Subject: [PATCH 53/81] feat(trade/datamanager): update CacheSpec with dynamic path resolution and DataManager improvements - Add dynamic base_dir resolution via get_dm_gen_path() in CacheSpec.__post_init__ - Update dividend and date utilities - Add new vars and config entries for DataManager --- trade/datamanager/base.py | 9 +++++++-- trade/datamanager/config.py | 1 + trade/datamanager/dividend.py | 6 +++--- trade/datamanager/market_data_helpers/dividends.py | 4 ++-- trade/datamanager/utils/date.py | 5 ++++- trade/datamanager/vars.py | 9 +++++++++ 6 files changed, 26 insertions(+), 8 deletions(-) diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py index 29bc344..7ecbc76 100644 --- a/trade/datamanager/base.py +++ b/trade/datamanager/base.py @@ -6,7 +6,7 @@ from trade.helpers.helper import CustomCache from trade.helpers.Logging import setup_logger from pathlib import Path -from .vars import DM_GEN_PATH +from .vars import get_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()) @@ -34,12 +34,16 @@ class CacheSpec: 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 + @property + def base_dir(self) -> Path: + """Allows dynamic base directory resolution, e.g., for live vs backtest.""" + return get_dm_gen_path() + class BaseDataManager(ABC): """ @@ -134,6 +138,7 @@ def get_cache(cls) -> CustomCache: ) return c + @classmethod def clear_all_caches(cls) -> None: """Clears caches for all registered DataManager subclasses.""" diff --git a/trade/datamanager/config.py b/trade/datamanager/config.py index 3758ba5..78d6bb2 100644 --- a/trade/datamanager/config.py +++ b/trade/datamanager/config.py @@ -31,6 +31,7 @@ class OptionDataConfig(metaclass=SingletonMetaClass): model_price: ModelPrice = ModelPrice.MIDPOINT filter_out_special_dividends: bool = True greeks_to_compute: Union[List[GreekType], GreekType] = GreekType.GREEKS + is_live: bool = False def assert_valid(self) -> None: diff --git a/trade/datamanager/dividend.py b/trade/datamanager/dividend.py index cbef24f..c15d903 100644 --- a/trade/datamanager/dividend.py +++ b/trade/datamanager/dividend.py @@ -21,7 +21,7 @@ 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.vars import get_times_series, get_dm_gen_path, load_name from trade.datamanager.config import OptionDataConfig from trade.datamanager.result import DividendsResult from trade.datamanager.base import BaseDataManager, CacheSpec @@ -41,8 +41,8 @@ TS = get_times_series() DIV_TEMP_CACHE = CustomCache( - location=DM_GEN_PATH.as_posix(), fname="dividend_temp_cache", expire_days=1, clear_on_exit=True - ) + location=get_dm_gen_path().as_posix(), fname="dividend_temp_cache", expire_days=1, clear_on_exit=True +) class DividendDataManager(BaseDataManager): """Manages dividend data retrieval, caching, and schedule construction for a specific symbol. diff --git a/trade/datamanager/market_data_helpers/dividends.py b/trade/datamanager/market_data_helpers/dividends.py index 5e01f76..5ab2398 100644 --- a/trade/datamanager/market_data_helpers/dividends.py +++ b/trade/datamanager/market_data_helpers/dividends.py @@ -7,7 +7,7 @@ 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.datamanager.vars import get_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 @@ -25,7 +25,7 @@ class SavedDividendsResult: ## 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 + location=get_dm_gen_path().as_posix(), 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.""" diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index c0d7806..2d85c23 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -170,6 +170,7 @@ def _guard_rail_dates( dates: Optional[list] = None, ) -> Tuple[datetime, datetime]: """Ensures start_date and end_date are within min_trade_date and max_trade_date.""" + if start_date < min_trade_date: logger.warning( f"Requested start_date {start_date.date()} is before available data. Adjusting to {min_trade_date.date()}." @@ -191,7 +192,8 @@ def _guard_rail_dates( f"Adjusted start_date {start_date.date()} is not in available dates. Adjusting to nearest available date." ) start_date = min(available_dates, key=lambda d: abs(d - start_date)) - if end_date not in available_dates: + end_is_not_today = end_date.date() != ny_now().date() + if end_date not in available_dates and end_is_not_today: logger.warning( f"Adjusted end_date {end_date.date()} is not in available dates. Adjusting to nearest available date." ) @@ -214,6 +216,7 @@ def _compute_max_allowable() -> pd.Timestamp: max_allowed_today = prev_busday if is_market_hrs else today else: max_allowed_today = today + return min(timestamp_exp, pd.Timestamp(max_allowed_today.date())) # For expired contracts, expiration day is the latest safe upper bound diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index 78099cf..29e0205 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -23,6 +23,15 @@ 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 get_dm_gen_path(is_live: bool = None) -> Path: + from .config import OptionDataConfig + if is_live is None: + is_live = OptionDataConfig().is_live + + global DM_GEN_PATH + if not DM_GEN_PATH.exists(): + DM_GEN_PATH.mkdir(parents=True) + return DM_GEN_PATH if not is_live else DM_GEN_PATH / "live" def set_times_series()-> "MarketTimeseries": from trade.datamanager.market_data import MarketTimeseries From 077ecacbf1bfc2ab2271b47a0ec8b4e7d5808a62 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Tue, 12 May 2026 06:13:00 -0400 Subject: [PATCH 54/81] docs(pnl_monitor): add comprehensive module docstring aligned with limits cog style --- .../riskmanager/position/cogs/pnl_monitor.py | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 9cff927..9448f43 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -1,3 +1,68 @@ +"""PnL monitoring cog for profit-locking and risk-off actions. + +Provides a position-level monitoring cog that evaluates realized/unrealized +profit and loss as part of the PositionAnalyzer pipeline, then emits +actionable opinions (HOLD, CLOSE, ROLL) using rule-based thresholds. + +Core Classes: + PnLMonitorCog: Applies PnL-driven management rules to open positions. + +Core Functions: + on_new_order_request: Re-scales requested tick cash when profitable. + _analyze_impl: Evaluates per-position PnL percent and produces actions. + _get_current_close_open_quantity: Aggregates BUY vs SELL/EXERCISE flow. + _has_closed_before: Detects whether a trade has partial close history. + +Processing Flow: + 1. Receive a position analysis context from PositionAnalyzer. + 2. Compute per-position PnL ratio: pnl / (entry_price * quantity). + 3. Apply rule set in priority order: + - Take-profit partial close when PnL ratio > 50% and quantity > 1. + - Roll when PnL ratio > 100% and roll criteria are satisfied. + - Optional stop-loss close when PnL ratio <= -70%. + - Otherwise HOLD. + 4. Stamp analysis metadata (date/reason/effective_date) on actions. + 5. Return CogActions with the resulting opinions. + +Risk/Assumptions: + - PnL thresholds are static and percentage-based. + - Effective dates are delayed by business days using t_plus_n. + - Stop-loss branch is disabled by default (enable_stop_loss=False). + - Quantity adjustments assume position quantities are integer-like. + - Trade history interpretation relies on direction labels: + BUY, SELL, EXERCISE. + +Rule Summary: + Profit lock (partial close): + If pnl_ratio > 0.5 and quantity > 1, close approximately half. + + Roll for large gains: + If pnl_ratio > 1.0 and either: + - quantity == 1, or + - the trade has prior close activity and quantity > 1, + then emit ROLL. + + Stop-loss (optional): + If pnl_ratio <= -0.7 and stop loss is enabled, close fully. + +Order Request Tick-Cash Adjustment: + During on_new_order_request, when symbol_total_pnl > 0: + - Normalize tick_cash to scaled dollars. + - Remove embedded pnl component from tick_cash. + - Add back 25% of pnl to lock profits while resizing. + - Mark is_tick_cash_scaled=True. + +Usage: + >>> cog = PnLMonitorCog() + >>> # Analyzer invokes this internally with PositionAnalysisContext + >>> # actions = cog.analyze(context) + +Notes: + - This cog is opinion-generating; execution remains downstream. + - Action reasons and verbose_info are populated for observability. + - Configuration defaults come from PnlMonitorConfig. +""" + from typing import Tuple, Optional import math import pandas as pd From 0b0a14a27f03a4b4fbcdd8cb99ee8c086fdf52ab Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Tue, 12 May 2026 06:14:25 -0400 Subject: [PATCH 55/81] docs(pnl_monitor): add comprehensive class and method docstrings --- .../riskmanager/position/cogs/pnl_monitor.py | 111 +++++++++++++++--- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 9448f43..e32d8c8 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -81,13 +81,35 @@ class PnLMonitorCog(BaseCog): - """ - Cog to monitor the PnL of open positions and trigger alerts or actions based on predefined thresholds. + """PnL-aware position management cog. + + This cog converts position-level PnL states into risk-manager opinions. It is + designed to run inside the position analysis cycle and produce deterministic + action proposals based on configurable threshold rules: + + 1. Partial profit-taking (CLOSE part of the position). + 2. Profit-protecting roll logic (ROLL the position). + 3. Optional stop-loss behavior (full CLOSE). + 4. HOLD when no trigger is met. + + In addition, it can adjust incoming order request capital (`tick_cash`) when + symbol-level PnL is positive, so downstream sizing preserves locked profits. """ default_config = PnlMonitorConfig() def __init__(self, config: Optional[PnlMonitorConfig] = None): + """Initialize the PnL monitor cog. + + Args: + config: Optional runtime configuration. If not provided, a default + `PnlMonitorConfig` is created. + + Notes: + - `enable_stop_loss` is initialized to ``False`` so the stop-loss + branch is opt-in. + - `default_config` remains available for framework-level defaults. + """ if config is None: config = PnlMonitorConfig() super().__init__(config) @@ -95,14 +117,35 @@ def __init__(self, config: Optional[PnlMonitorConfig] = None): self.enable_stop_loss = False # Enable stop loss by default def on_new_position(self, new_position_state: NewPositionState) -> None: - """ - Don't do anything on new position. + """Handle a newly created position state. + + Args: + new_position_state: Newly created position container provided by the + risk manager workflow. + + Returns: + None. This cog intentionally performs no mutation at creation time; + PnL-based decisions are deferred to analysis cycles. """ pass def on_new_order_request(self, new_request_state: OrderRequest) -> None: - """ - Don't do anything on new order request. + """Adjust request-level cash when current symbol PnL is positive. + + Process: + 1. Read `symbol_total_pnl` and request `tick_cash`. + 2. Normalize `tick_cash` into scaled-dollar form if required. + 3. If PnL > 0, remove embedded PnL from tick cash to avoid + double-counting. + 4. Add back 25% of PnL as incremental allocation. + 5. Persist updated `tick_cash` and mark `is_tick_cash_scaled=True`. + + Args: + new_request_state: Incoming order request that may be resized prior + to downstream execution and sizing. + + Returns: + None. Updates are applied in place on `new_request_state`. """ logger.info( f"Received new order request for {new_request_state.symbol} with signal ID {new_request_state.signal_id}. Monitoring PnL for this request." @@ -138,9 +181,28 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: ) def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: - """ - Analyze the current position context and return any actions to be taken. - For this PnL monitor, we will not take any specific actions, but we could log or trigger alerts if needed. + """Run PnL rule evaluation and return action opinions. + + Process: + 1. Iterate over open position states in the portfolio. + 2. Compute per-position PnL percentage: + ``pl_pct = pnl / (entry_price * quantity)`` with zero guards. + 3. Apply decision rules in order: + - ``pl_pct > 0.5`` and quantity > 1 -> partial CLOSE. + - ``pl_pct > 1.0`` and roll criteria pass -> ROLL. + - ``pl_pct <= -0.7`` with stop-loss enabled -> full CLOSE. + - otherwise -> HOLD. + 4. Attach analysis metadata (`analysis_date`, `reason`, + `verbose_info`, `effective_date`) to generated actions. + 5. Return aggregated `CogActions` for downstream arbitration. + + Args: + portfolio_context: Portfolio snapshot and metadata for the current + analysis date, including positions and backtest parameters. + + Returns: + CogActions: Opinion bundle generated by this cog for the current + analysis cycle. """ # logger.info(f"PnLMonitor received position analysis context {portfolio_context}. Analyzing PnL for open positions.") opinions = [] @@ -221,8 +283,18 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction ) def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[int, int]: - """ - Helper method to aggregate the total open and close quantities for a position based on its trade entries. + """Aggregate opened and closed quantity from trade entries. + + Args: + pos_state: Position state whose trade ledger entries are inspected. + + Returns: + Tuple[int, int]: + - open quantity from BUY entries + - close quantity from SELL/EXERCISE entries + + Notes: + Empty or missing trade ledgers return ``(0, 0)``. """ entries = pos_state.trades.entries() if pos_state.trades is not None else pd.DataFrame() if entries.empty: @@ -236,11 +308,20 @@ def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[in return open_qty, close_qty def _has_closed_before(self, trade_id: str, portfolio_context: PositionState) -> bool: - """ - Helper method to check if a position has been closed before based on its trade ID. + """Determine whether the position has prior close activity. + + Args: + trade_id: Trade identifier associated with the position. + portfolio_context: Position state containing the trade ledger. + + Returns: + bool: ``True`` if at least one SELL/EXERCISE entry exists after at + least one BUY entry; otherwise ``False``. - True if there has been at least one close transaction (SELL or EXERCISE) for the given trade ID, False otherwise. - False if there are no transactions for the trade ID or if all transactions are BUY. + Notes: + `trade_id` is accepted for API clarity and logging consistency with + caller logic, while the computation currently relies on the provided + `portfolio_context` entries. """ open_qty, close_qty = self._get_current_close_open_quantity(portfolio_context) if open_qty == 0: From e41d7726642eb2c995834b1804980b232cd7fd8d Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 17 May 2026 22:28:44 -0400 Subject: [PATCH 56/81] feat(backtester): propagate tplusn and track signal ids through execution --- trade/backtester_/_multi_asset_strategy.py | 7 +- trade/backtester_/_strategy.py | 116 +++++++++++++++++---- 2 files changed, 102 insertions(+), 21 deletions(-) diff --git a/trade/backtester_/_multi_asset_strategy.py b/trade/backtester_/_multi_asset_strategy.py index 1ed080c..3ff788a 100644 --- a/trade/backtester_/_multi_asset_strategy.py +++ b/trade/backtester_/_multi_asset_strategy.py @@ -10,7 +10,8 @@ def _setup_strategy( data: PTDataset, start_date: str, ticker: str, - params: Dict[str, Any] + params: Dict[str, Any], + tplusn: Optional[int] = 0 ) -> StrategyBase: """ Utility function to initialize a strategy instance with the given parameters. @@ -31,9 +32,8 @@ def _setup_strategy( for key, value in default_params.items(): if key not in params: params[key] = value - return strategy_class( - data=data, start_trading_date=start_date, ticker=ticker, **params + data=data, start_trading_date=start_date, ticker=ticker, tplusn=tplusn, **params ) @dataclass @@ -153,6 +153,7 @@ def __post_init__(self): data=self.data[ticker], start_date=self.start_date, ticker=ticker, + tplusn=self.tplusn, params=ticker_params ) self.current_open_positions[ticker] = False diff --git a/trade/backtester_/_strategy.py b/trade/backtester_/_strategy.py index 64d734c..c467f74 100644 --- a/trade/backtester_/_strategy.py +++ b/trade/backtester_/_strategy.py @@ -42,8 +42,6 @@ def __post_init__(self): self.values = self.series.to_numpy(copy=False) - - @dataclass class TradeDecision: ok: bool @@ -66,7 +64,11 @@ def __post_init__(self): self.signal_id = SignalID.parse(self.signal_id) except Exception as e: raise ValueError(f"Invalid signal_id string: {self.signal_id}") from e - raise TypeError("TradeDecision.signal_id must be of type SignalID, 'N/A', or None. Received type: {}".format(type(self.signal_id))) + raise TypeError( + "TradeDecision.signal_id must be of type SignalID, 'N/A', or None. Received type: {}".format( + type(self.signal_id) + ) + ) if self.pos_effect is not None and not isinstance(self.pos_effect, PositionEffect): raise TypeError("TradeDecision.pos_effect must be of type PositionEffect or None.") @@ -331,7 +333,7 @@ def position_open(self, value: bool): raise AttributeError( "position_open is a read-only property. To change position status, use open_action() and close_action() methods which handle state updates and validations." ) - + def additional_on_entry_info(self, *, date: pd.Timestamp = None, index: Optional[int] = None) -> Dict[str, Any]: """ Override this method in your strategy subclass to provide additional information to be stored in PositionInfo when a position is opened. @@ -342,7 +344,7 @@ def additional_on_entry_info(self, *, date: pd.Timestamp = None, index: Optional Dict[str, Any]: A dictionary of additional information to include in PositionInfo. This will be merged with the default fields when a position is opened. """ return {} - + def additional_on_exit_info(self, *, date: pd.Timestamp = None, index: Optional[int] = None) -> Dict[str, Any]: """ Override this method in your strategy subclass to provide additional information to be stored or logged when a position is closed. @@ -354,6 +356,39 @@ def additional_on_exit_info(self, *, date: pd.Timestamp = None, index: Optional[ """ return {} + def get_bt_params(self) -> Dict[str, Any]: + """ + Return the current backtest parameter values for this strategy instance. + + The returned mapping uses the keys declared on the subclass ``bt_params`` + contract and resolves each value from ``self`` via ``getattr`` so the result + reflects the current instance state. If any declared parameter is missing, + this raises an error immediately. + + Returns: + Dict[str, Any]: Current backtest parameter values keyed by ``bt_params``. + + Raises: + TypeError: If ``bt_params`` is not a dictionary. + AttributeError: If any declared backtest parameter is missing on the instance. + """ + bt_params = getattr(type(self), "bt_params", None) + if not isinstance(bt_params, dict): + raise TypeError(f"{type(self).__name__}.bt_params must be a dict.") + + current_params: Dict[str, Any] = {} + missing_keys: List[str] = [] + for key in bt_params.keys(): + if not hasattr(self, key): + missing_keys.append(key) + continue + current_params[key] = getattr(self, key) + + if missing_keys: + raise AttributeError(f"{type(self).__name__} is missing backtest parameter attribute(s): {missing_keys}.") + + return current_params + def _resolve(self, *, date: pd.Timestamp = None, index: int = None) -> Tuple[int, pd.Timestamp]: """ Resolve date or index into a validated (index, timestamp) tuple. @@ -424,9 +459,9 @@ def _sanitize_data(self) -> None: ) self._df.columns = self._df.columns.str.lower() - assert set(self._df.columns).issuperset( - {"open", "high", "low", "close", "volume"} - ), "Data must contain open, high, low, close, volume columns." + assert set(self._df.columns).issuperset({"open", "high", "low", "close", "volume"}), ( + "Data must contain open, high, low, close, volume columns." + ) @abstractmethod def setup(self) -> None: @@ -799,7 +834,9 @@ def add_indicator(self, name: str, series: pd.Series, overlay: bool = False, col self.add_indicator('SMA_20', sma_20, overlay=True, color='blue') """ # assumes you have an Indicator class somewhere - self.indicators[name] = Indicator(name=name, series=series, overlay=overlay, color=color, values=series.to_numpy(copy=False)) + self.indicators[name] = Indicator( + name=name, series=series, overlay=overlay, color=color, values=series.to_numpy(copy=False) + ) def get_indicator(self, name: str) -> Any: """ @@ -924,6 +961,7 @@ def simulate( current_price = float(close[i]) in_pos_prev = self.position_open position_side = self.position_side + # print(f"Index: {i}, Date: {ts}, Should Close: {self.should_close(index=i)}, Should Open: {self.should_open(index=i)} ") # 1) Apply return for the interval prev->current based on prior position state if i > 0 and in_pos_prev: @@ -944,7 +982,16 @@ def simulate( index=i, side=op.get("side"), signal_id=op.get("signal_id"), entry_price=current_price ) entry_price = current_price - trades.append({"date": ts, "action": "open", "price": current_price, "equity": eq, **self.additional_on_entry_info(index=i)}) + trades.append( + { + "date": ts, + "action": "open", + "price": current_price, + "equity": eq, + "signal_id": op.get("signal_id"), + **self.additional_on_entry_info(index=i), + } + ) elif op.get("action") == "close": ## Optionally enforce that the close signal is still valid at execution time (e.g., if tplusn > 0, the market conditions may have changed) @@ -963,6 +1010,7 @@ def simulate( "entry_price": entry_price, "side": position_side, "position_info": position_info, + "signal_id": position_info.signal_id if position_info else None, **self.additional_on_exit_info(index=i), } ) @@ -970,6 +1018,7 @@ def simulate( # 3) Check for new signals at t and schedule (or execute immediately if tn==0) open_decision = self.should_open(index=i) + close_decision = self.should_close(index=i) if open_decision.ok: exec_idx = i if tn_int == 0 else min(i + tn_int, n - 1) if tn_int == 0: @@ -982,7 +1031,16 @@ def simulate( entry_price=current_price, ) entry_price = current_price - trades.append({"date": ts, "action": "open", "price": current_price, "equity": eq, **self.additional_on_entry_info(index=i)}) + trades.append( + { + "date": ts, + "action": "open", + "price": current_price, + "equity": eq, + "signal_id": open_decision.signal_id, + **self.additional_on_entry_info(index=i), + } + ) else: pending.setdefault(exec_idx, []).append( { @@ -993,7 +1051,7 @@ def simulate( } ) - elif self.should_close(index=i): + elif close_decision.ok: exec_idx = i if tn_int == 0 else min(i + tn_int, n - 1) if tn_int == 0: if self.position_open: @@ -1010,11 +1068,13 @@ def simulate( "entry_price": entry_price, "side": position_side, "position_info": position_info, + "signal_id": position_info.signal_id if position_info else None, **self.additional_on_exit_info(index=i), } ) self.close_action(index=exec_idx) else: + print(f"Scheduling close at index {exec_idx} for signal at index {i}, date {ts}") pending.setdefault(exec_idx, []).append({"action": "close", "signal_index": i}) equity[i] = eq @@ -1037,6 +1097,7 @@ def simulate( "entry_price": entry_price, "side": self.position_side, "position_info": position_info, + "signal_id": position_info.signal_id if position_info else None, **self.additional_on_exit_info(index=n - 1), } ) @@ -1048,7 +1109,7 @@ def simulate( ## Build trades into a DataFrame for easier analysis (optional) return trades, equity_series - + def _convert_trades_to_df(self, trades: List[Dict[str, Any]]) -> pd.DataFrame: """ Convert the list of trade dictionaries into a pandas DataFrame for easier analysis. @@ -1060,7 +1121,7 @@ def _convert_trades_to_df(self, trades: List[Dict[str, Any]]) -> pd.DataFrame: """ if not trades: return pd.DataFrame() # return empty DataFrame if no trades - + ## convert list of dicts into list of two sequential dicts for open/close pairs, then build DataFrame trade_records = [] random_date = trades[0]["date"] if trades else None @@ -1075,6 +1136,7 @@ def _convert_trades_to_df(self, trades: List[Dict[str, Any]]) -> pd.DataFrame: "entry_equity": open_trade["equity"], "return_pct": None, "side": open_trade.get("side"), + "signal_id": open_trade.get("signal_id"), } if close_trade and close_trade["action"] == "close": record.update( @@ -1093,7 +1155,6 @@ def _convert_trades_to_df(self, trades: List[Dict[str, Any]]) -> pd.DataFrame: record.update({f"exit_{k}": close_trade.get(k) for k in exit_info_keys}) trade_records.append(record) return pd.DataFrame(trade_records).set_index("entry_date") - def plot_strategy_indicators(self, log_scale: bool = True, add_signal_marker: bool = True) -> go.Figure: """ @@ -1321,9 +1382,9 @@ def setup(self) -> None: average_type=self.average_type, ) - def open_action(self, *, signal_id = None, entry_price = None, side = None, date = None, index = None): + def open_action(self, *, signal_id=None, entry_price=None, side=None, date=None, index=None): idx, _ = self._resolve(date=date, index=index) - + if self.is_long: self.stop = update_atr_trail_long( close=float(self.close[idx]), @@ -1337,4 +1398,23 @@ def open_action(self, *, signal_id = None, entry_price = None, side = None, date loss=float(self.loss_series[idx]), prev_trail=self.stop, reset=False, - ) \ No newline at end of file + ) + + def close_action(self, *, date=None, index=None): + self.stop = None + + def is_close_signal(self, *, date=None, index=None) -> bool: + if self.is_long: + self.stop = update_atr_trail_long( + close=float(self.close[index]), + loss=float(self.loss_series[index]), + prev_trail=self.stop, + reset=False, + ) + elif self.is_short: + self.stop = update_atr_trail_short( + close=float(self.close[index]), + loss=float(self.loss_series[index]), + prev_trail=self.stop, + reset=False, + ) From ca3172cd476dfd9b4ec442c24aa6b3f0b31f8afa Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 17 May 2026 22:28:48 -0400 Subject: [PATCH 57/81] fix(riskmanager): avoid caching same-day chain snapshots --- EventDriven/riskmanager/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index ffe2d3f..4febbb4 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -470,8 +470,9 @@ def populate_cache_with_chain( if PATCH_TICKERS: chain_clipped["Root"] = chain_clipped["Root"].apply(swap_ticker) - - _PERSISTENT_CACHE[key] = chain_clipped ## Cache the chain data to avoid redundant API calls in the future + + if (pd.to_datetime(date)).date() != datetime.now().date(): + _PERSISTENT_CACHE[key] = chain_clipped ## Cache the chain data to avoid redundant API calls in the future chain_clipped.columns = chain_clipped.columns.str.capitalize() ## Create ID From 63533eadd2d89c991b7fd3a8a8f185bbd379b421 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 17 May 2026 22:28:54 -0400 Subject: [PATCH 58/81] style(riskmanager): remove extra blank lines in pnl monitor cog --- EventDriven/riskmanager/position/cogs/pnl_monitor.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index e32d8c8..b2cf104 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -77,9 +77,6 @@ logger = setup_logger("EventDriven.riskmanager.position.cogs.pnl_monitor", stream_log_level="INFO") - - - class PnLMonitorCog(BaseCog): """PnL-aware position management cog. From 2fc57e4838e5621a9d80cc6024fc3ceb5f859006 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 18 May 2026 20:24:16 -0400 Subject: [PATCH 59/81] Stuff --- .github/skills/tfp-db/SKILL.md | 168 ++++ trade/backtester_/__init__.py | 17 + .../option_vectorized_pnl_runner.py | 267 +++++++ .../option_vectorized_retrieval.py | 734 ++++++++++++++++++ trade/datamanager/market_data.py | 1 - .../test_option_vectorized_pnl_runner.py | 141 ++++ .../tests/test_option_vectorized_retrieval.py | 310 ++++++++ 7 files changed, 1637 insertions(+), 1 deletion(-) create mode 100644 .github/skills/tfp-db/SKILL.md create mode 100644 trade/backtester_/option_vectorized_pnl_runner.py create mode 100644 trade/backtester_/option_vectorized_retrieval.py create mode 100644 trade/tests/test_option_vectorized_pnl_runner.py create mode 100644 trade/tests/test_option_vectorized_retrieval.py diff --git a/.github/skills/tfp-db/SKILL.md b/.github/skills/tfp-db/SKILL.md new file mode 100644 index 0000000..dc47b54 --- /dev/null +++ b/.github/skills/tfp-db/SKILL.md @@ -0,0 +1,168 @@ +--- +name: tfp-db +description: >- + Guides MySQL access for the FinanceDatabase stack: multi-database layout, + prod vs test environments, master_config.database_configs registry, base_name + resolution, and safe read-only querying via MCP. Use when working with + FinanceDatabase, dbase/database, portfolio_data, master_config, environment + cloning, TFP-Algo database context, or MCP mysql_query against this server. +--- + +# TFP Database (FinanceDatabase MySQL) + +## When to use this skill + +Apply before any database exploration, SQL writing, or MCP `mysql_query` work tied to **FinanceDatabase** or **TFP-Algo**. Do not assume a single database or that schema names match Python `base_name` constants without checking the registry. + +## Architecture in one paragraph + +One MySQL **server** hosts many **databases** (MySQL schemas). Application code uses **base names** (e.g. `portfolio_data`). The **physical** schema name depends on **environment**: prod uses the base name; test/dev uses suffixed names registered in `master_config.database_configs`. Python resolves names via `get_database_name()` in `dbase/database/db_utils.py` after `set_environment_context()`. **MCP has no environment context** — you must use **physical** `database_name` values from the registry (or ask the user which environment). + +## Logical databases (base names) + +Constants in `dbase/database/db_utils.py` → class `Database`: + +| Base name | Typical role | +|-----------|----------------| +| `portfolio_config` | Portfolio configuration | +| `portfolio_data` | Core portfolio / trades data | +| `strategy_trades_signals` | Strategy trades & signals | +| `portfolio_signals` | Portfolio signals | +| `vol_surface` | Volatility surface | +| `securities_master` | Securities reference data | +| `master_config` | **Registry** — never suffixed; always `master_config` | + +**Excluded from cloning** (do not treat as app DBs): `information_schema`, `mysql`, `performance_schema`, `sys`. `master_config` is special (registry only). + +## Environment model + +### How Python picks an environment + +Priority (`get_environment()` in `db_utils.py`): + +1. CLI `--env` argument → environment string (often `test-{name}`) +2. Git branch: `main` → `prod`; any other branch → `test` +3. `ENVIRONMENT` env var +4. Default → `prod` + +Runtime context: `set_environment_context(environment, branch_name)` (usually from TFP-Algo `runner.py`). + +### How physical names are resolved + +| Environment | Physical DB name | +|-------------|------------------| +| `prod` | Same as `base_name` (e.g. `portfolio_data`) | +| Non-prod (e.g. `test`, `test-mean-reversion`) | Row in `master_config.database_configs` for `(base_name, environment)` | + +`master_config` is **never** suffixed. + +Common test pattern: `{base_name}_{environment}` (e.g. `portfolio_data_test-mean-reversion`) — **always confirm** via the registry; do not guess suffixes. + +## Source of truth: `master_config.database_configs` + +**This table maps environments to physical databases — not tables.** + +Known columns (from `db_management.py`): + +| Column | Meaning | +|--------|---------| +| `database_name` | Physical MySQL schema name (use in SQL) | +| `base_name` | Logical name (`portfolio_data`, etc.) | +| `environment` | e.g. `prod`, `test`, `test-mean-reversion` | +| `branch_name` | Optional git branch metadata | +| `is_active` | `TRUE` = environment currently uses this DB; `FALSE` = soft-deleted | +| `created_by` | Audit (often `system`) | + +### Starter queries (run first) + +**1. All active environments and databases** + +```sql +SELECT environment, base_name, database_name, branch_name, is_active +FROM master_config.database_configs +WHERE is_active = TRUE +ORDER BY environment, base_name; +``` + +**2. Resolve one base name for a specific environment** + +```sql +SELECT database_name +FROM master_config.database_configs +WHERE base_name = 'portfolio_data' + AND environment = 'prod' + AND is_active = TRUE +LIMIT 1; +``` + +**3. List environments only** + +```sql +SELECT DISTINCT environment +FROM master_config.database_configs +WHERE is_active = TRUE +ORDER BY environment; +``` + +**4. Tables inside a physical database** (after you know `database_name`) + +```sql +SELECT table_name, table_type, table_rows +FROM information_schema.tables +WHERE table_schema = 'portfolio_data' + AND table_type = 'BASE TABLE' +ORDER BY table_name; +``` + +Replace `'portfolio_data'` with the resolved physical name from step 1 or 2. + +## SQL conventions for agents + +1. **Always qualify** tables: `physical_database.table` (e.g. `portfolio_data.trades`). +2. **Confirm environment** with the user if unclear (`prod` vs a test env). Wrong env → wrong or empty data. +3. **Start from `master_config`** when exploring; never assume test DB names. +4. **Read-only by default** — MCP should use a read-only MySQL user; only `SELECT` / `SHOW` / `DESCRIBE` / safe `information_schema` queries unless the user explicitly enables writes elsewhere. +5. **LIMIT** large tables — use `information_schema.tables.table_rows` as a hint, then `SELECT ... LIMIT n`. +6. Python env vars (for reference, not MCP): `MYSQL_HOST`, `MYSQL_PORT`, `MYSQL_USER`, `MYSQL_PASSWORD`, optional `ENVIRONMENT`, `DBASE_DIR`. + +## MCP vs Python + +| Python (`dbase/database`) | MCP (`mysql_query`) | +|---------------------------|---------------------| +| `get_database_name('portfolio_data')` | Query `master_config.database_configs` | +| `set_environment_context(...)` | Ask user or infer from task | +| `get_engine(db)` auto-resolves | Use physical `database_name` in SQL | +| Multi-DB via context | Omit `MYSQL_DB`; use qualified names | + +## Environment management (context only) + +Implemented in `dbase/database/db_management.py` (CLI: `python -m dbase.database.db_management`): + +- **create** — clone prod (or source) schemas into a new test environment; registers rows in `database_configs` +- **list** — environments from registry +- **delete** — drops test DBs; soft-deletes registry rows; **`prod` cannot be deleted** +- **diff / sync** — compare environments; sync is **dry-run by default** (`--apply` to change) + +Agents should **not** run destructive CLI unless the user explicitly requests it. + +## Common mistakes to avoid + +- Treating `database_configs` as a table list → it lists **databases**, not tables. +- Querying `portfolio_data` when the user is on a feature branch that uses `portfolio_data_test-...`. +- Forgetting `is_active = TRUE` (includes retired environments). +- Using unqualified `SELECT * FROM trades` in multi-DB mode. +- Assuming MCP write guards replace a read-only MySQL user (they do not). + +## Workflow checklist + +1. Run registry query (all active configs) or ask user for **environment**. +2. Map each needed `base_name` → `database_name`. +3. List tables per physical DB via `information_schema` if needed. +4. Run targeted `SELECT` with qualified names and `LIMIT`. +5. If zero rows / missing table, re-check environment and physical name before debugging schema. + +## Code references + +- Registry query: `dbase/database/db_utils.py` → `_load_database_name_from_config` +- Environment helpers: `dbase/database/db_management.py` → `get_databases_for_environment`, `get_tables_for_database` +- Docs: `dbase/database/README.md` diff --git a/trade/backtester_/__init__.py b/trade/backtester_/__init__.py index e69de29..7d1d79e 100644 --- a/trade/backtester_/__init__.py +++ b/trade/backtester_/__init__.py @@ -0,0 +1,17 @@ +from trade.backtester_.option_vectorized_retrieval import ( + OptionRetrievalSignal, + OptionVectorizedRetriever, + OptionVectorizedRetrievalResult, +) +from trade.backtester_.option_vectorized_pnl_runner import ( + OptionVectorizedPnLRunner, + OptionVectorizedPnLRunResult, +) + +__all__ = [ + "OptionRetrievalSignal", + "OptionVectorizedRetriever", + "OptionVectorizedRetrievalResult", + "OptionVectorizedPnLRunner", + "OptionVectorizedPnLRunResult", +] diff --git a/trade/backtester_/option_vectorized_pnl_runner.py b/trade/backtester_/option_vectorized_pnl_runner.py new file mode 100644 index 0000000..9c9cd4a --- /dev/null +++ b/trade/backtester_/option_vectorized_pnl_runner.py @@ -0,0 +1,267 @@ +"""Option retrieval to PnL orchestration for vectorized backtests. + +Runs leg-level option PnL decomposition from retrieval outputs, then aggregates +results to trade, signal, and portfolio levels with a normalized equity curve. + +Core Dataclasses: + OptionVectorizedPnLRunResult: Output payload from runner execution. + +Core Functions: + OptionVectorizedPnLRunner.run: End-to-end retrieval->PnL->aggregation flow. + +Processing Flow: + 1. Map selected contracts to optticks. + 2. Run load_option_pnl_data per trade segment. + 3. Remove trade-adjustment effects per configuration. + 4. Aggregate daily attribution at trade/signal/portfolio levels. + 5. Build normalized portfolio equity curve. +""" + +from dataclasses import dataclass +from typing import Dict, List, Any + +import pandas as pd + +from trade.assets.calculate.xmultiply_attr_v2 import load_option_pnl_data +from trade.backtester_.option_vectorized_retrieval import OptionVectorizedRetrievalResult +from trade.helpers.helper import generate_option_tick_new as generate_opttick_new, to_datetime + + +@dataclass +class OptionVectorizedPnLRunResult: + """Output container for option retrieval-to-PnL execution.""" + + trade_daily: pd.DataFrame + trade_summary: pd.DataFrame + signal_daily: pd.DataFrame + portfolio_daily: pd.DataFrame + portfolio_equity_curve: pd.DataFrame + failures: pd.DataFrame + + +class OptionVectorizedPnLRunner: + """Runner that computes trade/signal/portfolio PnL from retrieval outputs.""" + + def __init__( + self, + *, + starting_nav: float = 1.0, + fail_fast: bool = False, + remove_trade_pnl_adjustment: bool = True, + ) -> None: + if starting_nav <= 0: + raise ValueError("starting_nav must be positive.") + self.starting_nav = float(starting_nav) + self.fail_fast = fail_fast + self.remove_trade_pnl_adjustment = remove_trade_pnl_adjustment + + def run(self, retrieval_result: OptionVectorizedRetrievalResult) -> OptionVectorizedPnLRunResult: + """Execute decomposition and aggregations from retrieval result. + + Args: + retrieval_result: Output from OptionVectorizedRetriever.run. + + Returns: + OptionVectorizedPnLRunResult with trade/signal/portfolio tables. + """ + selected = retrieval_result.selected_contracts.copy() + if selected.empty: + empty = pd.DataFrame() + return OptionVectorizedPnLRunResult( + trade_daily=empty, + trade_summary=empty, + signal_daily=empty, + portfolio_daily=empty, + portfolio_equity_curve=empty, + failures=empty, + ) + + required_cols = { + "signal_id", + "ticker", + "right", + "strike", + "expiration", + "roll_index", + "segment_start", + "segment_end", + } + missing = required_cols - set(selected.columns) + if missing: + raise ValueError(f"selected_contracts missing required columns: {sorted(missing)}") + + selected = selected.sort_values(["signal_id", "roll_index"]).reset_index(drop=True) + + trade_daily_frames: List[pd.DataFrame] = [] + trade_summary_rows: List[Dict[str, Any]] = [] + failures: List[Dict[str, Any]] = [] + + for row in selected.to_dict(orient="records"): + signal_id = str(row["signal_id"]) + roll_index = int(row["roll_index"]) + trade_id = f"{signal_id}__roll_{roll_index}" + + try: + self._validate_segment_presence(retrieval_result, signal_id=signal_id, roll_index=roll_index) + start_dt = to_datetime(str(row["segment_start"])) + end_dt = to_datetime(str(row["segment_end"])) + if end_dt < start_dt: + raise ValueError("segment_end is earlier than segment_start") + + opttick = generate_opttick_new( + symbol=str(row["ticker"]), + right=str(row["right"]), + exp=to_datetime(str(row["expiration"])).strftime("%Y-%m-%d"), + strike=float(row["strike"]), + ) + print(f"Processing trade_id={trade_id} with opttick={opttick} for segment {start_dt.date()} to {end_dt.date()}") + payload = load_option_pnl_data( + yesterday=start_dt, + today=end_dt, + opttick=opttick, + ) + print(f" Loaded attribution data with {len(payload.attribution)} rows and columns: {payload.attribution.columns.tolist()}") + attribution = pd.DataFrame(payload.attribution).copy() + if attribution.empty: + raise ValueError("empty attribution from load_option_pnl_data") + + attribution.index = pd.to_datetime(attribution.index) + attribution = attribution[(attribution.index >= start_dt) & (attribution.index <= end_dt)] + if attribution.empty: + raise ValueError("attribution empty after segment clipping") + + attribution = self._normalize_trade_adjustment_columns(attribution) + + attribution["signal_id"] = signal_id + attribution["trade_id"] = trade_id + attribution["roll_index"] = roll_index + attribution["ticker"] = str(row["ticker"]) + attribution["opttick"] = opttick + attribution["segment_start"] = start_dt.strftime("%Y-%m-%d") + attribution["segment_end"] = end_dt.strftime("%Y-%m-%d") + attribution["date"] = attribution.index + + trade_daily_frames.append(attribution.reset_index(drop=True)) + trade_summary_rows.append(self._build_trade_summary_row(attribution=attribution)) + except Exception as exc: + failures.append( + { + "signal_id": signal_id, + "roll_index": roll_index, + "trade_id": trade_id, + "reason": str(exc), + } + ) + if self.fail_fast: + raise + + trade_daily = pd.concat(trade_daily_frames, ignore_index=True) if trade_daily_frames else pd.DataFrame() + trade_summary = pd.DataFrame(trade_summary_rows) if trade_summary_rows else pd.DataFrame() + failures_df = pd.DataFrame(failures) if failures else pd.DataFrame() + + signal_daily = self._aggregate_daily(trade_daily=trade_daily, by=["date", "signal_id"]) + portfolio_daily = self._aggregate_daily(trade_daily=trade_daily, by=["date"]) + equity_curve = self._build_normalized_equity_curve(portfolio_daily=portfolio_daily) + + return OptionVectorizedPnLRunResult( + trade_daily=trade_daily, + trade_summary=trade_summary, + signal_daily=signal_daily, + portfolio_daily=portfolio_daily, + portfolio_equity_curve=equity_curve, + failures=failures_df, + ) + + @staticmethod + def _validate_segment_presence( + retrieval_result: OptionVectorizedRetrievalResult, + *, + signal_id: str, + roll_index: int, + ) -> None: + segments = retrieval_result.signal_ohlc.get(signal_id) + if segments is None: + raise ValueError(f"signal_ohlc missing signal_id '{signal_id}'") + if roll_index < 0 or roll_index >= len(segments): + raise ValueError(f"signal_ohlc missing segment for signal_id='{signal_id}', roll_index={roll_index}") + + def _normalize_trade_adjustment_columns(self, attribution: pd.DataFrame) -> pd.DataFrame: + """Remove trade adjustment effects and enforce total_pnl semantics.""" + out = attribution.copy() + + if self.remove_trade_pnl_adjustment and "trade_pnl_adjustment" in out.columns: + out = out.drop(columns=["trade_pnl_adjustment"]) + + if "total_pnl_excl_trade_pnl" in out.columns: + out["total_pnl"] = out["total_pnl_excl_trade_pnl"] + elif "total_pnl" not in out.columns: + numeric_cols = out.select_dtypes(include=["number"]).columns.tolist() + out["total_pnl"] = out[numeric_cols].sum(axis=1) if numeric_cols else 0.0 + + return out + + @staticmethod + def _build_trade_summary_row(attribution: pd.DataFrame) -> Dict[str, Any]: + row0 = attribution.iloc[0] + total_col = "total_pnl" if "total_pnl" in attribution.columns else "total_pnl_excl_trade_pnl" + out: Dict[str, Any] = { + "signal_id": row0["signal_id"], + "trade_id": row0["trade_id"], + "roll_index": int(row0["roll_index"]), + "ticker": row0["ticker"], + "opttick": row0["opttick"], + "segment_start": row0["segment_start"], + "segment_end": row0["segment_end"], + "n_days": int(attribution["date"].nunique()), + "total_pnl": float(attribution[total_col].sum()), + "explained_pnl": float(attribution["total_pnl_excl_trade_pnl"].sum()) + if "total_pnl_excl_trade_pnl" in attribution.columns + else float(attribution[total_col].sum()), + "unexplained_pnl": float(attribution["unexplained_pnl"].sum()) + if "unexplained_pnl" in attribution.columns + else 0.0, + } + return out + + @staticmethod + def _aggregate_daily(trade_daily: pd.DataFrame, by: List[str]) -> pd.DataFrame: + if trade_daily.empty: + return pd.DataFrame() + + ignore_cols = { + "signal_id", + "trade_id", + "ticker", + "opttick", + "segment_start", + "segment_end", + "date", + } + numeric_cols = [ + c for c in trade_daily.columns if c not in ignore_cols and pd.api.types.is_numeric_dtype(trade_daily[c]) + ] + if not numeric_cols: + grouped = trade_daily[by].drop_duplicates().copy() + return grouped.sort_values(by).reset_index(drop=True) + + grouped = trade_daily.groupby(by, as_index=False)[numeric_cols].sum() + return grouped.sort_values(by).reset_index(drop=True) + + def _build_normalized_equity_curve(self, portfolio_daily: pd.DataFrame) -> pd.DataFrame: + if portfolio_daily.empty: + return pd.DataFrame() + + date_col = "date" + if date_col not in portfolio_daily.columns: + return pd.DataFrame() + + pnl_col = "total_pnl" if "total_pnl" in portfolio_daily.columns else "total_pnl_excl_trade_pnl" + if pnl_col not in portfolio_daily.columns: + return pd.DataFrame() + + curve = portfolio_daily[[date_col, pnl_col]].copy() + curve = curve.sort_values(date_col).reset_index(drop=True) + curve["cum_pnl"] = curve[pnl_col].cumsum() + curve["equity"] = self.starting_nav + curve["cum_pnl"] + curve["normalized_equity"] = curve["equity"] / self.starting_nav + return curve[[date_col, pnl_col, "equity", "normalized_equity"]] diff --git a/trade/backtester_/option_vectorized_retrieval.py b/trade/backtester_/option_vectorized_retrieval.py new file mode 100644 index 0000000..035604d --- /dev/null +++ b/trade/backtester_/option_vectorized_retrieval.py @@ -0,0 +1,734 @@ +"""Vectorized option retrieval workflows for backtest signal windows. + +Provides a retrieval-only API that maps directional signal windows to nearest +option contracts using configurable DTE/moneyness targets, then loads full +contract OHLC time series for each matched signal. + +Core Dataclasses: + OptionRetrievalSignal: Canonical input signal for option selection. + SelectedOptionContract: Contract metadata selected for one signal. + UnmatchedSignal: Structured reason when no contract is selected. + OptionVectorizedRetrievalResult: End-to-end retrieval payload. + +Core Functions: + OptionVectorizedRetriever.run: Execute full retrieval pipeline. + OptionVectorizedRetriever.from_multi_asset_strategy: Build signals from + MultiAssetStrategy.simulate_all() output. + +Processing Flow: + 1. Normalize source inputs into canonical signal rows. + 2. Query option chains with retrieve_chain_bulk on signal start dates. + 3. Rank candidates by weighted DTE/moneyness distance. + 4. Retrieve selected contract OHLC via retrieve_eod_ohlc. + 5. Return matched/unmatched diagnostics and per-signal series. + +Risk/Assumptions: + - This module does not compute PnL or execution slippage. + - Explicit signal right takes precedence over side-derived right. + +Usage: + >>> retriever = OptionVectorizedRetriever(target_dte=30, target_moneyness=1.0, right="C") + >>> result = retriever.run(signals=[ + ... OptionRetrievalSignal( + ... ticker="AAPL", + ... start_date="2026-01-02", + ... end_date="2026-01-30", + ... side=1, + ... quantity=1.0, + ... signal_id="sig-1", + ... ) + ... ]) + >>> len(result.selected_contracts) >= 0 + True +""" + +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from typing import Optional, Union, List, Dict, Any, Tuple + +import pandas as pd + +from dbase.DataAPI.ThetaData import retrieve_chain_bulk, retrieve_eod_ohlc +from trade.backtester_._multi_asset_strategy import MultiAssetStrategy +from trade.datamanager.market_data import get_timeseries_obj +from trade.helpers.helper import to_datetime +from trade.datamanager.option_spot import OptionSpotDataManager + + +DateLike = Union[datetime, str] + + +@dataclass +class OptionRetrievalSignal: + """Canonical input signal for option retrieval. + + Args: + ticker: Underlier symbol. + start_date: Signal entry date (inclusive). + end_date: Signal exit date (inclusive). + side: Direction (+1 long, -1 short). + quantity: Signed or absolute desired quantity. + signal_id: Stable signal identifier. + right: Optional explicit option right ("C"/"P"). If provided, wins. + target_dte: Optional per-signal DTE override. + target_moneyness: Optional per-signal moneyness override. + metadata: Free-form per-signal metadata. + """ + + ticker: str + start_date: DateLike + end_date: DateLike + side: int + quantity: float + signal_id: str + right: Optional[str] = None + target_dte: Optional[int] = None + target_moneyness: Optional[float] = None + metadata: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class SelectedOptionContract: + """Selected contract metadata for one signal.""" + + signal_id: str + ticker: str + right: str + strike: float + expiration: str + chain_date: str + dte: int + moneyness: Optional[float] + score: float + roll_index: int = 0 + segment_start: Optional[str] = None + segment_end: Optional[str] = None + + +@dataclass +class UnmatchedSignal: + """Structured unmatched signal record.""" + + signal_id: str + ticker: str + reason: str + details: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class OptionVectorizedRetrievalResult: + """Output payload for vectorized option retrieval.""" + + normalized_signals: pd.DataFrame + selected_contracts: pd.DataFrame + unmatched_signals: pd.DataFrame + signal_ohlc: Dict[str, List[pd.DataFrame]] + + +class OptionVectorizedRetriever: + """Vectorized option contract retrieval and OHLC assembly. + + Exactly one source must be supplied at run-time: + - signals: Explicit list of OptionRetrievalSignal + - strategy: MultiAssetStrategy, adapted via simulate_all() + """ + + def __init__( + self, + target_dte: int, + target_moneyness: float, + right: Optional[str] = None, + *, + dte_tolerance: int = 10, + moneyness_tolerance: float = 0.2, + dte_weight: float = 1.0, + moneyness_weight: float = 1.0, + roll_enabled: bool = False, + roll_on_dte: int = 0, + end_time: str = "16:00", + print_url: bool = False, + ) -> None: + """Initialize retrieval defaults and selection controls. + + Args: + target_dte: Default DTE target used when signal override is absent. + target_moneyness: Default moneyness target used when override is absent. + right: Optional default right ("C" or "P"). + dte_tolerance: Maximum allowed absolute DTE distance. + moneyness_tolerance: Maximum allowed absolute moneyness distance. + dte_weight: Score weight for DTE distance. + moneyness_weight: Score weight for moneyness distance. + roll_enabled: If True, iteratively rolls contracts until signal end. + roll_on_dte: Roll trigger in calendar DTE days. Roll when exit is beyond + (expiration - roll_on_dte). + end_time: Chain retrieval clock time. + print_url: Pass-through debug flag for ThetaData API. + """ + if right is not None and right.upper() not in {"C", "P"}: + raise ValueError("right must be one of {'C', 'P'} when provided.") + self.target_dte = target_dte + self.target_moneyness = target_moneyness + self.right = right.upper() if right else None + self.dte_tolerance = dte_tolerance + self.moneyness_tolerance = moneyness_tolerance + self.dte_weight = dte_weight + self.moneyness_weight = moneyness_weight + self.roll_enabled = roll_enabled + self.roll_on_dte = int(roll_on_dte) + self.end_time = end_time + self.print_url = print_url + self._chain_spot_cache: Dict[str, pd.Series] = {} + + def run( + self, + *, + signals: Optional[List[OptionRetrievalSignal]] = None, + strategy: Optional[MultiAssetStrategy] = None, + include_open_positions: bool = False, + strategy_end_date: Optional[DateLike] = None, + n_size: Optional[int] = None, + ) -> OptionVectorizedRetrievalResult: + """Run the full retrieval workflow. + + Args: + signals: Explicit signal rows to process. + strategy: MultiAssetStrategy source; converted via simulate_all(). + include_open_positions: Include currently-open strategy positions. + strategy_end_date: End date used when strategy positions are still open. + n_size: Optional random sample size for the signal set. If None, all + normalized signals are processed. + Expected signal format: + Each signal must be an OptionRetrievalSignal with the following + fields: ticker, start_date, end_date, side, quantity, signal_id, + and optional right, target_dte, target_moneyness, metadata. The + start_date and end_date values may be YYYY-MM-DD strings or + datetime objects. + + Returns: + OptionVectorizedRetrievalResult with selected contracts and OHLC payloads. + """ + normalized_signals = self._build_signals( + signals=signals, + strategy=strategy, + include_open_positions=include_open_positions, + strategy_end_date=strategy_end_date, + ) + if n_size is not None: + if n_size <= 0: + raise ValueError("n_size must be a positive integer when provided.") + if n_size < len(normalized_signals): + normalized_signals = normalized_signals.sample(n=n_size).reset_index(drop=True) + self._prime_chain_spot_cache(normalized_signals=normalized_signals) + + selected: List[SelectedOptionContract] = [] + unmatched: List[UnmatchedSignal] = [] + signal_ohlc: Dict[str, List[pd.DataFrame]] = {} + + for row in normalized_signals.to_dict(orient="records"): + signal_row = OptionRetrievalSignal( + ticker=row["ticker"], + start_date=row["start_date"], + end_date=row["end_date"], + side=int(row["side"]), + quantity=float(row["quantity"]), + signal_id=str(row["signal_id"]), + right=row.get("right"), + target_dte=row.get("target_dte"), + target_moneyness=row.get("target_moneyness"), + metadata=row.get("metadata", {}), + ) + roll_contracts, ohlc_segments, roll_unmatched = self._retrieve_signal_with_rolls(signal=signal_row) + selected.extend(roll_contracts) + unmatched.extend(roll_unmatched) + if ohlc_segments: + signal_ohlc[signal_row.signal_id] = ohlc_segments + + selected_df = pd.DataFrame([s.__dict__ for s in selected]) if selected else pd.DataFrame() + unmatched_df = pd.DataFrame([u.__dict__ for u in unmatched]) if unmatched else pd.DataFrame() + + return OptionVectorizedRetrievalResult( + normalized_signals=normalized_signals, + selected_contracts=selected_df, + unmatched_signals=unmatched_df, + signal_ohlc=signal_ohlc, + ) + + def _build_signals( + self, + *, + signals: Optional[List[OptionRetrievalSignal]], + strategy: Optional[MultiAssetStrategy], + include_open_positions: bool, + strategy_end_date: Optional[DateLike], + ) -> pd.DataFrame: + """Build canonical signal DataFrame from exactly one input source.""" + if (signals is None and strategy is None) or (signals is not None and strategy is not None): + raise ValueError("Provide exactly one source: signals or strategy.") + + if signals is not None: + rows = [self._normalize_signal_dict(signal=s) for s in signals] + return pd.DataFrame(rows) + + assert strategy is not None + strategy_signals = self.from_multi_asset_strategy( + strategy=strategy, + include_open_positions=include_open_positions, + strategy_end_date=strategy_end_date, + ) + rows = [self._normalize_signal_dict(signal=s) for s in strategy_signals] + return pd.DataFrame(rows) + + def _normalize_signal_dict(self, signal: OptionRetrievalSignal) -> Dict[str, Any]: + """Normalize a single signal and enforce required fields.""" + start_dt = to_datetime(signal.start_date) + end_dt = to_datetime(signal.end_date) + + if end_dt < start_dt: + raise ValueError(f"Signal {signal.signal_id} has end_date earlier than start_date.") + if signal.side not in (-1, 1): + raise ValueError(f"Signal {signal.signal_id} side must be -1 or 1.") + + right = self._resolve_signal_right(signal=signal) + + return { + "ticker": signal.ticker.upper(), + "start_date": start_dt.strftime("%Y-%m-%d"), + "end_date": end_dt.strftime("%Y-%m-%d"), + "side": int(signal.side), + "quantity": float(signal.quantity), + "signal_id": str(signal.signal_id), + "right": right, + "target_dte": signal.target_dte, + "target_moneyness": signal.target_moneyness, + "metadata": signal.metadata or {}, + } + + def _resolve_signal_right(self, signal: OptionRetrievalSignal) -> str: + """Resolve right using precedence: explicit signal > instance default > side.""" + if signal.right is not None: + right = signal.right.upper() + elif self.right is not None: + right = self.right + else: + right = "C" if signal.side > 0 else "P" + + if right not in {"C", "P"}: + raise ValueError(f"Signal {signal.signal_id} has invalid right '{right}'.") + return right + + @staticmethod + def from_multi_asset_strategy( + strategy: MultiAssetStrategy, + *, + include_open_positions: bool = False, + strategy_end_date: Optional[DateLike] = None, + ) -> List[OptionRetrievalSignal]: + """Create canonical signals from MultiAssetStrategy simulation trades. + + Args: + strategy: QuantTools MultiAssetStrategy instance. + include_open_positions: Include opens with missing closes. + strategy_end_date: End date used for open positions when included. + + Returns: + List of OptionRetrievalSignal records inferred from open/close trades. + """ + results = strategy.simulate_all(finalize=not include_open_positions) + signals: List[OptionRetrievalSignal] = [] + fallback_end_dt = to_datetime(strategy_end_date) if strategy_end_date is not None else None + + for ticker, trades in results.trades.items(): + pending_open: Dict[str, Dict[str, Any]] = {} + open_counter = 0 + for trade in trades: + action = str(trade.get("action", "")).lower() + sig_id = str(trade.get("signal_id") or f"{ticker}-open-{open_counter}") + if action == "open": + open_counter += 1 + pending_open[sig_id] = trade + continue + if action != "close": + continue + + open_trade = pending_open.pop(sig_id, None) + if open_trade is None: + continue + + close_side = int(trade.get("side", 1)) + signals.append( + OptionRetrievalSignal( + ticker=ticker, + start_date=to_datetime(open_trade["date"]).strftime("%Y-%m-%d"), + end_date=to_datetime(trade["date"]).strftime("%Y-%m-%d"), + side=1 if close_side >= 0 else -1, + quantity=1.0, + signal_id=sig_id, + ) + ) + + if include_open_positions and pending_open: + if fallback_end_dt is None: + raise ValueError( + "strategy_end_date is required when include_open_positions=True and unmatched opens exist." + ) + for sig_id, open_trade in pending_open.items(): + signals.append( + OptionRetrievalSignal( + ticker=ticker, + start_date=to_datetime(open_trade["date"]).strftime("%Y-%m-%d"), + end_date=fallback_end_dt.strftime("%Y-%m-%d"), + side=1, + quantity=1.0, + signal_id=sig_id, + ) + ) + + return signals + + def _select_contract_for_signal( + self, + signal: OptionRetrievalSignal, + *, + chain_date: Optional[str] = None, + roll_index: int = 0, + ) -> Tuple[Optional[SelectedOptionContract], Optional[UnmatchedSignal]]: + """Select the nearest contract for one signal from chain data.""" + chain_date = chain_date or to_datetime(signal.start_date).strftime("%Y-%m-%d") + target_right = self._resolve_signal_right(signal=signal) + + try: + chain = retrieve_chain_bulk( + symbol=signal.ticker, + exp=0, + start_date=chain_date, + end_date=chain_date, + end_time=self.end_time, + option_type=target_right, + print_url=self.print_url, + ) + except Exception as exc: + return None, UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="chain_retrieval_error", + details={"error": str(exc)}, + ) + + if chain is None or len(chain) == 0: + return None, UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="empty_chain", + ) + + normalized_chain, err = self._normalize_chain( + chain=chain, + asof_date=chain_date, + target_right=target_right, + ticker=signal.ticker, + ) + if err is not None: + return None, UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="invalid_chain_schema", + details={"error": err}, + ) + + target_dte = signal.target_dte if signal.target_dte is not None else self.target_dte + target_moneyness = signal.target_moneyness if signal.target_moneyness is not None else self.target_moneyness + + ranked = normalized_chain.copy() + ranked["dte_distance"] = (ranked["dte"] - float(target_dte)).abs() + ranked["mny_distance"] = (ranked["moneyness"] - float(target_moneyness)).abs() + ranked["score"] = self.dte_weight * ranked["dte_distance"] + self.moneyness_weight * ranked["mny_distance"] + + if "open_interest" not in ranked.columns: + ranked["open_interest"] = 0.0 + ranked = ranked.sort_values( + by=["score", "dte_distance", "mny_distance", "open_interest"], + ascending=[True, True, True, False], + ).reset_index(drop=True) + + best = ranked.iloc[0] + if float(best["dte_distance"]) > float(self.dte_tolerance): + return None, UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="dte_out_of_tolerance", + details={"distance": float(best["dte_distance"]), "tolerance": self.dte_tolerance}, + ) + if float(best["mny_distance"]) > float(self.moneyness_tolerance): + return None, UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="moneyness_out_of_tolerance", + details={"distance": float(best["mny_distance"]), "tolerance": self.moneyness_tolerance}, + ) + + contract = SelectedOptionContract( + signal_id=signal.signal_id, + ticker=signal.ticker, + right=str(best["right"]), + strike=float(best["strike"]), + expiration=to_datetime(best["expiration"]).strftime("%Y-%m-%d"), + chain_date=chain_date, + dte=int(best["dte"]), + moneyness=float(best["moneyness"]), + score=float(best["score"]), + roll_index=roll_index, + ) + return contract, None + + def _retrieve_signal_with_rolls( + self, + *, + signal: OptionRetrievalSignal, + ) -> Tuple[List[SelectedOptionContract], List[pd.DataFrame], List[UnmatchedSignal]]: + """Retrieve one or many contract segments for a signal. + + If roll is enabled and signal end extends past roll trigger, this method + iteratively re-selects contracts on subsequent roll dates. + """ + selected_contracts: List[SelectedOptionContract] = [] + unmatched: List[UnmatchedSignal] = [] + segments: List[pd.DataFrame] = [] + + signal_end_dt = to_datetime(signal.end_date) + segment_start_dt = to_datetime(signal.start_date) + roll_index = 0 + + while segment_start_dt <= signal_end_dt: + chain_date = segment_start_dt.strftime("%Y-%m-%d") + contract, miss = self._select_contract_for_signal( + signal, + chain_date=chain_date, + roll_index=roll_index, + ) + if miss is not None: + unmatched.append(miss) + break + assert contract is not None + + expiration_dt = to_datetime(contract.expiration) + if self.roll_enabled: + trigger_dt = expiration_dt - timedelta(days=max(self.roll_on_dte, 0)) + segment_end_dt = min(signal_end_dt, trigger_dt) + if segment_end_dt < segment_start_dt: + # Guard for near-expiry selections when roll_on_dte is large. + segment_end_dt = min(signal_end_dt, expiration_dt) + else: + segment_end_dt = signal_end_dt + + contract.segment_start = segment_start_dt.strftime("%Y-%m-%d") + contract.segment_end = segment_end_dt.strftime("%Y-%m-%d") + selected_contracts.append(contract) + + seg_signal = OptionRetrievalSignal( + ticker=signal.ticker, + start_date=contract.segment_start, + end_date=contract.segment_end, + side=signal.side, + quantity=signal.quantity, + signal_id=signal.signal_id, + right=signal.right, + target_dte=signal.target_dte, + target_moneyness=signal.target_moneyness, + metadata=signal.metadata, + ) + ohlc_df, ohlc_err = self._retrieve_signal_ohlc(signal=seg_signal, contract=contract) + if ohlc_err is not None: + unmatched.append(ohlc_err) + break + ohlc_df["roll_index"] = roll_index + ohlc_df["segment_start"] = contract.segment_start + ohlc_df["segment_end"] = contract.segment_end + segments.append(ohlc_df) + + if not self.roll_enabled: + break + if segment_end_dt >= signal_end_dt: + break + + segment_start_dt = segment_end_dt + timedelta(days=1) + roll_index += 1 + + return selected_contracts, segments, unmatched + + def _retrieve_signal_ohlc( + self, + *, + signal: OptionRetrievalSignal, + contract: SelectedOptionContract, + ) -> Tuple[pd.DataFrame, Optional[UnmatchedSignal]]: + """Retrieve full OHLC time series for one selected contract.""" + start_str = to_datetime(signal.start_date).strftime("%Y-%m-%d") + end_str = to_datetime(signal.end_date).strftime("%Y-%m-%d") + + try: + # ohlc = retrieve_eod_ohlc( + # symbol=contract.ticker, + # start_date=start_str, + # end_date=end_str, + # strike=contract.strike, + # exp=contract.expiration, + # right=contract.right, + # print_url=self.print_url, + # ) + ohlc = OptionSpotDataManager(symbol=contract.ticker).get_option_spot_timeseries( + start_date=start_str, + end_date=end_str, + strike=contract.strike, + expiration=contract.expiration, + right=contract.right, + ).timeseries + except Exception as exc: + return pd.DataFrame(), UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="ohlc_retrieval_error", + details={"error": str(exc)}, + ) + + if ohlc is None or len(ohlc) == 0: + return pd.DataFrame(), UnmatchedSignal( + signal_id=signal.signal_id, + ticker=signal.ticker, + reason="empty_ohlc", + ) + + ohlc_df = pd.DataFrame(ohlc).copy() + ohlc_df["signal_id"] = signal.signal_id + ohlc_df["ticker"] = contract.ticker + ohlc_df["strike"] = contract.strike + ohlc_df["expiration"] = contract.expiration + ohlc_df["right"] = contract.right + return ohlc_df, None + + def _normalize_chain( + self, + chain: pd.DataFrame, + asof_date: str, + target_right: str, + ticker: str, + ) -> Tuple[pd.DataFrame, Optional[str]]: + """Normalize chain DataFrame into ranking columns used by selector.""" + frame = pd.DataFrame(chain).copy() + strike_col = self._find_col(frame.columns.tolist(), ["strike", "Strike"]) + right_col = self._find_col(frame.columns.tolist(), ["right", "Right", "put_call", "putcall", "option_type"]) + exp_col = self._find_col(frame.columns.tolist(), ["expiration", "Expiration", "exp", "expiry"]) + if strike_col is None or right_col is None or exp_col is None: + return pd.DataFrame(), "missing required columns strike/right/expiration" + + oi_col = self._find_col(frame.columns.tolist(), ["open_interest", "Open_interest", "OpenInterest", "oi"]) + + frame["strike"] = pd.to_numeric(frame[strike_col], errors="coerce") + frame["right"] = frame[right_col].astype(str).str.upper() + frame = frame[frame["right"] == target_right].copy() + + exp_raw = frame[exp_col].astype(str) + exp_fmt = exp_raw.str.replace(r"[^0-9]", "", regex=True) + exp_parsed = exp_raw.where(exp_fmt.str.len() != 8, other=exp_fmt) + frame["expiration"] = to_datetime(exp_parsed.tolist()) + asof_dt = to_datetime(asof_date) + frame["dte"] = (frame["expiration"] - asof_dt).dt.days + + frame = frame[(frame["dte"] >= 0) & frame["strike"].notna() & (frame["strike"] > 0)].copy() + if frame.empty: + return pd.DataFrame(), "no candidates after right/dte filtering" + + if oi_col is not None: + frame["open_interest"] = pd.to_numeric(frame[oi_col], errors="coerce").fillna(0.0) + else: + frame["open_interest"] = 0.0 + + chain_spot = self._get_chain_spot_on_date(ticker=ticker, date_str=asof_date) + if chain_spot is None or chain_spot <= 0: + return pd.DataFrame(), "missing chain_spot on asof date" + frame["spot"] = float(chain_spot) + + # Keep moneyness convention identical to populate_cache_with_chain: + # Calls: Strike / spot; Puts: spot / Strike + frame["moneyness"] = 0.0 + frame.loc[frame["right"] == "C", "moneyness"] = ( + frame.loc[frame["right"] == "C", "strike"] / frame.loc[frame["right"] == "C", "spot"] + ) + frame.loc[frame["right"] == "P", "moneyness"] = ( + frame.loc[frame["right"] == "P", "spot"] / frame.loc[frame["right"] == "P", "strike"] + ) + + return frame, None + + def _prime_chain_spot_cache(self, normalized_signals: pd.DataFrame) -> None: + """Preload chain-spot timeseries per ticker for signal date range. + + Uses MarketTimeseries chain_spot endpoint to ensure moneyness is always + computed from chain spot rather than equity spot. + """ + self._chain_spot_cache = {} + if normalized_signals.empty: + return + + mt = get_timeseries_obj(live=False) + grouped = normalized_signals.groupby("ticker", as_index=False).agg( + start_date=("start_date", "min"), + end_date=("start_date", "max"), + ) + for row in grouped.to_dict(orient="records"): + ticker = str(row["ticker"]).upper() + start_str = to_datetime(row["start_date"]).strftime("%Y-%m-%d") + end_str = to_datetime(row["end_date"]).strftime("%Y-%m-%d") + try: + ts = mt.get_timeseries( + sym=ticker, + factor="chain_spot", + start_date=start_str, + end_date=end_str, + ).chain_spot + except Exception: + continue + + if ts is None or len(ts) == 0: + continue + + ts_df = pd.DataFrame(ts).copy() + ts_df.index = pd.to_datetime(ts_df.index) + spot_col = self._find_col( + ts_df.columns.tolist(), + ["close", "Close", "spot", "Spot", "chain_spot", "Chain_spot"], + ) + if spot_col is None: + continue + + series = pd.to_numeric(ts_df[spot_col], errors="coerce").dropna() + if series.empty: + continue + + series.index = pd.to_datetime(series.index) + self._chain_spot_cache[ticker] = series.sort_index() + + def _get_chain_spot_on_date(self, ticker: str, date_str: str) -> Optional[float]: + """Get chain spot for ticker on date, using forward-fill within cached history.""" + series = self._chain_spot_cache.get(ticker.upper()) + if series is None or series.empty: + return None + + target = to_datetime(date_str) + if target in series.index: + val = series.loc[target] + return float(val) if pd.notna(val) else None + + prior = series[series.index <= target] + if prior.empty: + return None + val = prior.iloc[-1] + return float(val) if pd.notna(val) else None + + @staticmethod + def _find_col(columns: List[str], aliases: List[str]) -> Optional[str]: + """Find the first matching column name by case-insensitive alias.""" + lookup = {col.lower(): col for col in columns} + for alias in aliases: + if alias.lower() in lookup: + return lookup[alias.lower()] + return None diff --git a/trade/datamanager/market_data.py b/trade/datamanager/market_data.py index 853a4bc..d07fc67 100644 --- a/trade/datamanager/market_data.py +++ b/trade/datamanager/market_data.py @@ -703,7 +703,6 @@ def _get_spot_timeseries(self, sym: str, start: str = None, end: str = None, *ar 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, diff --git a/trade/tests/test_option_vectorized_pnl_runner.py b/trade/tests/test_option_vectorized_pnl_runner.py new file mode 100644 index 0000000..c1716e7 --- /dev/null +++ b/trade/tests/test_option_vectorized_pnl_runner.py @@ -0,0 +1,141 @@ +from types import SimpleNamespace + +import pandas as pd + +from trade.backtester_.option_vectorized_pnl_runner import OptionVectorizedPnLRunner +from trade.backtester_.option_vectorized_retrieval import OptionVectorizedRetrievalResult + + +def _make_retrieval_result() -> OptionVectorizedRetrievalResult: + selected_contracts = pd.DataFrame( + [ + { + "signal_id": "sig_a", + "ticker": "AAPL", + "right": "C", + "strike": 100.0, + "expiration": "2026-01-17", + "roll_index": 0, + "segment_start": "2026-01-02", + "segment_end": "2026-01-05", + }, + { + "signal_id": "sig_a", + "ticker": "AAPL", + "right": "C", + "strike": 105.0, + "expiration": "2026-01-24", + "roll_index": 1, + "segment_start": "2026-01-05", + "segment_end": "2026-01-07", + }, + ] + ) + + dummy_seg_0 = pd.DataFrame( + { + "open": [1.0], + "high": [1.0], + "low": [1.0], + "close": [1.0], + }, + index=pd.to_datetime(["2026-01-02"]), + ) + dummy_seg_1 = pd.DataFrame( + { + "open": [1.0], + "high": [1.0], + "low": [1.0], + "close": [1.0], + }, + index=pd.to_datetime(["2026-01-05"]), + ) + + return OptionVectorizedRetrievalResult( + normalized_signals=selected_contracts[["signal_id", "ticker"]].drop_duplicates().copy(), + selected_contracts=selected_contracts, + signal_ohlc={"sig_a": [dummy_seg_0, dummy_seg_1]}, + unmatched_signals=pd.DataFrame(), + ) + + +def test_runner_aggregates_rolls_and_builds_normalized_equity(monkeypatch): + retrieval_result = _make_retrieval_result() + + def _mock_opttick(*, symbol, right, exp, strike): + return f"{symbol}_{right}_{exp}_{int(strike)}" + + def _mock_load_option_pnl_data(*, yesterday, today, opttick, payload=None): + dates = pd.date_range(pd.Timestamp(yesterday), pd.Timestamp(today), freq="D") + attribution = pd.DataFrame( + { + "delta_pnl": [0.4] * len(dates), + "total_pnl_excl_trade_pnl": [1.0] * len(dates), + "trade_pnl_adjustment": [7.0] * len(dates), + "unexplained_pnl": [0.1] * len(dates), + }, + index=dates, + ) + return SimpleNamespace(attribution=attribution) + + monkeypatch.setattr( + "trade.backtester_.option_vectorized_pnl_runner.generate_opttick_new", + _mock_opttick, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_pnl_runner.load_option_pnl_data", + _mock_load_option_pnl_data, + ) + + runner = OptionVectorizedPnLRunner(starting_nav=100.0) + result = runner.run(retrieval_result) + + assert result.failures.empty + assert len(result.trade_summary) == 2 + + assert "trade_pnl_adjustment" not in result.trade_daily.columns + assert (result.trade_daily["total_pnl"] == result.trade_daily["total_pnl_excl_trade_pnl"]).all() + + overlap_row = result.signal_daily[ + (result.signal_daily["date"] == pd.Timestamp("2026-01-05")) & (result.signal_daily["signal_id"] == "sig_a") + ] + assert len(overlap_row) == 1 + assert float(overlap_row.iloc[0]["total_pnl"]) == 2.0 + + eq = result.portfolio_equity_curve + first_day = eq[eq["date"] == pd.Timestamp("2026-01-02")].iloc[0] + assert float(first_day["normalized_equity"]) == 1.01 + + +def test_runner_collects_failures_when_roll_segment_missing(monkeypatch): + retrieval_result = _make_retrieval_result() + retrieval_result.signal_ohlc = {"sig_a": [retrieval_result.signal_ohlc["sig_a"][0]]} + + def _mock_opttick(*, symbol, right, exp, strike): + return f"{symbol}_{right}_{exp}_{int(strike)}" + + def _mock_load_option_pnl_data(*, yesterday, today, opttick, payload=None): + dates = pd.date_range(pd.Timestamp(yesterday), pd.Timestamp(today), freq="D") + attribution = pd.DataFrame( + { + "total_pnl_excl_trade_pnl": [1.0] * len(dates), + "trade_pnl_adjustment": [0.0] * len(dates), + }, + index=dates, + ) + return SimpleNamespace(attribution=attribution) + + monkeypatch.setattr( + "trade.backtester_.option_vectorized_pnl_runner.generate_opttick_new", + _mock_opttick, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_pnl_runner.load_option_pnl_data", + _mock_load_option_pnl_data, + ) + + result = OptionVectorizedPnLRunner().run(retrieval_result) + + assert len(result.failures) == 1 + assert "missing segment" in str(result.failures.iloc[0]["reason"]).lower() + assert len(result.trade_summary) == 1 diff --git a/trade/tests/test_option_vectorized_retrieval.py b/trade/tests/test_option_vectorized_retrieval.py new file mode 100644 index 0000000..1ad5d45 --- /dev/null +++ b/trade/tests/test_option_vectorized_retrieval.py @@ -0,0 +1,310 @@ +from datetime import datetime + +import pandas as pd +import pytest + +from trade.backtester_.option_vectorized_retrieval import ( + OptionRetrievalSignal, + OptionVectorizedRetriever, +) + + +def test_run_requires_exactly_one_source() -> None: + retriever = OptionVectorizedRetriever(target_dte=30, target_moneyness=1.0) + with pytest.raises(ValueError): + retriever.run(signals=None, strategy=None) + + +def test_explicit_right_precedence() -> None: + retriever = OptionVectorizedRetriever(target_dte=30, target_moneyness=1.0, right="P") + signal = OptionRetrievalSignal( + ticker="AAPL", + start_date="2026-01-02", + end_date="2026-01-30", + side=1, + quantity=1.0, + signal_id="s1", + right="C", + ) + assert retriever._resolve_signal_right(signal) == "C" + + +def test_run_selects_contract_and_returns_ohlc(monkeypatch: pytest.MonkeyPatch) -> None: + retriever = OptionVectorizedRetriever( + target_dte=30, + target_moneyness=1.0, + right="C", + dte_tolerance=5, + moneyness_tolerance=0.2, + ) + + chain = pd.DataFrame( + { + "Root": ["AAPL", "AAPL"], + "Expiration": ["20260201", "20260215"], + "Strike": [100.0, 110.0], + "Right": ["C", "C"], + "Spot": [100.0, 100.0], + "Open_interest": [1000, 500], + } + ) + + ohlc = pd.DataFrame( + { + "Open": [1.0, 1.1], + "High": [1.2, 1.3], + "Low": [0.9, 1.0], + "Close": [1.1, 1.2], + "Midpoint": [1.05, 1.15], + }, + index=pd.DatetimeIndex([datetime(2026, 1, 2), datetime(2026, 1, 3)]), + ) + + chain_spot_ts = pd.DataFrame( + { + "close": [100.0, 100.0], + }, + index=pd.DatetimeIndex([datetime(2026, 1, 2), datetime(2026, 1, 3)]), + ) + + def _mock_chain_bulk(**kwargs): + return chain + + def _mock_eod_ohlc(**kwargs): + return ohlc + + class _MockTsResult: + def __init__(self, chain_spot: pd.DataFrame): + self.chain_spot = chain_spot + + class _MockMarketTimeseries: + def get_timeseries(self, **kwargs): + return _MockTsResult(chain_spot=chain_spot_ts) + + def _mock_get_timeseries_obj(*args, **kwargs): + return _MockMarketTimeseries() + + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_chain_bulk", + _mock_chain_bulk, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_eod_ohlc", + _mock_eod_ohlc, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.get_timeseries_obj", + _mock_get_timeseries_obj, + ) + + signal = OptionRetrievalSignal( + ticker="AAPL", + start_date="2026-01-02", + end_date="2026-01-10", + side=1, + quantity=1.0, + signal_id="sig-123", + ) + result = retriever.run(signals=[signal]) + + assert len(result.selected_contracts) == 1 + assert result.unmatched_signals.empty + assert "sig-123" in result.signal_ohlc + assert len(result.signal_ohlc["sig-123"]) == 1 + assert not result.signal_ohlc["sig-123"][0].empty + + +def test_roll_enabled_builds_multiple_segments(monkeypatch: pytest.MonkeyPatch) -> None: + retriever = OptionVectorizedRetriever( + target_dte=5, + target_moneyness=1.0, + right="C", + roll_enabled=True, + roll_on_dte=0, + dte_tolerance=50, + moneyness_tolerance=1.0, + ) + + chain_first = pd.DataFrame( + { + "Root": ["AAPL"], + "Expiration": ["20260105"], + "Strike": [100.0], + "Right": ["C"], + "Open_interest": [1000], + } + ) + chain_second = pd.DataFrame( + { + "Root": ["AAPL"], + "Expiration": ["20260120"], + "Strike": [100.0], + "Right": ["C"], + "Open_interest": [900], + } + ) + + def _mock_chain_bulk(**kwargs): + if kwargs["start_date"] <= "2026-01-05": + return chain_first + return chain_second + + def _mock_eod_ohlc(**kwargs): + start_dt = pd.to_datetime(kwargs["start_date"]) + end_dt = pd.to_datetime(kwargs["end_date"]) + idx = pd.date_range(start=start_dt, end=end_dt, freq="D") + return pd.DataFrame( + { + "Open": [1.0] * len(idx), + "High": [1.1] * len(idx), + "Low": [0.9] * len(idx), + "Close": [1.0] * len(idx), + "Midpoint": [1.0] * len(idx), + }, + index=idx, + ) + + chain_spot_ts = pd.DataFrame( + { + "close": [100.0] * 30, + }, + index=pd.date_range(start="2026-01-01", periods=30, freq="D"), + ) + + class _MockTsResult: + def __init__(self, chain_spot: pd.DataFrame): + self.chain_spot = chain_spot + + class _MockMarketTimeseries: + def get_timeseries(self, **kwargs): + return _MockTsResult(chain_spot=chain_spot_ts) + + def _mock_get_timeseries_obj(*args, **kwargs): + return _MockMarketTimeseries() + + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_chain_bulk", + _mock_chain_bulk, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_eod_ohlc", + _mock_eod_ohlc, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.get_timeseries_obj", + _mock_get_timeseries_obj, + ) + + signal = OptionRetrievalSignal( + ticker="AAPL", + start_date="2026-01-02", + end_date="2026-01-15", + side=1, + quantity=1.0, + signal_id="sig-roll", + ) + result = retriever.run(signals=[signal]) + + assert result.unmatched_signals.empty + assert "sig-roll" in result.signal_ohlc + assert len(result.signal_ohlc["sig-roll"]) == 2 + assert len(result.selected_contracts) == 2 + assert set(result.selected_contracts["roll_index"].tolist()) == {0, 1} + + +def test_run_n_size_samples_signals(monkeypatch: pytest.MonkeyPatch) -> None: + retriever = OptionVectorizedRetriever( + target_dte=30, + target_moneyness=1.0, + right="C", + dte_tolerance=5, + moneyness_tolerance=0.2, + ) + + chain = pd.DataFrame( + { + "Root": ["AAPL", "MSFT"], + "Expiration": ["20260201", "20260201"], + "Strike": [100.0, 100.0], + "Right": ["C", "C"], + "Spot": [100.0, 100.0], + "Open_interest": [1000, 1000], + } + ) + + ohlc = pd.DataFrame( + { + "Open": [1.0], + "High": [1.2], + "Low": [0.9], + "Close": [1.1], + "Midpoint": [1.05], + }, + index=pd.DatetimeIndex([datetime(2026, 1, 2)]), + ) + + chain_spot_ts = pd.DataFrame( + {"close": [100.0, 100.0]}, + index=pd.DatetimeIndex([datetime(2026, 1, 2), datetime(2026, 1, 3)]), + ) + + def _mock_chain_bulk(**kwargs): + return chain + + def _mock_eod_ohlc(**kwargs): + return ohlc + + class _MockTsResult: + def __init__(self, chain_spot: pd.DataFrame): + self.chain_spot = chain_spot + + class _MockMarketTimeseries: + def get_timeseries(self, **kwargs): + return _MockTsResult(chain_spot=chain_spot_ts) + + def _mock_get_timeseries_obj(*args, **kwargs): + return _MockMarketTimeseries() + + def _sample_first(self, n=None, *args, **kwargs): + return self.iloc[:n].copy() + + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_chain_bulk", + _mock_chain_bulk, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.retrieve_eod_ohlc", + _mock_eod_ohlc, + ) + monkeypatch.setattr( + "trade.backtester_.option_vectorized_retrieval.get_timeseries_obj", + _mock_get_timeseries_obj, + ) + monkeypatch.setattr(pd.DataFrame, "sample", _sample_first) + + signals = [ + OptionRetrievalSignal( + ticker="AAPL", + start_date="2026-01-02", + end_date="2026-01-10", + side=1, + quantity=1.0, + signal_id="sig-a", + ), + OptionRetrievalSignal( + ticker="MSFT", + start_date="2026-01-02", + end_date="2026-01-10", + side=1, + quantity=1.0, + signal_id="sig-b", + ), + ] + + full_result = retriever.run(signals=signals) + assert set(full_result.normalized_signals["signal_id"].tolist()) == {"sig-a", "sig-b"} + assert set(full_result.selected_contracts["signal_id"].tolist()) == {"sig-a", "sig-b"} + + sampled_result = retriever.run(signals=signals, n_size=1) + assert len(sampled_result.normalized_signals) == 1 + assert len(sampled_result.selected_contracts) == 1 From 347619c1d1fcbf8ecf44a049e52c14a9c5112325 Mon Sep 17 00:00:00 2001 From: mac mini Date: Thu, 21 May 2026 11:54:06 -0500 Subject: [PATCH 60/81] declare module --- module_test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 module_test/__init__.py diff --git a/module_test/__init__.py b/module_test/__init__.py new file mode 100644 index 0000000..e69de29 From 112484494e5e8b6b597e079da4df6e6c538510bb Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sat, 23 May 2026 00:33:36 -0400 Subject: [PATCH 61/81] Add vectorized/donchian cogs and timeseries cache cleanup --- EventDriven/configs/core.py | 18 +- EventDriven/riskmanager/market_timeseries.py | 162 +++++++- EventDriven/riskmanager/new_base.py | 2 +- .../riskmanager/position/cogs/__init__.py | 12 +- .../riskmanager/position/cogs/donchian_cog.py | 348 ++++++++++++++++++ .../position/cogs/mean_reversion.py | 1 - .../riskmanager/position/cogs/pnl_monitor.py | 20 +- .../riskmanager/position/cogs/vectorized.py | 191 ++++++++++ .../riskmanager/special_dividends.yaml | 1 + 9 files changed, 735 insertions(+), 20 deletions(-) create mode 100644 EventDriven/riskmanager/position/cogs/donchian_cog.py create mode 100644 EventDriven/riskmanager/position/cogs/vectorized.py diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 28d908c..d9f0c9b 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -335,4 +335,20 @@ class PnlMonitorConfig(BaseCogConfig): """ name: Optional[str] = "PnLMonitorCog" - enabled: bool = True \ No newline at end of file + enabled: bool = True + + +@pydantic_dataclass +class VectorizedCogConfig(BaseCogConfig): + """ + Configuration dataclass for VectorizedCog. + + Simple position monitoring cog that: + - Validates new positions by cash availability (informational) + - Monitors open positions for DTE-based roll triggers + """ + + name: str = "VectorizedCog" + enabled: bool = True + dte_limit_enabled: bool = True + dte_threshold: int = 30 diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index e6d5f99..bf1d95d 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -165,13 +165,15 @@ from trade.helpers.Logging import setup_logger from trade.helpers.pools import _change_global_stream_level from EventDriven.dataclasses.timeseries import AtTimeOptionData, AtTimePositionData +from EventDriven.types import TradeID -logger = setup_logger("EventDriven.riskmanager.market_timeseries", stream_log_level="INFO") +logger = setup_logger("EventDriven.riskmanager.market_timeseries", stream_log_level="WARNING") logger.info("Changing pools log level to WARNING for market_timeseries module") _change_global_stream_level("WARNING") SPECIAL_DIVIDENDS: Dict[str, List[Dict[str, Any]]] = {} NO_SPECIAL_DIVIDENDS: List[str] = [] + def load_special_divs(): global SPECIAL_DIVIDENDS, NO_SPECIAL_DIVIDENDS loc = files("EventDriven.riskmanager").joinpath("special_dividends.yaml") @@ -196,7 +198,7 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): target="processed_option_data", clear_on_exit=True ) ## Cache to store processed option data, cleared on exit to avoid polluting the cache with potentially large data that might not be needed in future sessions. self.position_data_cache = load_riskmanager_cache(target="position_data") - + self.special_dividends = load_riskmanager_cache(target="special_dividend") self.splits = load_riskmanager_cache(target="splits_raw") self.adjusted_strike_cache = load_riskmanager_cache(target="adjusted_strike_cache") @@ -207,7 +209,7 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): self.undl_timeseries_config = UndlTimeseriesConfig() self.option_price_config = OptionPriceConfig() self.lock = Lock() - + ## Private attrs self._backup_position_data_cache = load_riskmanager_cache( target="position_data_backup", clear_on_exit=True, create_on_missing=True @@ -215,7 +217,7 @@ def __init__(self, _start: Union[datetime, str], _end: Union[datetime, str]): self._loaded_special_dividends = {} ## Load special dividends info from yaml file into cache. - ## Reloads on initialization to ensure we have the most up-to-date information, and to avoid issues with mutable global state if the module is reloaded during the session. + ## Reloads on initialization to ensure we have the most up-to-date information, and to avoid issues with mutable global state if the module is reloaded during the session. load_special_divs() def skip(self, position_id: str, date: Union[datetime, str], column: str = "Midpoint") -> bool: @@ -229,7 +231,7 @@ def skip(self, position_id: str, date: Union[datetime, str], column: str = "Midp skip = skips_meta.get(column.capitalize()) return skip.get("skip_day", False) - + def pre_run_setup(self): """ Pre-run setup for BacktestTimeseries. Currently, all necessary setup is done in __init__, so this method is a placeholder for any future setup steps that might be needed before the backtest run starts. @@ -240,6 +242,136 @@ def pre_run_setup(self): self._backup_position_data_cache.clear() self.adjusted_strike_cache.clear() + def _get_trade_id(self, trade_id: Union[str, TradeID]) -> TradeID: + """Normalize a trade identifier to a TradeID instance. + + Args: + trade_id: Trade identifier as a raw string or TradeID. + + Returns: + Normalized TradeID instance. + """ + return trade_id if isinstance(trade_id, TradeID) else TradeID(str(trade_id)) + + def _get_associated_option_ticks(self, opttick: str) -> set[str]: + """Get base and split/dividend-adjusted option ticks for a leg. + + Builds the associated option tick universe by replaying all valid + split/dividend event boundaries across the backtest window. + """ + associated_ticks: set[str] = {opttick} + meta = parse_option_tick(opttick) + + start_dt = to_datetime(self.start_date) + effective_end = min(to_datetime(self.end_date), to_datetime(meta["exp_date"])) + + try: + splits = self.get_splits(meta["ticker"], bkt_end_date=effective_end) + except Exception as exc: + logger.warning(f"Could not load splits for {opttick}: {exc}") + splits = [] + + try: + dividends = self.get_special_dividends(meta["ticker"]) + except Exception as exc: + logger.warning(f"Could not load special dividends for {opttick}: {exc}") + dividends = {} + + events: List[tuple[pd.Timestamp, float, str]] = [] + + for split_date, split_factor in splits: + split_date = to_datetime(split_date) + if compare_dates.inbetween(split_date, start_dt, effective_end): + events.append((split_date, float(split_factor), "SPLIT")) + + for div_date, div_amount in dividends.items(): + div_date = to_datetime(div_date) + if compare_dates.inbetween(div_date, start_dt, effective_end): + events.append((div_date, float(div_amount), "DIVIDEND")) + + events.sort(key=lambda item: item[0]) + if not events: + return associated_ticks + + # Boundary model mirrors generate_option_data_for_trade behavior where + # check_date partitions events into post-event and pre-event regimes. + for boundary in range(len(events) + 1): + adj_strike = float(meta["strike"]) + for idx, (_, factor, event_type) in enumerate(events): + pre_event_regime = idx >= boundary + if pre_event_regime: + if event_type == "SPLIT": + adj_strike /= factor + elif event_type == "DIVIDEND": + adj_strike -= factor + else: + if event_type == "SPLIT": + adj_strike *= factor + elif event_type == "DIVIDEND": + adj_strike += factor + + associated_ticks.add( + generate_option_tick_new( + symbol=meta["ticker"], + strike=adj_strike, + right=meta["put_call"], + exp=meta["exp_date"], + ) + ) + + return associated_ticks + + def delete_position_data(self, trade_id: Union[str, TradeID]) -> None: + """Delete cached data for a single trade and its legs. + + Removes position-level data from the main and backup caches, then clears + any session-loaded option data and option cache entries for the trade's + individual legs. + + Args: + trade_id: Trade identifier to delete. + """ + trade_id_obj = self._get_trade_id(trade_id) + + self.position_data_cache.pop(str(trade_id_obj), None) + self._backup_position_data_cache.pop(str(trade_id_obj), None) + + leg_option_ticks = { + leg_meta[1] for leg_meta in trade_id_obj.legs if isinstance(leg_meta, (list, tuple)) and len(leg_meta) > 1 + } + associated_leg_option_ticks: set[str] = set() + for leg_tick in leg_option_ticks: + associated_leg_option_ticks.update(self._get_associated_option_ticks(leg_tick)) + logger.info( + f"Deleting position data for trade {trade_id_obj}. " + f"Removing option data for legs and associated adjusted ticks: {associated_leg_option_ticks}" + ) + + for cache_key in list(self.session_loaded_option_cache.keys()): + if isinstance(cache_key, tuple) and cache_key and cache_key[0] in associated_leg_option_ticks: + self.session_loaded_option_cache.pop(cache_key, None) + + for option_tick in associated_leg_option_ticks: + self.options_cache.pop(option_tick, None) + + def delete_all_positions_data(self) -> None: + """Clear all cached position-level data. + + Removes both the active position cache and the backup cache. + """ + self.position_data_cache.clear() + self._backup_position_data_cache.clear() + + def delete_all_timeseries_data(self) -> None: + """Clear all cached position and option timeseries data. + + Removes all position-level data plus session-loaded option data and the + global option cache. + """ + self.delete_all_positions_data() + self.session_loaded_option_cache.clear() + self.options_cache.clear() + def set_splits(self, d): """ Setter for splits @@ -251,18 +383,18 @@ def set_splits(self, d): if compare_dates.inbetween(d[0], self.start_date, self.end_date): splits_dict[k].append(d) return splits_dict - + def get_special_dividends(self, ticker: str) -> List[Dict[str, float]]: """ Retrieve special dividend information for a given ticker. - If the ticker is in the no_special_dividends list, returns an empty list. - If the ticker is in the special_dividends list, returns the corresponding dividend information. - If the ticker is not found in either list, raises a ValueError. - """ + """ load_special_divs() if ticker in self._loaded_special_dividends: return self._loaded_special_dividends[ticker] - + if ticker in NO_SPECIAL_DIVIDENDS: return {} elif ticker in SPECIAL_DIVIDENDS: @@ -275,7 +407,7 @@ def get_special_dividends(self, ticker: str) -> List[Dict[str, float]]: raise ValueError( f"Ticker {ticker} not found in either special or no special dividends list. Please update the yaml file accordingly." ) - + def get_list_of_splits(self, ticker: str) -> List[Dict[str, float]]: """ Retrieve list of splits for a given ticker. @@ -283,7 +415,6 @@ def get_list_of_splits(self, ticker: str) -> List[Dict[str, float]]: t = self.market_timeseries._get_chain_spot_timeseries(ticker, "1990-01-01") return list((t[t["split_ratio"] != 1]["split_ratio"]).items()) - def get_splits(self, ticker: str, bkt_end_date: pd.Timestamp): """ Retrieve splits for a given ticker, updating the cache if necessary. @@ -318,7 +449,9 @@ def get_position_data(self, position_id: str) -> pd.DataFrame: d = self.position_data_cache.get(position_id, pd.DataFrame()) if not d.empty: logger.info(f"Position data for {position_id} found in position data cache, returning cached data") - d = self._backup_position_data_cache.get(position_id, d) ## Return the backup data if it exists, otherwise return the original data. This allows us to retain the original position data before any adjustments, while still allowing for adjustments to be made to the position data in the main cache. + d = self._backup_position_data_cache.get( + position_id, d + ) ## Return the backup data if it exists, otherwise return the original data. This allows us to retain the original position data before any adjustments, while still allowing for adjustments to be made to the position data in the main cache. return d def get_at_time_option_data(self, opttick: str, date: Union[datetime, str]) -> AtTimeOptionData: @@ -480,7 +613,9 @@ def get_timeseries(_id, direction): ## Cache the position data self.position_data_cache[position_id] = position_data - self._backup_position_data_cache[position_id] = position_data.copy() ## Store a copy of the original position data in the backup cache before any adjustments are made, to ensure we have the original data available for future reference if needed. + self._backup_position_data_cache[position_id] = ( + position_data.copy() + ) ## Store a copy of the original position data in the backup cache before any adjustments are made, to ensure we have the original data available for future reference if needed. return position_data @@ -613,9 +748,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: ) ## Ensure that we load data at least until the expiration date of the option, even if it is after the backtest end date, to account for any splits or dividends that might happen until expiration. ## Check if there's any split/special dividend - splits = self.splits.get(meta["ticker"], []) splits = self.get_splits(meta["ticker"], bkt_end_date=effective_end) - dividends = self.special_dividends.get(meta["ticker"], {}) dividends = self.get_special_dividends(meta["ticker"]) to_adjust_split = [] @@ -689,7 +822,6 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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"] ) diff --git a/EventDriven/riskmanager/new_base.py b/EventDriven/riskmanager/new_base.py index 3d65b27..75e92db 100644 --- a/EventDriven/riskmanager/new_base.py +++ b/EventDriven/riskmanager/new_base.py @@ -483,7 +483,7 @@ def get_order(self, req: OrderRequest) -> NewPositionState: ## Process order if not order_failed(order): - print(f"\nOrder Received:\n") + print("\nOrder Received:\n") pprint(order) position_id = order["data"]["trade_id"] else: diff --git a/EventDriven/riskmanager/position/cogs/__init__.py b/EventDriven/riskmanager/position/cogs/__init__.py index 7ef1ba4..dfcda81 100644 --- a/EventDriven/riskmanager/position/cogs/__init__.py +++ b/EventDriven/riskmanager/position/cogs/__init__.py @@ -10,6 +10,11 @@ - Delta, gamma, vega, theta monitoring - Capital allocation management + VectorizedCog (vectorized.py): + - DTE-based roll trigger detection + - New position cash validation (informational) + - Minimal, stateless position monitoring + Utilities: analyze_utils.py: - DTE calculation from position IDs @@ -31,7 +36,7 @@ - Opinion-based recommendation system Usage: - from EventDriven.riskmanager.position.cogs import LimitsAndSizingCog + from EventDriven.riskmanager.position.cogs import LimitsAndSizingCog, VectorizedCog from EventDriven.riskmanager.position.cogs.analyze_utils import ( get_dte_and_moneyness_from_trade_id ) @@ -40,3 +45,8 @@ - ../base.py: BaseCog abstract class definition - ../analyzer.py: Cog orchestration and reconciliation """ + +from EventDriven.riskmanager.position.cogs.vectorized import VectorizedCog +from EventDriven.configs.core import VectorizedCogConfig + +__all__ = ["VectorizedCog", "VectorizedCogConfig"] diff --git a/EventDriven/riskmanager/position/cogs/donchian_cog.py b/EventDriven/riskmanager/position/cogs/donchian_cog.py new file mode 100644 index 0000000..4c7332e --- /dev/null +++ b/EventDriven/riskmanager/position/cogs/donchian_cog.py @@ -0,0 +1,348 @@ +from typing import Dict +import math +from EventDriven.riskmanager.position.base import BaseCog +from EventDriven.configs.core import MeanReversionSizerConfigs +from typing import Optional +from EventDriven.dataclasses.states import NewPositionState, PositionAnalysisContext, CogActions +import numpy as np +from EventDriven.riskmanager.sizer._utils import default_delta_limit, delta_position_sizing +from EventDriven.riskmanager.position.cogs.limits import _LimitsMetaData +from EventDriven.types import SignalID +from trade.backtester_._multi_asset_strategy import MultiAssetStrategy +from dataclasses import dataclass +import pandas as pd +from EventDriven.configs.base import pydantic_dataclass +from EventDriven.configs.core import BaseConfigs, _CustomFrozenBaseConfigs, BaseCogConfig, StrategyLimitsEnabled +from pydantic import ConfigDict, Field +from EventDriven.dataclasses.limits import PositionLimits +from .limits import LimitsAndSizingCog +from trade.helpers.Logging import setup_logger +from EventDriven.riskmanager.actions import ROLL, Changes +from EventDriven.configs.core import VectorizedCogConfig +from .analyze_utils import get_dte_and_moneyness_from_trade_id + +logger = setup_logger("EventDriven.riskmanager.position.cogs.donchian_cog") + +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True), ) +class DonchianMomentumCogConfig(BaseCogConfig): + """Configuration for DonchianMomentumCog.""" + + name: str = "DonchianMomentumCog" + sizing_lev: int = 1 + min_scale: float = 0.75 + max_scale: float = 1.50 + dte_limit_enabled: bool = True + dte_threshold: int = 15 + + +@dataclass +class _DonchianLimitsMetaData(_LimitsMetaData): + breakout_score: float = None + rvol: float = None + + +class DonchianMomentumCog(BaseCog): + """ + Donchian Momentum Cog for position analysis. + + This cog evaluates the momentum of a position based on its entry price relative to + the Donchian channel. It emits opinions on whether the position is showing positive + or negative momentum, which can be used for risk management decisions. + """ + default_config: DonchianMomentumCogConfig = DonchianMomentumCogConfig() + def __init__( + self, + eq_strategy: MultiAssetStrategy, + config: Optional[DonchianMomentumCogConfig] = None +): + if config is None: + config: DonchianMomentumCogConfig = DonchianMomentumCogConfig() + + self._config: DonchianMomentumCogConfig = config + # super().__init__(config) + self.eq_strategy = eq_strategy + self.position_limits: Dict[str, PositionLimits] = {} + self.position_metadata: Dict[str, _DonchianLimitsMetaData] = {} + + def on_new_position(self, state: NewPositionState) -> None: + """ + Analyze the momentum of a new position based on its entry price and the Donchian channel. + + Args: + state: The new position state after creation. + Returns: + None (this cog is for analysis and opinion generation, not direct action). + """ + order = state.order + requet = state.request + ticker = state.symbol + undl_data = state.undl_at_time_data + chain_spot = undl_data.chain_spot["close"] + option_chain = state.at_time_data + cash_available = requet.tick_cash + signal_id = SignalID(order.signal_id) + opt_price = option_chain.get_price() + date = order["date"] + info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) + breakout_score = info.get("momentum_breakout_score", 0) + rvol = info.get("momentum_rvol", 0) + if not signal_id.strategy_slug.startswith("donchian"): + logger.warning(f"Signal ID {signal_id} does not appear to be from a Donchian strategy. Skipping momentum analysis.") + q = 1 + order["data"]["quantity"] = q + breakout_score = None + rvol = None + delta = option_chain.delta + scaled_limit = 1 + scaler = None + + + else: + scaler = self.get_momentum_scaler( + breakout_score=breakout_score, + rvol=rvol, + min_mult=self.config.min_scale, + max_mult=self.config.max_scale + ) + + base_limit = default_delta_limit( + cash_available=cash_available, + underlier_price_at_time=chain_spot, + sizing_lev=self.config.sizing_lev, + ) + + scaled_limit = base_limit * scaler + + ## Scale the delta down to 90% to allow room for natural delta movement and reduce overtrading risk + tgt_delta = scaled_limit * 0.9 + delta = option_chain.delta + + q = delta_position_sizing( + cash_available=cash_available, + option_price_at_time=opt_price, + delta=delta, + delta_limit=tgt_delta, + ) + + if q == 0: + logger.warning( + f"Position {order['data']['trade_id']} has zero quantity after momentum scaling. " + f"Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}, " + f"Base Delta Limit: {base_limit:.4f}, Scaled Delta Limit: {scaled_limit:.4f}, " + f"Target Delta (90% of Scaled Limit): {tgt_delta:.4f}, Option Delta: {delta:.4f}" + ) + if cash_available > opt_price: + logger.info( + f"Cash available (${cash_available:.2f}) is greater than option price (${opt_price:.2f}), but quantity is zero." + f" Setting quantity to 1 to allow position opening and future momentum adjustments." + ) + q = 1 + logger.info( + f"Calculated delta limit: {scaled_limit:.4f}, resulting quantity: {q} lev: {self.config.sizing_lev}. Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}. Trade ID: {order['data']['trade_id']} with option delta {delta:.4f}" + ) + order["data"]["quantity"] = q + metadata = _DonchianLimitsMetaData( + trade_id=order["data"]["trade_id"], + date=order["date"], + signal_id=order["signal_id"], + scalar=scaler, + sizing_lev=self.config.sizing_lev, + delta_per_contract=delta, + option_price=opt_price, + undl_price=undl_data.chain_spot["close"], + delta_lmt=scaled_limit, + new_quantity=q, + breakout_score=breakout_score, + rvol=rvol, + ) + self.position_metadata[order["data"]["trade_id"]] = metadata + + pos_lmts = PositionLimits( + delta=scaled_limit, + dte=self.config.dte_threshold, + creation_date=state.request.date, + ) + state.limits = pos_lmts + + self._save_position_limits(order["data"]["trade_id"], order["signal_id"], pos_lmts) + self._store_metadata(metadata) + logger.debug(f"Stored momentum metadata for trade ID {order['data']['trade_id']}: {metadata}") + + + def _save_position_limits(self, trade_id: str, signal_id: str, limits: PositionLimits) -> None: + """ + Save the position limits for future reference. + + Args: + trade_id: The unique identifier for the trade. + signal_id: The identifier for the signal that generated the trade. + limits: The PositionLimits object containing the limits to be saved. + Returns: + None + """ + self.position_limits[trade_id] = limits + logger.debug(f"Saved position limits for trade ID {trade_id}: {limits}") + + def _store_metadata(self, metadata: _DonchianLimitsMetaData) -> None: + """ + Store the metadata associated with a position for future analysis. + + Args: + metadata: The _DonchianLimitsMetaData object containing the metadata to be stored. + Returns: + None + """ + self.position_metadata[metadata.trade_id] = metadata + logger.debug(f"Stored metadata for trade ID {metadata.trade_id}: {metadata}") + + def get_momentum_scaler( + self, + *, + breakout_score: float, + rvol: float, + min_mult: float = 0.75, + max_mult: float = 1.50, + ) -> float: + """ + Compute bounded momentum sizing multiplier using: + - breakout score + - realized volatility + + Returns + ------- + float + Position sizing multiplier. + """ + + + mult = 1.0 + + # -------------------------------------------------- + # Breakout Score + # -------------------------------------------------- + + if not pd.isna(breakout_score): + # strongest breakout regimes + if breakout_score >= 2: + mult *= 1.25 + + elif breakout_score >= 1: + mult *= 1.10 + + # weak breakout + elif breakout_score < 0.25: + mult *= 0.90 + + # -------------------------------------------------- + # Realized Volatility + # -------------------------------------------------- + + if not pd.isna(rvol): + # strongest observed region + if 0.60 <= rvol < 1.00: + mult *= 1.25 + + elif 0.40 <= rvol < 0.60: + mult *= 1.10 + + # compressed / weak momentum + elif rvol < 0.15: + mult *= 0.85 + + # ultra-extreme / unstable + elif rvol >= 1.00: + mult *= 0.95 + + # -------------------------------------------------- + # Final Clamp + # -------------------------------------------------- + mult = max(min_mult, mult) + mult = min(max_mult, mult) + + return float(mult) + + def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: + """ + Analyze open positions for DTE-based roll triggers. + + Process: + 1. Iterate over all open positions in portfolio + 2. Calculate DTE using get_dte_and_moneyness_from_trade_id() + 3. If dte_limit_enabled and dte < dte_threshold: + - Create ROLL opinion with reason + 4. Return aggregated CogActions + + Args: + context: Portfolio snapshot with positions, date, and backtest parameters. + + Returns: + CogActions: Opinions generated by this cog (roll recommendations). + """ + opinions = [] + + if not self.config.dte_limit_enabled: + logger.debug("DTE limit disabled; no roll checks performed") + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) + + positions = context.portfolio.positions + portfolio_meta = context.portfolio_meta + last_updated = context.portfolio.last_updated + t_plus_n = portfolio_meta.t_plus_n + t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) + backtest_start = portfolio_meta.start_date + + for pos_state in positions: + try: + # Calculate DTE using exact same method as limits cog + dte, _ = get_dte_and_moneyness_from_trade_id( + trade_id=pos_state.trade_id, + check_date=pos_state.last_updated, + check_price=pos_state.current_underlier_data.chain_spot["close"], + start=backtest_start, + is_backtest=portfolio_meta.is_backtest, + ) + + qty = pos_state.quantity + + # Check roll threshold + if dte < self.config.dte_threshold: + logger.info( + f"Position {pos_state.trade_id}: DTE {dte} below threshold {self.config.dte_threshold}. " + f"Recommending ROLL." + ) + + action = ROLL( + trade_id=pos_state.trade_id, + action=Changes(quantity_diff=0, new_quantity=qty), + ) + action.reason = ( + f"DTE {dte} below threshold {self.config.dte_threshold}. Rolling to extend duration." + ) + action.analysis_date = last_updated + action.effective_date = last_updated + t_plus_n_bdays + + # Create opinion for this position + action.verbose_info = ( + f"Analysis: {action.analysis_date} | Effective: {action.effective_date} | " + f"Trade: {pos_state.trade_id} | DTE: {dte} | Threshold: {self.config.dte_threshold}" + ) + pos_state.action = action + opinions.append(pos_state) + else: + logger.debug( + f"Position {pos_state.trade_id}: DTE {dte} >= threshold {self.config.dte_threshold}. No action." + ) + + except Exception as e: + logger.warning(f"Error calculating DTE for position {pos_state.trade_id}: {e}. Skipping roll check.") + logger.warning(f"Stack trace: ", exc_info=True) + continue + + logger.info( + f"VectorizedCog analysis complete. {len(opinions)} roll opinion(s) generated for {len(positions)} position(s)." + ) + + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) + + + diff --git a/EventDriven/riskmanager/position/cogs/mean_reversion.py b/EventDriven/riskmanager/position/cogs/mean_reversion.py index 8289706..95a28e9 100644 --- a/EventDriven/riskmanager/position/cogs/mean_reversion.py +++ b/EventDriven/riskmanager/position/cogs/mean_reversion.py @@ -1,5 +1,4 @@ import math - from EventDriven.riskmanager.position.base import BaseCog from EventDriven.configs.core import MeanReversionSizerConfigs from typing import Optional diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index b2cf104..5d3cf2d 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -95,12 +95,18 @@ class PnLMonitorCog(BaseCog): default_config = PnlMonitorConfig() - def __init__(self, config: Optional[PnlMonitorConfig] = None): + def __init__( + self, + config: Optional[PnlMonitorConfig] = None, + max_trade_dollar_size: Optional[float] = None, + ): """Initialize the PnL monitor cog. Args: config: Optional runtime configuration. If not provided, a default `PnlMonitorConfig` is created. + max_trade_dollar_size: Optional cap on trade dollar size to prevent excessive allocation when scaling up with profits. If None, no cap is applied. + Notes: - `enable_stop_loss` is initialized to ``False`` so the stop-loss @@ -112,6 +118,7 @@ def __init__(self, config: Optional[PnlMonitorConfig] = None): super().__init__(config) self.config = config self.enable_stop_loss = False # Enable stop loss by default + self.max_trade_dollar_size = max_trade_dollar_size def on_new_position(self, new_position_state: NewPositionState) -> None: """Handle a newly created position state. @@ -154,6 +161,17 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: ## Scale to dollar amount for easier interpretation tick_cash = tick_cash * 100 if not new_request_state.is_tick_cash_scaled else tick_cash + ## If max_trade_dollar_size is set, cap the tick_cash to prevent excessive allocation + if self.max_trade_dollar_size is not None and tick_cash > self.max_trade_dollar_size: + logger.info( + f"Tick cash of ${tick_cash:.2f} exceeds max_trade_dollar_size of ${self.max_trade_dollar_size:.2f}. Capping tick cash to max_trade_dollar_size." + ) + + ## If tick_cash > max_trade_dollar_size, tick_cash = max_trade_dollar_size + ## If tick_cash <= max_trade_dollar_size, tick_cash remains unchanged + new_request_state.tick_cash = min(tick_cash, self.max_trade_dollar_size) + return + ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. if pnl is not None and pnl > 0: additional_tick_cash = ( diff --git a/EventDriven/riskmanager/position/cogs/vectorized.py b/EventDriven/riskmanager/position/cogs/vectorized.py new file mode 100644 index 0000000..50b34a7 --- /dev/null +++ b/EventDriven/riskmanager/position/cogs/vectorized.py @@ -0,0 +1,191 @@ +"""VectorizedCog: Lightweight position monitoring and validation cog. + +Provides minimal, stateless position analysis focusing on: + 1. New position validation: Cash availability check (informational) + 2. Ongoing position analysis: DTE-based roll triggers + +Core Class: + VectorizedCog: Enforces DTE-based rolling and validates new positions + +Configuration: + VectorizedCogConfig: Controls dte_limit_enabled and dte_threshold + +Processing Flow: + on_new_position(): + - Called after position created + - Validates cash sufficient for position quantity + - Logs result (informational only; position already committed) + + _analyze_impl(): + - Iterates open positions + - Computes DTE using get_dte_and_moneyness_from_trade_id() + - Emits ROLL opinion if dte_limit_enabled and dte < dte_threshold + - Returns CogActions with roll recommendations + +DTE Calculation: + Uses analyze_utils.get_dte_and_moneyness_from_trade_id(): + - Parses trade_id via parse_position_id() + - Extracts exp_date from all legs + - Calculates: (exp_date - check_date).days + - Returns minimum DTE across legs + +Usage: + >>> config = VectorizedCogConfig(dte_threshold=30) + >>> cog = VectorizedCog(config=config) + >>> analyzer.add_cog(cog) + +Design: + - Minimal logic: Single responsibility (DTE monitoring) + - Stateless: No position tracking or metadata storage + - Reusable: Works alongside other cogs (LimitsAndSizingCog, PnLMonitorCog) + - Extensible: Name "Vectorized" signals future batch/SIMD optimizations +""" + +import pandas as pd +from typing import Optional +from EventDriven.riskmanager.position.base import BaseCog +from EventDriven.dataclasses.states import NewPositionState, PositionAnalysisContext, CogActions +from EventDriven.riskmanager.actions import ROLL, Changes +from EventDriven.configs.core import VectorizedCogConfig +from .analyze_utils import get_dte_and_moneyness_from_trade_id +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.position.cogs.vectorized", stream_log_level="INFO") + + +class VectorizedCog(BaseCog): + """ + Lightweight position cog for DTE-based rolling and new position validation. + + This cog enforces a single roll criterion: if a position's days-to-expiration + falls below a configurable threshold, emit a ROLL opinion. Additionally, it + provides an informational hook on new position creation to log cash validation. + """ + + default_config = VectorizedCogConfig() + + def __init__(self, config: Optional[VectorizedCogConfig] = None): + """Initialize the VectorizedCog. + + Args: + config: Optional runtime configuration. Defaults to VectorizedCogConfig(). + """ + if config is None: + config = VectorizedCogConfig() + super().__init__(config) + + def on_new_position(self, new_pos_state: NewPositionState) -> None: + """ + Hook called when a new position is detected (post-creation). + + Validates that available cash is sufficient for the position quantity. + This is informational only—position creation is already committed by RiskManager. + + Args: + new_pos_state: The new position state after creation. + + Returns: + None (no modifications; hook is logging-only). + """ + try: + order = new_pos_state.order + request = new_pos_state.request + option_price = new_pos_state.at_time_data.get_price() + quantity = order["data"]["quantity"] + tick_cash = request.tick_cash + + position_cost = quantity * option_price * 100 # Option contracts are $100 notional per contract + can_afford = tick_cash >= position_cost + + status = "SUFFICIENT" if can_afford else "INSUFFICIENT" + logger.info( + f"New position {order['data']['trade_id']}: " + f"Cash validation: {status}. " + f"Cost=${position_cost:.2f}, Available=${tick_cash:.2f}, " + f"Quantity={quantity}, Option Price=${option_price:.4f}" + ) + except Exception as e: + logger.warning(f"Error during cash validation for new position: {e}") + + def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: + """ + Analyze open positions for DTE-based roll triggers. + + Process: + 1. Iterate over all open positions in portfolio + 2. Calculate DTE using get_dte_and_moneyness_from_trade_id() + 3. If dte_limit_enabled and dte < dte_threshold: + - Create ROLL opinion with reason + 4. Return aggregated CogActions + + Args: + context: Portfolio snapshot with positions, date, and backtest parameters. + + Returns: + CogActions: Opinions generated by this cog (roll recommendations). + """ + opinions = [] + + if not self.config.dte_limit_enabled: + logger.debug("DTE limit disabled; no roll checks performed") + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) + + positions = context.portfolio.positions + portfolio_meta = context.portfolio_meta + last_updated = context.portfolio.last_updated + t_plus_n = portfolio_meta.t_plus_n + t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) + backtest_start = portfolio_meta.start_date + + for pos_state in positions: + try: + # Calculate DTE using exact same method as limits cog + dte, _ = get_dte_and_moneyness_from_trade_id( + trade_id=pos_state.trade_id, + check_date=pos_state.last_updated, + check_price=pos_state.current_underlier_data.chain_spot["close"], + start=backtest_start, + is_backtest=portfolio_meta.is_backtest, + ) + + qty = pos_state.quantity + + # Check roll threshold + if dte < self.config.dte_threshold: + logger.info( + f"Position {pos_state.trade_id}: DTE {dte} below threshold {self.config.dte_threshold}. " + f"Recommending ROLL." + ) + + action = ROLL( + trade_id=pos_state.trade_id, + action=Changes(quantity_diff=0, new_quantity=qty), + ) + action.reason = ( + f"DTE {dte} below threshold {self.config.dte_threshold}. Rolling to extend duration." + ) + action.analysis_date = last_updated + action.effective_date = last_updated + t_plus_n_bdays + + # Create opinion for this position + action.verbose_info = ( + f"Analysis: {action.analysis_date} | Effective: {action.effective_date} | " + f"Trade: {pos_state.trade_id} | DTE: {dte} | Threshold: {self.config.dte_threshold}" + ) + pos_state.action = action + opinions.append(pos_state) + else: + logger.debug( + f"Position {pos_state.trade_id}: DTE {dte} >= threshold {self.config.dte_threshold}. No action." + ) + + except Exception as e: + logger.warning(f"Error calculating DTE for position {pos_state.trade_id}: {e}. Skipping roll check.") + logger.warning(f"Stack trace: ", exc_info=True) + continue + + logger.info( + f"VectorizedCog analysis complete. {len(opinions)} roll opinion(s) generated for {len(positions)} position(s)." + ) + + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) diff --git a/EventDriven/riskmanager/special_dividends.yaml b/EventDriven/riskmanager/special_dividends.yaml index a0ce706..e028fd6 100644 --- a/EventDriven/riskmanager/special_dividends.yaml +++ b/EventDriven/riskmanager/special_dividends.yaml @@ -9,6 +9,7 @@ no_special_dividends: - AMZN - AMD - NFLX + - SPY special_dividends: COST: From 5827a65cce7a900572fec8038b845c8312945b7f Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sat, 23 May 2026 00:33:36 -0400 Subject: [PATCH 62/81] Improve trade plotting, signal tagging, and temp cache isolation --- EventDriven/new_portfolio.py | 70 +++++++++++++++++++--- trade/backtester_/_helper.py | 6 +- trade/backtester_/_multi_asset_strategy.py | 2 +- trade/helpers/helper.py | 6 ++ 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index 20ef1c3..1fe3c00 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -42,8 +42,10 @@ from trade.helpers.helper import change_to_last_busday, is_USholiday from trade.backtester_.utils.aggregators import AggregatorParent from trade.backtester_.utils.utils import plot_portfolio -from typing import Optional, Union +from typing import Optional, Union, List import plotly +import plotly.graph_objects as go +from plotly.subplots import make_subplots from EventDriven.dataclasses.states import ( AtTimePositionData, PositionState, @@ -679,7 +681,7 @@ def create_order(self, signal_event: SignalEvent, position_type: str, order_type direction=signal_event.signal_type, signal_id=signal_event.signal_id, signal_total_pnl=self._get_signal_total_pnl(signal_event.signal_id), - symbol_total_pnl=self._get_symbol_total_pnl(signal_event.symbol) + symbol_total_pnl=self._get_symbol_total_pnl(signal_event.symbol), ) ) self.position_cache[cache_key] = position_state @@ -719,7 +721,7 @@ def _get_signal_total_pnl(self, signal_id: str): if trade.signal_id == signal_id: total_pnl = total_pnl + trade.total_pnl return total_pnl - + def _get_symbol_total_pnl(self, symbol: str): """ Helper function to calculate total PnL for a given symbol across all trades. @@ -801,7 +803,7 @@ def analyze_signal(self, event: SignalEvent): if order_event is not None: self.eventScheduler.put(order_event) - def analyze_positions(self) -> StrategyChangeMeta: + def analyze_positions(self, dt: Optional[pd.Timestamp] = None) -> StrategyChangeMeta: """ Analyze the current positions and determine if any need to be rolled """ @@ -820,7 +822,7 @@ def analyze_positions(self) -> StrategyChangeMeta: ## Create Context for current positions ## Analyze positions using RiskManager ## Extract events from meta changes and schedule them - dt = pd.to_datetime(self.eventScheduler.current_date) + dt = pd.to_datetime(dt or self.eventScheduler.current_date) if self._is_holiday(dt): self.logger.warning(f"Market is closed on {dt}, skipping") return @@ -943,7 +945,6 @@ def _construct_position( "quantity": quantity, "entry_price": entry_price, "market_value": close * quantity * 100, - } return position @@ -987,7 +988,7 @@ def _create_ctx(self, date: pd.Timestamp, positions: dict = None) -> PositionAna entry_date=entry_date, trades=trade_obj, signal_total_pnl=self._get_signal_total_pnl(signal_id), - symbol_total_pnl=self._get_symbol_total_pnl(tick) + symbol_total_pnl=self._get_symbol_total_pnl(tick), ) positions_states.append(pos_state) @@ -1358,6 +1359,61 @@ def get_trade_timeseries(self, trade_id: str, signal_id: Optional[str] = None) - return position_slice.loc[start_dt.normalize() : end_dt.normalize()] + def plot_trade_timeseries( + self, + trade_id: str, + y1: str = "s0_close", + y2: Optional[str] = "Midpoint", + columns: Optional[List[str]] = None, + title: Optional[str] = None, + ) -> go.Figure: + """Plot trade timeseries data with optional dual y-axis. + + Args: + trade_id: Trade identifier string (e.g., "&L:TSLA20210115C480"). + y1: Column name for primary y-axis. + y2: Column name for secondary y-axis. If None, only y1 is plotted. + columns: Optional list of columns to retrieve from trade timeseries. + title: Optional title for the plot. + + Returns: + Interactive plotly figure with single or dual y-axis. + """ + trade_df = self.get_trade_timeseries(trade_id) + if columns is not None: + missing_columns = [col for col in columns if col not in trade_df.columns] + if missing_columns: + raise ValueError( + f"Columns {missing_columns} not found in trade timeseries. Available: {trade_df.columns.tolist()}" + ) + df = trade_df[columns] + else: + df = trade_df + + if y1 not in df.columns: + raise ValueError(f"Column '{y1}' not found in trade timeseries. Available: {df.columns.tolist()}") + if y2 is not None and y2 not in df.columns: + raise ValueError(f"Column '{y2}' not found in trade timeseries. Available: {df.columns.tolist()}") + + if title is None: + title = f"Trade Timeseries: {trade_id}" + + if y2 is None: + fig = go.Figure() + fig.add_trace(go.Scatter(x=df.index, y=df[y1], name=y1, mode="lines")) + fig.update_layout(title=title, xaxis_title="Date", yaxis_title=y1) + return fig + + fig = make_subplots(specs=[[{"secondary_y": True}]]) + fig.add_trace(go.Scatter(x=df.index, y=df[y1], name=y1, mode="lines"), secondary_y=False) + fig.add_trace(go.Scatter(x=df.index, y=df[y2], name=y2, mode="lines"), secondary_y=True) + + fig.update_xaxes(title_text="Date") + fig.update_yaxes(title_text=y1, secondary_y=False) + fig.update_yaxes(title_text=y2, secondary_y=True) + fig.update_layout(title=title) + return fig + def get_at_time_position_data(self, position_id: TradeID, date=None) -> AtTimePositionData: """ Get the position data at a given time diff --git a/trade/backtester_/_helper.py b/trade/backtester_/_helper.py index c1a2525..d65ed79 100644 --- a/trade/backtester_/_helper.py +++ b/trade/backtester_/_helper.py @@ -90,11 +90,11 @@ def _next(self): if open_decision.side == 1: if verbose: print("Going LONG") - self.buy() + self.buy(tag=open_decision.signal_id) elif open_decision.side == -1: if verbose: print("Going SHORT") - self.sell() + self.sell(tag=open_decision.signal_id) else: raise ValueError(f"Invalid side in open_decision: {open_decision.side}") self.brain.open_action( @@ -111,7 +111,7 @@ def _next(self): if verbose: print(f"Closing position on {date} at price {self.data.Close[-1]}") print(f"Info: {self.brain.info_on_date(date=date)}") - self.position.close() + self.position.close(tag=close_decision.signal_id) self.brain.close_action(date=date) # Create the Strategy subclass dynamically diff --git a/trade/backtester_/_multi_asset_strategy.py b/trade/backtester_/_multi_asset_strategy.py index 3ff788a..6b3e570 100644 --- a/trade/backtester_/_multi_asset_strategy.py +++ b/trade/backtester_/_multi_asset_strategy.py @@ -421,7 +421,7 @@ def close_action(self, ticker: str, current_date: str): """ strategy = self.get_strategy(ticker) self.current_open_positions[ticker] = False - return strategy.close_action(current_date) + return strategy.close_action(date = current_date, index = None) def info_on_date(self, ticker: str, current_date: str) -> Dict[str, Any]: """ diff --git a/trade/helpers/helper.py b/trade/helpers/helper.py index 21e1393..3b22c57 100644 --- a/trade/helpers/helper.py +++ b/trade/helpers/helper.py @@ -230,6 +230,12 @@ def __init__( if location else Path(os.environ.get("WORK_DIR")) / ".cache" / fname ) + + # 1.1 If clear_on_exit is True, dir becomes a folder ending with _tmp and each new instance is a randomly generated subfolder within it. + # This is to ensure that multiple instances of CustomCache with clear_on_exit=True do not interfere with each other and can be safely cleared on exit without affecting other caches. + if clear_on_exit: + dir = dir.with_name(f"{dir.name}_tmp") / shortuuid.random(length=8) + self.dir = dir self.fname = fname self.expiry_date = ( From 65a3105ad99ac51add6dcfc82a64f9c1c05a1393 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sat, 23 May 2026 01:01:47 -0400 Subject: [PATCH 63/81] Add global datamanager cache toggle and guard cache write paths --- trade/datamanager/base.py | 18 ++++---- trade/datamanager/dividend.py | 45 +++++++++++-------- .../market_data_helpers/dividends.py | 33 +++++++++----- trade/datamanager/utils/cache.py | 9 ++++ trade/datamanager/utils/date.py | 38 ++++++++-------- trade/datamanager/vars.py | 30 +++++++++++++ 6 files changed, 118 insertions(+), 55 deletions(-) diff --git a/trade/datamanager/base.py b/trade/datamanager/base.py index 7ecbc76..7ac9b9e 100644 --- a/trade/datamanager/base.py +++ b/trade/datamanager/base.py @@ -6,9 +6,10 @@ from trade.helpers.helper import CustomCache from trade.helpers.Logging import setup_logger from pathlib import Path -from .vars import get_dm_gen_path +from .vars import get_dm_gen_path, get_enable_caching 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) @@ -74,7 +75,7 @@ def __init_subclass__(cls, **kwargs: Any) -> 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") @@ -122,14 +123,13 @@ def __init__( 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, @@ -137,8 +137,7 @@ def get_cache(cls) -> CustomCache: 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.""" @@ -198,6 +197,9 @@ 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 not get_enable_caching(): + logger.info(f"Caching disabled. Skipping cache write for key: {key}") + return if expire is None: expire = self.cache_spec.default_expire_seconds self.cache.set(key, value, expire=expire) @@ -224,7 +226,7 @@ def get_or_compute( force=True bypasses cache read, recomputes and overwrites cache. """ - if not force: + if get_enable_caching() and not force: hit = self.cache.get(key, default=None) if hit is not None: return hit # type: ignore[return-value] diff --git a/trade/datamanager/dividend.py b/trade/datamanager/dividend.py index c15d903..afcfe29 100644 --- a/trade/datamanager/dividend.py +++ b/trade/datamanager/dividend.py @@ -21,7 +21,7 @@ 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, get_dm_gen_path, load_name +from trade.datamanager.vars import get_times_series, get_dm_gen_path, load_name, get_enable_caching from trade.datamanager.config import OptionDataConfig from trade.datamanager.result import DividendsResult from trade.datamanager.base import BaseDataManager, CacheSpec @@ -44,6 +44,7 @@ location=get_dm_gen_path().as_posix(), fname="dividend_temp_cache", expire_days=1, clear_on_exit=True ) + class DividendDataManager(BaseDataManager): """Manages dividend data retrieval, caching, and schedule construction for a specific symbol. @@ -109,9 +110,7 @@ def __new__(cls, symbol: str, *args: Any, **kwargs: Any) -> "DividendDataManager cls.INSTANCES[symbol] = instance return cls.INSTANCES[symbol] - def __init__( - self, symbol: str, *, enable_namespacing: bool = False - ) -> None: + 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 @@ -161,6 +160,10 @@ def cache_it(self, key: str, value: Any, *, expire: Optional[int] = None, _type: ## 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 not get_enable_caching(): + logger.info(f"Caching disabled. Skipping dividend cache write for key: {key}") + return + if _type == "discrete": existing = self.get(key, default=None) today = datetime.today().date() @@ -209,6 +212,7 @@ def get_div_yield_history(self, symbol: str) -> pd.Series: """ div_history = TS._get_dividend_yield_timeseries(symbol) return div_history + ## Discrete dividend schedule retrieval with caching. def get_discrete_dividend_schedule( self, @@ -387,20 +391,24 @@ def _get_discrete_schedule_timeseries( undo_adjust=undo_adjust, maturity=mat_str, ) - cached_series, is_partial, missing_start_date, missing_end_date = _check_cache_for_timeseries_data_structure(self, key, start_str, end_str) + cached_series, is_partial, missing_start_date, missing_end_date = _check_cache_for_timeseries_data_structure( + self, key, start_str, end_str + ) # cached_series = self.get(key, default=None) if cached_series is not None and not is_partial: logger.info(f"Cache hit for discrete schedule timeseries key: {key}") 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)) + (cached_series.index >= pd.to_datetime(start_date)) & (cached_series.index <= pd.to_datetime(end_date)) ] return cached_series, key else: - start_str, end_str = to_datetime(missing_start_date).strftime("%Y-%m-%d"), to_datetime(missing_end_date).strftime("%Y-%m-%d") - + start_str, end_str = ( + to_datetime(missing_start_date).strftime("%Y-%m-%d"), + to_datetime(missing_end_date).strftime("%Y-%m-%d"), + ) + # Build from scratch for missing dates # Fetch ONCE: all events from start_date to maturity_date full_schedule, _ = self.get_discrete_dividend_schedule( @@ -438,7 +446,9 @@ def _get_discrete_schedule_timeseries( 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 = _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 @@ -574,7 +584,9 @@ def get_schedule( 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}") + 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: @@ -583,7 +595,9 @@ def get_schedule( ## 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) + 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) @@ -597,9 +611,6 @@ def get_schedule( 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 @@ -679,9 +690,8 @@ def rt( ) result.rt = True return result - - def offload(self, *args: Any, **kwargs: Any) -> None: + def offload(self, *args: Any, **kwargs: Any) -> None: """ Placeholder for offload logic (not implemented). @@ -697,4 +707,3 @@ def offload(self, *args: Any, **kwargs: Any) -> None: >>> div_mgr.offload() # No-op """ logger.info(f"No offload logic implemented for {self.CACHE_NAME}") - diff --git a/trade/datamanager/market_data_helpers/dividends.py b/trade/datamanager/market_data_helpers/dividends.py index 5ab2398..d7430a6 100644 --- a/trade/datamanager/market_data_helpers/dividends.py +++ b/trade/datamanager/market_data_helpers/dividends.py @@ -2,15 +2,16 @@ from openbb import obb import pandas as pd from trade.optionlib.config.defaults import ( - OPTION_TIMESERIES_START_DATE, # noqa + 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 get_dm_gen_path +from trade.datamanager.vars import get_dm_gen_path, get_enable_caching 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") @@ -27,6 +28,8 @@ class SavedDividendsResult: DIVIDEND_CACHE = CustomCache( location=get_dm_gen_path().as_posix(), 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.""" @@ -57,13 +60,14 @@ def resample_dividends_to_daily(div_series: pd.Series, buffer: int = 30) -> pd.S resampled.index = pd.to_datetime(resampled.index, format="%Y-%m-%d") resampled.name = "dividend_amount" resampled.index.name = "datetime" - resampled.sort_index(inplace=True) + resampled.sort_index(inplace=True) return resampled + def get_div_schedule( - ticker: str, - filter_specials: bool = None, - ) -> pd.Series: + ticker: str, + filter_specials: bool = None, +) -> pd.Series: """ 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. @@ -82,14 +86,14 @@ def get_div_schedule( # Check if ticker is in cache if filter_specials is None: filter_specials = OptionDataConfig().filter_out_special_dividends - key = (ticker, filter_specials) - if key not in DIVIDEND_CACHE: + + def _fetch_from_source() -> SavedDividendsResult: 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( + return SavedDividendsResult( symbol=ticker, historicals=div_history["amount"], resampled_timeseries=None, @@ -99,12 +103,18 @@ def get_div_schedule( div_history = pd.DataFrame( {"amount": [0]}, index=pd.bdate_range(start="2001-01-01", end=datetime.now(), freq="1Q") ) - dividends_data = SavedDividendsResult( + return SavedDividendsResult( symbol=ticker, historicals=div_history["amount"], resampled_timeseries=None, last_updated=datetime.now(), ) + + key = (ticker, filter_specials) + if not get_enable_caching(): + dividends_data = _fetch_from_source() + elif key not in DIVIDEND_CACHE: + dividends_data = _fetch_from_source() DIVIDEND_CACHE[key] = dividends_data else: @@ -113,7 +123,7 @@ def get_div_schedule( # 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) + return get_div_schedule(ticker, filter_specials=filter_specials) # Filter out dividends >= 7.5 if filter_specials: @@ -121,6 +131,7 @@ def get_div_schedule( 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. diff --git a/trade/datamanager/utils/cache.py b/trade/datamanager/utils/cache.py index a91d854..016e501 100644 --- a/trade/datamanager/utils/cache.py +++ b/trade/datamanager/utils/cache.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from .date import _should_save_today, DATE_HINT from ..base import BaseDataManager +from ..vars import get_enable_caching from .data_structure import _data_structure_sanitize from trade.datamanager.exceptions import EmptyDataException from trade.datamanager.utils.logging import get_logging_level, UTILS_LOGGER_NAME @@ -247,6 +248,10 @@ def _cache_it_timeseries_data_structure( checked_missing_dates are merged with any already-stored checked-missing dates on the existing cache entry, so coverage knowledge accumulates across writes. """ + if not get_enable_caching(): + logger.info(f"Caching disabled. Skipping cache write for key: {key}") + return + # Extract existing checked-missing dates before unwrapping _CachedData. existing_checked_missing: List[DATE_HINT] = [] if isinstance(existing, _CachedData): @@ -315,6 +320,10 @@ def _cache_it_timeseries_data_structure( 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 get_enable_caching(): + logger.info(f"Caching disabled. Skipping list cache write for key: {key}") + return + if not isinstance(value, list): raise TypeError(f"Expected list. Recieved {type(value)}") diff --git a/trade/datamanager/utils/date.py b/trade/datamanager/utils/date.py index 2d85c23..e9a294c 100644 --- a/trade/datamanager/utils/date.py +++ b/trade/datamanager/utils/date.py @@ -5,7 +5,7 @@ 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.datamanager.vars import TODAY_RELOAD_CUTOFF, MIN_TIME_BEFORE_REAL_TIME, get_enable_caching 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 @@ -170,7 +170,7 @@ def _guard_rail_dates( dates: Optional[list] = None, ) -> Tuple[datetime, datetime]: """Ensures start_date and end_date are within min_trade_date and max_trade_date.""" - + if start_date < min_trade_date: logger.warning( f"Requested start_date {start_date.date()} is before available data. Adjusting to {min_trade_date.date()}." @@ -264,24 +264,26 @@ def _compute_max_allowable() -> pd.Timestamp: # For non-expired options, max changes with time, so do not persist it. if is_expired: max_allowable = max_trade_date - LIST_DATE_CACHE.set( - key=opttick, - value={ - "min_date": pd.Timestamp(min_trade_date), - "max_date": pd.Timestamp(max_allowable), - "range": dates, - }, - expire=None, - ) + if get_enable_caching(): + LIST_DATE_CACHE.set( + key=opttick, + value={ + "min_date": pd.Timestamp(min_trade_date), + "max_date": pd.Timestamp(max_allowable), + "range": dates, + }, + expire=None, + ) else: max_allowable = _compute_max_allowable() - LIST_DATE_CACHE.set( - key=opttick, - value={ - "min_date": pd.Timestamp(min_trade_date), - }, - expire=None, - ) + if get_enable_caching(): + LIST_DATE_CACHE.set( + key=opttick, + value={ + "min_date": pd.Timestamp(min_trade_date), + }, + expire=None, + ) return _guard_rail_dates(start_date, end_date, min_trade_date, max_allowable, dates=dates) diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index 29e0205..32f57da 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -23,6 +23,36 @@ 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 _parse_bool_env(var_name: str, default: bool = True) -> bool: + raw = os.getenv(var_name) + if raw is None: + return default + + value = raw.strip().lower() + if value in {"1", "true", "t", "yes", "y", "on"}: + return True + if value in {"0", "false", "f", "no", "n", "off"}: + return False + + logger.warning( + f"Invalid boolean value '{raw}' for env var {var_name}. Falling back to default={default}." + ) + return default + + +ENABLE_CACHING: bool = _parse_bool_env("DATAMANAGER_ENABLE_CACHE", default=True) + + +def get_enable_caching() -> bool: + global ENABLE_CACHING + return ENABLE_CACHING + + +def set_enable_caching(enabled: bool) -> None: + global ENABLE_CACHING + ENABLE_CACHING = bool(enabled) + def get_dm_gen_path(is_live: bool = None) -> Path: from .config import OptionDataConfig if is_live is None: From 4b299912db004a8bf4f1ccbf56a886360f3baa16 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sat, 23 May 2026 01:01:47 -0400 Subject: [PATCH 64/81] Update pnl monitor logic --- EventDriven/riskmanager/position/cogs/pnl_monitor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 5d3cf2d..0a6f0fe 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -98,7 +98,7 @@ class PnLMonitorCog(BaseCog): def __init__( self, config: Optional[PnlMonitorConfig] = None, - max_trade_dollar_size: Optional[float] = None, + max_trade_dollar_size: Optional[float] = None ): """Initialize the PnL monitor cog. @@ -106,7 +106,7 @@ def __init__( config: Optional runtime configuration. If not provided, a default `PnlMonitorConfig` is created. max_trade_dollar_size: Optional cap on trade dollar size to prevent excessive allocation when scaling up with profits. If None, no cap is applied. - + Notes: - `enable_stop_loss` is initialized to ``False`` so the stop-loss @@ -162,14 +162,15 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: tick_cash = tick_cash * 100 if not new_request_state.is_tick_cash_scaled else tick_cash ## If max_trade_dollar_size is set, cap the tick_cash to prevent excessive allocation - if self.max_trade_dollar_size is not None and tick_cash > self.max_trade_dollar_size: + if self.max_trade_dollar_size is not None: logger.info( - f"Tick cash of ${tick_cash:.2f} exceeds max_trade_dollar_size of ${self.max_trade_dollar_size:.2f}. Capping tick cash to max_trade_dollar_size." + f"Max trade dollar size is set to ${self.max_trade_dollar_size:.2f}. Original tick cash: ${tick_cash:.2f}." ) ## If tick_cash > max_trade_dollar_size, tick_cash = max_trade_dollar_size ## If tick_cash <= max_trade_dollar_size, tick_cash remains unchanged new_request_state.tick_cash = min(tick_cash, self.max_trade_dollar_size) + new_request_state.is_tick_cash_scaled = True # Mark as scaled since we're treating it as a dollar amount now return ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. From f7676ee4db62bbf0cff20ce1309199c5065a8030 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 25 May 2026 19:25:13 -0400 Subject: [PATCH 65/81] chore: add copilot workflow skills and gitignore updates --- .github/copilot-instructions.md | 23 ++++++ .github/skills/commit-strategy/SKILL.md | 67 +++++++++++++++++ .github/skills/docstring-standards/SKILL.md | 83 +++++++++++++++++++++ .gitignore | 3 + trade/backtester_/_helper.py | 2 +- 5 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 .github/skills/commit-strategy/SKILL.md create mode 100644 .github/skills/docstring-standards/SKILL.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c707e35..7a75941 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,6 +5,16 @@ This is a quantitative trading system focused on options pricing and risk manage ## Code Style & Standards +### Commit Requests (Always-On) +- Every time the user asks for a commit, first generate a commit strategy. +- Separate changes into the most relevant concerns before committing. +- For each planned commit, state: + - What will be committed (files/hunks grouped by concern). + - The commit message. +- Keep unrelated concerns in separate commits. +- Use the commit strategy skill for detailed grouping and message rules: + - `.github/skills/commit-strategy/SKILL.md` + ### 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 @@ -41,6 +51,19 @@ date_obj = to_datetime(datetime.now()) - Include Args, Returns, Raises, and Examples sections - Examples should be executable and demonstrate real-world usage +### Strict Docstring Policy (Always-On) +- These rules are mandatory for every Python file that is created or edited. +- Follow PEP 257 conventions for all docstrings. +- Use Google-style docstring sections. +- Every module must have a module-level docstring as the first statement. +- Every class must have a class docstring. +- Every function and method must have a docstring, including `__init__` and private helpers. +- Every docstring must include a concise summary line. +- Add `Args`, `Returns`, and `Raises` sections when applicable. +- Include an `Examples` section with executable usage when practical. +- Keep docstrings factual and behavior-focused; avoid placeholder text. +- When updating code, also update stale docstrings in the same edited scope. + ### Module-Level Docstring Format - All Python modules must start with a module docstring as the very first statement. - Use this schema for module docstrings: diff --git a/.github/skills/commit-strategy/SKILL.md b/.github/skills/commit-strategy/SKILL.md new file mode 100644 index 0000000..79f5278 --- /dev/null +++ b/.github/skills/commit-strategy/SKILL.md @@ -0,0 +1,67 @@ +# Commit Strategy Skill + +Use this skill whenever the user asks to create commits, propose commits, or requests a commit plan. + +## Objectives +- Separate code changes into the most relevant concerns. +- Produce a clear commit strategy before committing. +- Ensure each commit is cohesive, minimal, and reversible. + +## Required Workflow +1. Inspect changed files and classify each change by concern. +2. Run a pre-commit quality scan on changed files for: + - Potential bugs. + - Failure in logic. + - Misspellings. + - Commented-out code blocks (do not flag plain explanatory comments). +3. Group files/hunks into concern-based commit buckets. +4. Present a commit strategy that lists: + - What each commit includes. + - Why the grouping is correct. + - The commit message for each commit. + - A concise scan summary of findings (or explicitly state no findings). +5. Keep unrelated changes out of the same commit. +6. Prefer multiple small focused commits over one mixed commit. + +## Concern Grouping Rules +- Separate by behavior surface (feature, bug fix, refactor, docs, tests, config). +- Separate by subsystem when changes are independent. +- Keep tests with the behavior they verify. +- Keep formatting-only changes isolated when possible. +- Do not mix broad cleanup with functional changes. + +## Commit Message Rules +- Use imperative mood. +- Keep the subject concise and specific. +- Explain intent, not implementation noise. +- Suggested style: `: ` + +## Output Template +When a commit is requested, provide: + +0. Pre-Commit Scan + - Potential bugs: + - Logic failures: + - Misspellings: + - Commented-out code blocks: + +1. Commit 1 + - Scope: + - Concern: + - Message: + +2. Commit 2 + - Scope: + - Concern: + - Message: + +## Example +1. Commit 1 + - Scope: `EventDriven/riskmanager/position/cogs/pnl_monitor.py` + - Concern: Fix quantity math and logging edge cases. + - Message: `riskmanager: fix pnl monitor quantity and logging bugs` + +2. Commit 2 + - Scope: `EventDriven/riskmanager/position/cogs/pnl_monitor.py`, `EventDriven/riskmanager/position/cogs/test_pnl_monitor.py` + - Concern: Add regression coverage for updated behavior. + - Message: `tests: add pnl monitor regression cases` diff --git a/.github/skills/docstring-standards/SKILL.md b/.github/skills/docstring-standards/SKILL.md new file mode 100644 index 0000000..4e7ee9a --- /dev/null +++ b/.github/skills/docstring-standards/SKILL.md @@ -0,0 +1,83 @@ +# Docstring Standards Skill + +Use this skill when adding or revising Python docstrings in QuantTools. + +## Required Rules +- Follow PEP 257. +- Use Google-style docstrings. +- Add a module docstring as the first statement in every Python module. +- Add docstrings for every class. +- Add docstrings for every function and method, including `__init__` and private helpers. +- Keep summary lines concise and imperative where possible. +- Keep docs aligned with implementation; do not leave stale descriptions. + +## Required Sections +Use sections only when applicable: +- Args: +- Returns: +- Raises: +- Examples: + +## Module Docstring Template +```python +"""One-line module summary. + +Short 2-4 line overview of scope and purpose. + +Core Classes: + ClassA: Purpose. + +Core Functions: + fn_a: Purpose. + +Usage: + >>> result = fn_a(...) +""" +``` + +## Function Docstring Template +```python +def func(a: int, b: int) -> int: + """Return the combined score for two inputs. + + Args: + a: First input value. + b: Second input value. + + Returns: + Combined score. + + Raises: + ValueError: If either input is negative. + + Examples: + >>> func(2, 3) + 5 + """ +``` + +## Class and __init__ Template +```python +class Example: + """Represent an example service. + + Examples: + >>> ex = Example(name="demo") + >>> ex.name + 'demo' + """ + + def __init__(self, name: str) -> None: + """Initialize the example service. + + Args: + name: Service name. + """ +``` + +## Quality Checklist +- Summary line ends with a period. +- Line wrapping is readable and consistent. +- Sections are present only when needed. +- Types in docstrings do not contradict type hints. +- Example snippets are realistic and runnable where practical. diff --git a/.gitignore b/.gitignore index 4c7717d..fde0faa 100644 --- a/.gitignore +++ b/.gitignore @@ -191,6 +191,9 @@ EventDriven/Testing/text.txt ## Only relevant for me since I do the scheduling trade/helpers/*vol_resolve*.json +## Irrelavant misc files +trade/optionlib/model_issue_notes.txt + ## Pickle files *.pkl \qtenv diff --git a/trade/backtester_/_helper.py b/trade/backtester_/_helper.py index d65ed79..748a24b 100644 --- a/trade/backtester_/_helper.py +++ b/trade/backtester_/_helper.py @@ -111,7 +111,7 @@ def _next(self): if verbose: print(f"Closing position on {date} at price {self.data.Close[-1]}") print(f"Info: {self.brain.info_on_date(date=date)}") - self.position.close(tag=close_decision.signal_id) + self.position.close() self.brain.close_action(date=date) # Create the Strategy subclass dynamically From aaaa0dc5eef4cd10c1de106437160473e13d98f8 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 25 May 2026 19:25:13 -0400 Subject: [PATCH 66/81] feat: refine risk cogs and data manager workflows --- EventDriven/configs/core.py | 75 ++++++ EventDriven/execution.py | 4 +- EventDriven/riskmanager/market_timeseries.py | 34 +++ .../riskmanager/position/cogs/__init__.py | 13 +- .../riskmanager/position/cogs/donchian_cog.py | 229 +++++++++++------- .../position/cogs/mean_reversion.py | 151 ++++++++++-- .../riskmanager/position/cogs/plain_sizing.py | 223 +++++++++++++++++ .../riskmanager/position/cogs/pnl_monitor.py | 53 ++-- .../riskmanager/position/cogs/vectorized.py | 6 + EventDriven/riskmanager/utils.py | 8 +- trade/datamanager/option_spot.py | 8 +- trade/datamanager/result.py | 3 + trade/datamanager/utils/classification.py | 76 +++++- trade/datamanager/utils/model.py | 5 + trade/datamanager/vars.py | 11 +- 15 files changed, 741 insertions(+), 158 deletions(-) create mode 100644 EventDriven/riskmanager/position/cogs/plain_sizing.py diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index d9f0c9b..535cb21 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -337,6 +337,62 @@ class PnlMonitorConfig(BaseCogConfig): name: Optional[str] = "PnLMonitorCog" enabled: bool = True + @property + def enable_stop_loss(self) -> bool: + """ + Enable dynamic stop loss based on position characteristics or market conditions. This property can be used to determine whether to apply stop loss logic in the PnLMonitorCog. For example, you might want to enable stop loss for certain high-risk positions or during volatile market conditions, while keeping it disabled for more stable positions or in calmer markets. The actual logic for determining when to enable stop loss would depend on your specific risk management strategy and could involve factors such as volatility, position size, time to expiration, or other relevant metrics. + """ + # Placeholder logic for dynamic stop loss enabling - this can be replaced with actual conditions based on market data or position characteristics + return False + + @property + def roll_profit_threshold(self) -> float: + """ + Dynamic property to determine the maximum PnL percentage threshold for rolling positions to lock in profits. + The idea is that if a position has achieved a certain percentage gain relative to its entry price, it may be prudent to roll the position to lock in those profits and potentially free up capital for new trades. + This threshold can help prevent giving back gains in volatile markets while still allowing for significant profit capture. + Note: This is a simplified example and should be tested and adjusted based on historical performance and risk tolerance. + """ + return 1.0 # For example, this could represent a 100% gain relative to entry price as a threshold for rolling to lock in profits. + + @property + def lock_in_profit_threshold(self) -> float: + """ + Threshold to determine when to close a portion of the position to lock in profits. For example, if set to 0.5, it would trigger when the position has achieved a 50% gain relative to its entry price, prompting the closure of half the position to secure profits while allowing the rest to run. + """ + return 0.5 + + @property + def stop_loss_pct(self) -> float: + """ + Threshold to determine the stop loss percentage for exiting positions to prevent further losses. For example, if set to -0.7, it would trigger when the position has incurred a 70% loss relative to its entry price, prompting the closure of the position to limit further losses. + """ + return -0.7 + + @property + def profit_lock_in_pct(self): + """ + This controls the amount of profit locked to be added to cash used in next trade. + For example, if a position is closed with a 50% gain and profit_lock_in_pct is 0.5, then an additional 25% of the entry price (which is 50% of the gain) + would be added to the cash available for the next trade. This allows for a portion of the profits to be reinvested while still locking in gains. + """ + return 0.25 + + +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) +class PnLMonitorConfigConfigurable(BaseCogConfig): + """ + Configurable version of PnLMonitorConfig that allows dynamic adjustment of thresholds. + """ + + roll_profit_threshold: float = 1.0 # Default to 100% gain threshold for rolling to lock in profits + stop_loss_pct: float = -0.7 # Default to 70% loss threshold for exiting positions + profit_lock_in_pct: float = 0.25 # Default to adding 25% of the gain back into cash for next trade + lock_in_profit_threshold: float = ( + 0.5 # Default to 50% gain threshold for closing half the position to lock in profits + ) + enable_stop_loss: bool = False # Default to having stop loss disabled, can be enabled based on position characteristics or market conditions + @pydantic_dataclass class VectorizedCogConfig(BaseCogConfig): @@ -352,3 +408,22 @@ class VectorizedCogConfig(BaseCogConfig): enabled: bool = True dte_limit_enabled: bool = True dte_threshold: int = 30 + + +@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) +class PlainSizingCogConfig(BaseCogConfig): + """Configuration dataclass for PlainSizingCog. + + Simple sizing cog that: + - Sizes new positions using default_delta_limit + - Applies a one-contract affordability fallback when quantity is zero + - Monitors open positions for DTE-based roll triggers + - Skips checks for strategies matching excluded slug tokens + """ + + name: str = "PlainSizingCog" + enabled: bool = True + sizing_lev: float = 1.0 + dte_limit_enabled: bool = True + dte_threshold: int = 30 + exclude_strategy_slug_tokens: List[str] = Field(default_factory=list) diff --git a/EventDriven/execution.py b/EventDriven/execution.py index a72acf7..b33fb5d 100644 --- a/EventDriven/execution.py +++ b/EventDriven/execution.py @@ -381,7 +381,7 @@ def execute_order(self, order_event: OrderEvent): # Recompute quantity downward if cost exceeds available cash unit_cost = price + self.commission_rate logger.info( - f"Unit cost: {unit_cost}, Cash available: {order_event.cash}, Direction: {order_event.direction}, Signal ID: {order_event.signal_id}" + f"Unit cost: {unit_cost}, Cash available: {order_event.cash}, Direction: {order_event.direction}, Signal ID: {order_event.signal_id}, slippage_pct: {slippage_pct:.4f}, slippage_value: {slippage_pct_value:.4f}" ) if order_event.direction == "BUY": max_affordable_quantity = math.floor(order_event.cash / unit_cost) @@ -398,7 +398,7 @@ def execute_order(self, order_event: OrderEvent): quantity -= 1 total_cost = quantity * price + self.commission_rate logger.info( - f"Max affordable quantity: {max_affordable_quantity}, Raw quantity: {raw_quantity}, Final quantity: {quantity}, Signal ID: {order_event.signal_id}, Total Cost: {quantity * price + self.commission_rate}, Cash: {order_event.cash}" + f"Max affordable quantity: {max_affordable_quantity}, Raw quantity: {raw_quantity}, Final quantity: {quantity}, Signal ID: {order_event.signal_id}, Total Cost: {quantity * price + self.commission_rate}, Cash: {order_event.cash}, slippage_pct: {slippage_pct:.4f}, slippage_value: {slippage_pct_value:.4f}" ) elif order_event.direction == "SELL": diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index bf1d95d..999f365 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -735,6 +735,7 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: 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. """ key = (opttick, to_datetime(check_date).strftime("%Y-%m-%d")) + all_optticks = [] if key in self.session_loaded_option_cache: logger.info( f"Option data for {opttick} on {check_date} already generated in session, returning cached data" @@ -798,6 +799,9 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: self.session_loaded_option_cache[key] = ( data ## Cache the loaded data for the session to avoid re-loading it if the same option and date is requested again during the session ) + self._option_data_sanity_check( + data=data, associated_optticks={opttick}, check_date=check_date + ) return data # If there are splits, we need to load the data for each tick after adjusting strikes @@ -855,10 +859,12 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: adj_data[cols] /= factor segments.append(adj_data) + all_optticks.append(adj_opttick) base_data = self.load_position_data(opttick).copy() base_data["adj_strike"] = meta["strike"] ## Original strike base_data["factor"] = 1.0 + all_optticks.append(opttick) first_event_date = to_adjust_split[0][0] if to_adjust_split else self.start_date if compare_dates.is_before(check_date, first_event_date): @@ -880,7 +886,35 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: start_date=window_start, end_date=window_end, ) + + self.session_loaded_option_cache[key] = ( final_data ## Cache the generated data for the session to avoid re-generating it if the same option and date is requested again during the session ) + self._option_data_sanity_check( + data=final_data, associated_optticks=set(all_optticks), check_date=check_date + ) ## Perform sanity check to ensure the generated option data includes the check date for all associated option ticks, and if not, clear the relevant cache entries to maintain cache integrity. return final_data + + def _option_data_sanity_check( + self, + data: pd.DataFrame, + associated_optticks: set[str], + check_date: Union[datetime, str], + ) -> None: + """Perform sanity checks on the option data for the associated option ticks.""" + if check_date not in data.index: + logger.warning(f"Check date {check_date} not found in option data index for associated ticks: {associated_optticks}") + + ## Delete the cached data for the associated option ticks. + for opttick in associated_optticks: + if opttick in self.options_cache: + logger.warning(f"Deleting cached option data for {opttick} due to missing check date {check_date}") + self.options_cache.pop(opttick, None) + session_key = (opttick, to_datetime(check_date).strftime("%Y-%m-%d")) + if session_key in self.session_loaded_option_cache: + logger.warning(f"Deleting cached session option data for {opttick} on {check_date} due to missing check date in options cache") + self.session_loaded_option_cache.pop(session_key, None) + + ## Finally, raise an error to indicate the issue with the option data for the associated ticks. + raise ValueError(f"Check date {check_date} not found in option data index for associated ticks: {associated_optticks}. Cached data for these ticks has been deleted to maintain cache integrity.") diff --git a/EventDriven/riskmanager/position/cogs/__init__.py b/EventDriven/riskmanager/position/cogs/__init__.py index dfcda81..5bdd627 100644 --- a/EventDriven/riskmanager/position/cogs/__init__.py +++ b/EventDriven/riskmanager/position/cogs/__init__.py @@ -15,6 +15,12 @@ - New position cash validation (informational) - Minimal, stateless position monitoring + PlainSizingCog (plain_sizing.py): + - default_delta_limit based quantity sizing + - Quantity fallback to 1 when affordable and computed size is 0 + - DTE-based roll trigger detection + - Strategy-slug exclusion checks for both sizing and analysis + Utilities: analyze_utils.py: - DTE calculation from position IDs @@ -36,7 +42,7 @@ - Opinion-based recommendation system Usage: - from EventDriven.riskmanager.position.cogs import LimitsAndSizingCog, VectorizedCog + from EventDriven.riskmanager.position.cogs import PlainSizingCog, VectorizedCog from EventDriven.riskmanager.position.cogs.analyze_utils import ( get_dte_and_moneyness_from_trade_id ) @@ -47,6 +53,7 @@ """ from EventDriven.riskmanager.position.cogs.vectorized import VectorizedCog -from EventDriven.configs.core import VectorizedCogConfig +from EventDriven.riskmanager.position.cogs.plain_sizing import PlainSizingCog +from EventDriven.configs.core import VectorizedCogConfig, PlainSizingCogConfig -__all__ = ["VectorizedCog", "VectorizedCogConfig"] +__all__ = ["VectorizedCog", "VectorizedCogConfig", "PlainSizingCog", "PlainSizingCogConfig"] diff --git a/EventDriven/riskmanager/position/cogs/donchian_cog.py b/EventDriven/riskmanager/position/cogs/donchian_cog.py index 4c7332e..13daff8 100644 --- a/EventDriven/riskmanager/position/cogs/donchian_cog.py +++ b/EventDriven/riskmanager/position/cogs/donchian_cog.py @@ -1,10 +1,24 @@ +"""Donchian momentum position sizing and roll opinion cog. + +Applies Donchian-specific sizing logic for new positions and emits DTE-based +ROLL opinions during analysis for Donchian strategy positions. + +Core Classes: + DonchianMomentumCogConfig: Runtime knobs for scaling and DTE checks. + DonchianMomentumCog: Position sizing and analysis implementation. + +Core Dataclasses: + _DonchianLimitsMetaData: Per-trade sizing metadata capture. + +Usage: + >>> cog = DonchianMomentumCog(eq_strategy=strategy) + >>> cog.on_new_position(state) +""" + from typing import Dict -import math from EventDriven.riskmanager.position.base import BaseCog -from EventDriven.configs.core import MeanReversionSizerConfigs from typing import Optional from EventDriven.dataclasses.states import NewPositionState, PositionAnalysisContext, CogActions -import numpy as np from EventDriven.riskmanager.sizer._utils import default_delta_limit, delta_position_sizing from EventDriven.riskmanager.position.cogs.limits import _LimitsMetaData from EventDriven.types import SignalID @@ -12,34 +26,37 @@ from dataclasses import dataclass import pandas as pd from EventDriven.configs.base import pydantic_dataclass -from EventDriven.configs.core import BaseConfigs, _CustomFrozenBaseConfigs, BaseCogConfig, StrategyLimitsEnabled -from pydantic import ConfigDict, Field +from EventDriven.configs.core import BaseCogConfig +from pydantic import ConfigDict from EventDriven.dataclasses.limits import PositionLimits -from .limits import LimitsAndSizingCog from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.actions import ROLL, Changes -from EventDriven.configs.core import VectorizedCogConfig from .analyze_utils import get_dte_and_moneyness_from_trade_id logger = setup_logger("EventDriven.riskmanager.position.cogs.donchian_cog") -@pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True), ) + +@pydantic_dataclass( + config=ConfigDict(arbitrary_types_allowed=True), +) class DonchianMomentumCogConfig(BaseCogConfig): """Configuration for DonchianMomentumCog.""" - + name: str = "DonchianMomentumCog" sizing_lev: int = 1 min_scale: float = 0.75 - max_scale: float = 1.50 + max_scale: float = 2.0 dte_limit_enabled: bool = True dte_threshold: int = 15 - + @dataclass class _DonchianLimitsMetaData(_LimitsMetaData): + """Metadata payload recorded for Donchian-sized positions.""" + breakout_score: float = None rvol: float = None - + class DonchianMomentumCog(BaseCog): """ @@ -49,21 +66,34 @@ class DonchianMomentumCog(BaseCog): the Donchian channel. It emits opinions on whether the position is showing positive or negative momentum, which can be used for risk management decisions. """ + default_config: DonchianMomentumCogConfig = DonchianMomentumCogConfig() - def __init__( - self, - eq_strategy: MultiAssetStrategy, - config: Optional[DonchianMomentumCogConfig] = None -): + + def __init__(self, eq_strategy: MultiAssetStrategy, config: Optional[DonchianMomentumCogConfig] = None): + """Initialize the Donchian momentum cog. + + Args: + eq_strategy: Strategy interface used to fetch indicator data. + config: Optional runtime configuration for sizing and analysis. + """ if config is None: config: DonchianMomentumCogConfig = DonchianMomentumCogConfig() - + + super().__init__(config) self._config: DonchianMomentumCogConfig = config - # super().__init__(config) self.eq_strategy = eq_strategy self.position_limits: Dict[str, PositionLimits] = {} self.position_metadata: Dict[str, _DonchianLimitsMetaData] = {} - + self.config: DonchianMomentumCogConfig = config + + def is_donchian_strategy(self, signal_id: str) -> bool: + """Return True when signal_id belongs to a Donchian strategy.""" + try: + return SignalID(signal_id).strategy_slug.startswith("donchian") + except Exception: + logger.warning(f"Unable to parse signal id {signal_id} for Donchian check.", exc_info=True) + return False + def on_new_position(self, state: NewPositionState) -> None: """ Analyze the momentum of a new position based on its entry price and the Donchian channel. @@ -74,72 +104,79 @@ def on_new_position(self, state: NewPositionState) -> None: None (this cog is for analysis and opinion generation, not direct action). """ order = state.order - requet = state.request + request = state.request ticker = state.symbol undl_data = state.undl_at_time_data chain_spot = undl_data.chain_spot["close"] option_chain = state.at_time_data - cash_available = requet.tick_cash + cash_available = ( + request.tick_cash if request.is_tick_cash_scaled else request.tick_cash * 100 + ) # Scale cash to match option notional if not already scaled signal_id = SignalID(order.signal_id) + if not self.is_donchian_strategy(order.signal_id): + logger.debug( + f"Skipping Donchian momentum sizing for non-Donchian signal {signal_id}. " + f"Trade ID: {order['data']['trade_id']}" + ) + return + opt_price = option_chain.get_price() date = order["date"] info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) breakout_score = info.get("momentum_breakout_score", 0) + delta = option_chain.delta rvol = info.get("momentum_rvol", 0) - if not signal_id.strategy_slug.startswith("donchian"): - logger.warning(f"Signal ID {signal_id} does not appear to be from a Donchian strategy. Skipping momentum analysis.") - q = 1 - order["data"]["quantity"] = q - breakout_score = None - rvol = None - delta = option_chain.delta - scaled_limit = 1 - scaler = None - - - else: - scaler = self.get_momentum_scaler( - breakout_score=breakout_score, - rvol=rvol, - min_mult=self.config.min_scale, - max_mult=self.config.max_scale - ) + scaler = self.get_momentum_scaler( + breakout_score=breakout_score, rvol=rvol, min_mult=self.config.min_scale, max_mult=self.config.max_scale + ) + rvol_str = f"{rvol:.4f}" if rvol is not None and not pd.isna(rvol) else "None" + logger.info( + f"Calculated momentum scaler: {scaler:.2f} for trade ID {order['data']['trade_id']} with breakout score {breakout_score} and RVOL {rvol_str}" + ) - base_limit = default_delta_limit( - cash_available=cash_available, - underlier_price_at_time=chain_spot, - sizing_lev=self.config.sizing_lev, - ) + base_limit = default_delta_limit( + cash_available=cash_available, + underlier_price_at_time=chain_spot, + sizing_lev=self.config.sizing_lev, + ) + + scaled_limit = base_limit * scaler + logger.info( + f"Base delta limit: {base_limit:.4f}, Scaled delta limit: {scaled_limit:.4f} for trade ID {order['data']['trade_id']} with momentum scaler {scaler:.2f}" + ) - scaled_limit = base_limit * scaler + ## Scale the delta down to 90% to allow room for natural delta movement and reduce overtrading risk + tgt_delta = scaled_limit * 0.9 - ## Scale the delta down to 90% to allow room for natural delta movement and reduce overtrading risk - tgt_delta = scaled_limit * 0.9 - delta = option_chain.delta + q = delta_position_sizing( + cash_available=cash_available, + option_price_at_time=opt_price, + delta=delta, + delta_limit=tgt_delta, + ) - q = delta_position_sizing( - cash_available=cash_available, - option_price_at_time=opt_price, - delta=delta, - delta_limit=tgt_delta, + if q == 0: + logger.warning( + f"Position {order['data']['trade_id']} has zero quantity after momentum scaling. " + f"Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}, " + f"Base Delta Limit: {base_limit:.4f}, Scaled Delta Limit: {scaled_limit:.4f}, " + f"Target Delta (90% of Scaled Limit): {tgt_delta:.4f}, Option Delta: {delta:.4f}" ) + if cash_available >= opt_price * 100: # Check if cash can afford at least 1 contract + logger.info( + f"Cash available (${cash_available:.2f}) is greater than option price (${opt_price * 100:.2f}), but quantity is zero." + f" Setting quantity to 1 to allow position opening and future momentum adjustments." + ) + q = 1 - if q == 0: + else: logger.warning( - f"Position {order['data']['trade_id']} has zero quantity after momentum scaling. " - f"Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}, " - f"Base Delta Limit: {base_limit:.4f}, Scaled Delta Limit: {scaled_limit:.4f}, " - f"Target Delta (90% of Scaled Limit): {tgt_delta:.4f}, Option Delta: {delta:.4f}" + f"Cash available (${cash_available:.2f}) is insufficient to afford even 1 contract at option price (${opt_price * 100:.2f}). Quantity remains zero." ) - if cash_available > opt_price: - logger.info( - f"Cash available (${cash_available:.2f}) is greater than option price (${opt_price:.2f}), but quantity is zero." - f" Setting quantity to 1 to allow position opening and future momentum adjustments." - ) - q = 1 - logger.info( - f"Calculated delta limit: {scaled_limit:.4f}, resulting quantity: {q} lev: {self.config.sizing_lev}. Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}. Trade ID: {order['data']['trade_id']} with option delta {delta:.4f}" - ) + + logger.info( + f"Calculated delta limit: {scaled_limit:.4f}, resulting quantity: {q} lev: {self.config.sizing_lev}. Breakout Score: {breakout_score}, RVOL: {rvol}, Scaler: {scaler:.2f}. Trade ID: {order['data']['trade_id']} with option delta {delta:.4f}" + ) order["data"]["quantity"] = q metadata = _DonchianLimitsMetaData( trade_id=order["data"]["trade_id"], @@ -167,7 +204,6 @@ def on_new_position(self, state: NewPositionState) -> None: self._save_position_limits(order["data"]["trade_id"], order["signal_id"], pos_lmts) self._store_metadata(metadata) logger.debug(f"Stored momentum metadata for trade ID {order['data']['trade_id']}: {metadata}") - def _save_position_limits(self, trade_id: str, signal_id: str, limits: PositionLimits) -> None: """ @@ -179,7 +215,7 @@ def _save_position_limits(self, trade_id: str, signal_id: str, limits: PositionL limits: The PositionLimits object containing the limits to be saved. Returns: None - """ + """ self.position_limits[trade_id] = limits logger.debug(f"Saved position limits for trade ID {trade_id}: {limits}") @@ -191,7 +227,7 @@ def _store_metadata(self, metadata: _DonchianLimitsMetaData) -> None: metadata: The _DonchianLimitsMetaData object containing the metadata to be stored. Returns: None - """ + """ self.position_metadata[metadata.trade_id] = metadata logger.debug(f"Stored metadata for trade ID {metadata.trade_id}: {metadata}") @@ -214,7 +250,6 @@ def get_momentum_scaler( Position sizing multiplier. """ - mult = 1.0 # -------------------------------------------------- @@ -223,11 +258,17 @@ def get_momentum_scaler( if not pd.isna(breakout_score): # strongest breakout regimes - if breakout_score >= 2: - mult *= 1.25 + if breakout_score >= 2.5: + mult *= 2 + + elif breakout_score >= 2: + mult *= 1.6 + + elif breakout_score >= 1.5: + mult *= 1.40 elif breakout_score >= 1: - mult *= 1.10 + mult *= 1.15 # weak breakout elif breakout_score < 0.25: @@ -237,21 +278,27 @@ def get_momentum_scaler( # Realized Volatility # -------------------------------------------------- - if not pd.isna(rvol): - # strongest observed region - if 0.60 <= rvol < 1.00: - mult *= 1.25 + if not pd.isna(rvol): + # strongest observed region + if rvol >= 0.75 and rvol < 1.00: + mult *= 2 - elif 0.40 <= rvol < 0.60: - mult *= 1.10 + elif 0.60 <= rvol < 0.75: + mult *= 1.6 - # compressed / weak momentum - elif rvol < 0.15: - mult *= 0.85 + elif 0.50 <= rvol < 0.60: + mult *= 1.40 - # ultra-extreme / unstable - elif rvol >= 1.00: - mult *= 0.95 + elif 0.40 <= rvol < 0.50: + mult *= 1.15 + + # compressed / weak momentum + elif rvol < 0.15: + mult *= 0.85 + + # ultra-extreme / unstable + elif rvol >= 1.00: + mult *= 0.95 # -------------------------------------------------- # Final Clamp @@ -293,6 +340,9 @@ def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: for pos_state in positions: try: + if not self.is_donchian_strategy(pos_state.signal_id): + continue + # Calculate DTE using exact same method as limits cog dte, _ = get_dte_and_moneyness_from_trade_id( trade_id=pos_state.trade_id, @@ -339,10 +389,7 @@ def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: continue logger.info( - f"VectorizedCog analysis complete. {len(opinions)} roll opinion(s) generated for {len(positions)} position(s)." + f"DonchianCog analysis complete. {len(opinions)} roll opinion(s) generated for {len(positions)} position(s)." ) return CogActions(date=context.date, source_cog=self.name, opinions=opinions) - - - diff --git a/EventDriven/riskmanager/position/cogs/mean_reversion.py b/EventDriven/riskmanager/position/cogs/mean_reversion.py index 95a28e9..a84184b 100644 --- a/EventDriven/riskmanager/position/cogs/mean_reversion.py +++ b/EventDriven/riskmanager/position/cogs/mean_reversion.py @@ -1,3 +1,20 @@ +"""Mean reversion position sizing cogs for EventDriven risk management. + +Implements z-score-based position sizing and fallback quantity handling for +mean-reversion strategies, plus a senior variant with additional logging. + +Core Classes: + MeanReversionSizerCog: Base z-score sizing and metadata capture cog. + SeniorMeanReversionSizerCog: Extended wrapper with additional logging. + +Core Dataclasses: + _MRLimitsMetaData: Per-trade sizing metadata for mean-reversion logic. + +Usage: + >>> cog = MeanReversionSizerCog(eq_strategy=strategy) + >>> cog.on_new_position(state) +""" + import math from EventDriven.riskmanager.position.base import BaseCog from EventDriven.configs.core import MeanReversionSizerConfigs @@ -6,44 +23,71 @@ import numpy as np from EventDriven.riskmanager.sizer._utils import default_delta_limit, delta_position_sizing from EventDriven.riskmanager.position.cogs.limits import _LimitsMetaData +from EventDriven.types import SignalID from trade.backtester_._multi_asset_strategy import MultiAssetStrategy from dataclasses import dataclass import pandas as pd from trade.helpers.Logging import setup_logger + logger = setup_logger("EventDriven.riskmanager.position.cogs.mean_reversion") @dataclass class _MRLimitsMetaData(_LimitsMetaData): + """Metadata stored for mean-reversion sizing decisions.""" + zscore: float = None zexcess: float = None + class MeanReversionSizerCog(BaseCog): + """Z-score-driven sizing cog for mean-reversion strategies.""" + default_config: MeanReversionSizerConfigs = MeanReversionSizerConfigs() def __init__(self, eq_strategy: MultiAssetStrategy, config: Optional[MeanReversionSizerConfigs] = None): + """Initialize the mean-reversion sizing cog. + + Args: + eq_strategy: Strategy interface used to fetch indicator data. + config: Optional runtime sizing configuration. + """ if config is None: - config: MeanReversionSizerConfigs = self.default_config + config = MeanReversionSizerConfigs() super().__init__(config) self.eq_strategy = eq_strategy self.position_metadata = {} + self.config: MeanReversionSizerConfigs = config def on_new_position(self, state: NewPositionState): + """Size a new position using z-score-based mean-reversion scaling. + + Args: + state: New position state to size and annotate with metadata. + + Returns: + None. Updates are applied in place to ``state.order`` metadata. + """ order = state.order - requet = state.request + request = state.request ticker = state.symbol undl_data = state.undl_at_time_data option_chain = state.at_time_data - cash_available = requet.tick_cash + cash_available = request.tick_cash opt_price = option_chain.get_price() date = order["date"] info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) - z_raw = (info["indicators"]["zscore"]) + z_raw = info["mean_reversion_indicators"]["zscore"] + signal_id = SignalID(order["signal_id"]) + if "mean_reversion" not in signal_id.strategy_slug: + logger.warning( + f"MeanReversionSizerCog received a new position for strategy {signal_id.strategy_slug} which does not contain 'mean_reversion'. This cog is designed for mean reversion strategies. Proceeding with sizing but please verify if this is intentional." + ) + return # Return if not a mean reversion strategy. ## scale z-score on the remainder of the distance to the minimum z-score threshold. ## if z-score is below the minimum threshold, no scaling is applied - z_excess = max(0, abs(z_raw) - self.config.min_zscore) - + z_excess = max(0, abs(z_raw) - self.config.min_zscore) ## Calculate scaler based on z-score and config parameters scaler_raw = 1 + self.config.beta * z_excess @@ -51,12 +95,9 @@ def on_new_position(self, state: NewPositionState): ## Update order quantity based on scaler limit = self.get_delta_limit( - tick_cash=cash_available, - chain_spot=undl_data.chain_spot["close"], - date=date, - ticker=ticker + tick_cash=cash_available, chain_spot=undl_data.chain_spot["close"], date=date, ticker=ticker ) - + delta = state.at_time_data.delta q = delta_position_sizing( cash_available=cash_available, @@ -87,11 +128,29 @@ def on_new_position(self, state: NewPositionState): logger.info(f"Storing metadata for trade_id {order['data']['trade_id']}: {metadata}") self.position_metadata[order["data"]["trade_id"]] = metadata - def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: - return CogActions( - opinions=[], strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name + if q == 0: + self.on_new_position_update( + state + ) # Check if we can adjust quantity to 1 based on cash and option price, and log the rationale behind it. + + metadata.new_quantity = order["data"][ + "quantity" + ] # Update metadata with final quantity after potential adjustment in on_position_update + logger.info( + f"Final quantity for trade_id {order['data']['trade_id']} after potential adjustment: {metadata.new_quantity}. Updated metadata: {metadata}" ) - + + def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: + """Return no ongoing analysis opinions for this sizing-focused cog. + + Args: + portfolio_context: Snapshot used by analysis pipeline. + + Returns: + Empty CogActions payload for this cycle. + """ + return CogActions(opinions=[], date=portfolio_context.date, source_cog=self.name) + def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestamp, ticker: str) -> float: """ Calculate delta limits based on cash, spot price, and z-score for mean reversion scaling. @@ -108,7 +167,7 @@ def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestam """ info = self.eq_strategy.info_on_date(ticker=ticker, current_date=date) - z_raw = info["indicators"]["zscore"] + z_raw = info["mean_reversion_indicators"]["zscore"] ## scale z-score on the remainder of the distance to the minimum z-score threshold. ## if z-score is below the minimum threshold, no scaling is applied @@ -127,28 +186,70 @@ def get_delta_limit(self, tick_cash: float, chain_spot: float, date: pd.Timestam limit = base_delta * scaler return limit + def on_new_position_update(self, state: NewPositionState): + """ + If q=0 for a new position, we make it 1 if the cash available and option price would allow for at least 1 contract to be bought. This is to avoid situations where the sizing logic results in 0 quantity due to the z-score being just below the threshold, but in reality, based on the cash and option price, we could still take a small position. This method checks for that scenario and adjusts the quantity accordingly while logging the rationale behind it. + """ + if state.order["data"]["quantity"] == 0: + logger.info( + f"Order quantity is 0 after mean reversion sizing for trade_id {state.order['data']['trade_id']}. This means the position will not be opened based on the current z-score and cash constraints." + ) + cash_available = state.request.tick_cash + option_price_at_time = state.at_time_data.get_price() + max_size_cash_can_buy = abs(math.floor(cash_available / (option_price_at_time * 100))) + if max_size_cash_can_buy >= 1: + logger.info( + f"However, based on the available cash of {cash_available} and option price of {option_price_at_time}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Consider adjusting the beta or z-score thresholds to allow for smaller position sizes in such scenarios." + ) + state.order["data"]["quantity"] = ( + 1 # Optionally set to 1 to allow for at least a small position, or keep at 0 to strictly follow the sizing logic. + ) + else: + logger.info( + f"Based on the available cash of {cash_available} and option price of {option_price_at_time}, even a single contract cannot be afforded. The order will remain at quantity 0." + ) + return state + class SeniorMeanReversionSizerCog(MeanReversionSizerCog): + """Extended mean-reversion sizing cog with extra fallback logging.""" + def __init__(self, eq_strategy: MultiAssetStrategy, config: Optional[MeanReversionSizerConfigs] = None): + """Initialize the senior mean-reversion cog. + + Args: + eq_strategy: Strategy interface used to fetch indicator data. + config: Optional runtime sizing configuration. + """ super().__init__(eq_strategy, config) def on_new_position(self, state): + """Run base sizing then apply senior-level quantity-0 diagnostics. + + Args: + state: New position state passed through sizing workflow. + + Returns: + Result from base ``on_new_position`` execution. """ - Overrides the on_new_position method to add logging and handle cases where the order quantity is reduced to 0 after mean reversion sizing. - If the order quantity is 0, it logs this information and checks if based on the available cash and option price, at least 1 contract could be afforded. If so, it logs this insight and optionally sets the""" ret = super().on_new_position(state) if state.order["data"]["quantity"] == 0: - logger.info(f"Order quantity is 0 after mean reversion sizing for trade_id {state.order['data']['trade_id']}. This means the position will not be opened based on the current z-score and cash constraints.") + logger.info( + f"Order quantity is 0 after mean reversion sizing for trade_id {state.order['data']['trade_id']}. This means the position will not be opened based on the current z-score and cash constraints." + ) cash_available = state.request.tick_cash option_price_at_time = state.at_time_data.get_price() max_size_cash_can_buy = abs(math.floor(cash_available / (option_price_at_time * 100))) if max_size_cash_can_buy >= 1: - logger.info(f"However, based on the available cash of {cash_available} and option price of {option_price_at_time}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Consider adjusting the beta or z-score thresholds to allow for smaller position sizes in such scenarios.") - state.order["data"]["quantity"] = 1 # Optionally set to 1 to allow for at least a small position, or keep at 0 to strictly follow the sizing logic. + logger.info( + f"However, based on the available cash of {cash_available} and option price of {option_price_at_time}, the strategy could afford to buy up to {max_size_cash_can_buy} contracts. Consider adjusting the beta or z-score thresholds to allow for smaller position sizes in such scenarios." + ) + state.order["data"]["quantity"] = ( + 1 # Optionally set to 1 to allow for at least a small position, or keep at 0 to strictly follow the sizing logic. + ) else: - logger.info(f"Based on the available cash of {cash_available} and option price of {option_price_at_time}, even a single contract cannot be afforded. The order will remain at quantity 0.") + logger.info( + f"Based on the available cash of {cash_available} and option price of {option_price_at_time}, even a single contract cannot be afforded. The order will remain at quantity 0." + ) return ret - - - diff --git a/EventDriven/riskmanager/position/cogs/plain_sizing.py b/EventDriven/riskmanager/position/cogs/plain_sizing.py new file mode 100644 index 0000000..2482458 --- /dev/null +++ b/EventDriven/riskmanager/position/cogs/plain_sizing.py @@ -0,0 +1,223 @@ +"""Plain sizing cog for default delta-limit based position management. + +Provides a lightweight sizing and analysis cog that: +- Sizes new positions using default_delta_limit. +- Applies a one-contract affordability fallback when quantity is zero. +- Emits DTE-based ROLL opinions during analysis. +- Skips sizing and analysis checks for excluded strategy slug tokens. + +Core Classes: + PlainSizingCog: Position sizing and DTE-based roll analysis implementation. + +Configuration: + PlainSizingCogConfig: Runtime knobs for sizing leverage, DTE checks, + and strategy-slug exclusion tokens. + +Usage: + >>> cog = PlainSizingCog() + >>> cog.on_new_position(new_position_state) +""" + +from typing import Dict, Optional + +import pandas as pd + +from EventDriven.configs.core import PlainSizingCogConfig +from EventDriven.dataclasses.limits import PositionLimits +from EventDriven.dataclasses.states import CogActions, NewPositionState, PositionAnalysisContext +from EventDriven.riskmanager.actions import Changes, ROLL +from EventDriven.riskmanager.position.base import BaseCog +from EventDriven.riskmanager.position.cogs.analyze_utils import get_dte_and_moneyness_from_trade_id +from EventDriven.riskmanager.position.cogs.limits import _LimitsMetaData +from EventDriven.riskmanager.sizer._utils import default_delta_limit, delta_position_sizing +from EventDriven.types import SignalID +from trade.helpers.Logging import setup_logger + +logger = setup_logger("EventDriven.riskmanager.position.cogs.plain_sizing", stream_log_level="INFO") + + +class PlainSizingCog(BaseCog): + """Default delta-limit sizing cog with DTE-based roll recommendations.""" + + default_config = PlainSizingCogConfig() + + def __init__(self, config: Optional[PlainSizingCogConfig] = None): + """Initialize the plain sizing cog. + + Args: + config: Optional runtime configuration for sizing, roll checks, + and exclusion behavior. + """ + if config is None: + config = PlainSizingCogConfig() + super().__init__(config) + self.position_limits: Dict[str, PositionLimits] = {} + self.position_metadata: Dict[str, _LimitsMetaData] = {} + self.config: PlainSizingCogConfig = config + + def _is_excluded_strategy(self, signal_id: str) -> bool: + """Return whether a signal should be excluded from checks. + + Args: + signal_id: Raw signal identifier. + + Returns: + True if any configured exclusion token is contained in the + strategy slug; otherwise False. + """ + try: + slug = SignalID(signal_id).strategy_slug + except Exception: + logger.warning(f"Unable to parse signal id {signal_id} for exclusion check.", exc_info=True) + return False + + tokens = self.config.exclude_strategy_slug_tokens or [] + return any(token and token in slug for token in tokens) + + def on_new_position(self, state: NewPositionState) -> None: + """Size a newly created position with default delta limits. + + Process: + 1. Skip if strategy slug matches exclusion tokens. + 2. Compute default delta limit from available cash. + 3. Compute quantity via delta_position_sizing. + 4. If quantity is zero and one contract is affordable, set quantity to 1. + 5. Store limits and metadata on state. + + Args: + state: New position state to size. + + Returns: + None. Updates are applied in place on ``state``. + """ + order = state.order + request = state.request + + if self._is_excluded_strategy(order.signal_id): + logger.debug( + f"Skipping plain sizing for excluded strategy signal {order.signal_id}. " + f"Trade ID: {order['data']['trade_id']}" + ) + return + else: + logger.info( + f"Applying plain sizing for signal {order.signal_id}. " + f"Trade ID: {order['data']['trade_id']}" + ) + + undl_data = state.undl_at_time_data + option_chain = state.at_time_data + chain_spot = undl_data.chain_spot["close"] + opt_price = option_chain.get_price() + delta = option_chain.delta + + cash_available = request.tick_cash if request.is_tick_cash_scaled else request.tick_cash * 100 + + limit = default_delta_limit( + cash_available=cash_available, + underlier_price_at_time=chain_spot, + sizing_lev=self.config.sizing_lev, + ) + + q = delta_position_sizing( + cash_available=cash_available, + option_price_at_time=opt_price, + delta=delta, + delta_limit=limit, + ) + + if q == 0 and cash_available >= opt_price * 100: + logger.info( + f"Quantity was zero for trade {order['data']['trade_id']} but one contract is affordable. " + "Setting quantity to 1." + ) + q = 1 + + order["data"]["quantity"] = q + + pos_lmts = PositionLimits( + delta=limit, + dte=self.config.dte_threshold, + creation_date=request.date, + ) + state.limits = pos_lmts + self.position_limits[order["data"]["trade_id"]] = pos_lmts + + metadata = _LimitsMetaData( + trade_id=order["data"]["trade_id"], + date=order["date"], + signal_id=order["signal_id"], + scalar=1.0, + sizing_lev=self.config.sizing_lev, + delta_per_contract=delta, + option_price=opt_price, + undl_price=chain_spot, + delta_lmt=limit, + new_quantity=q, + ) + self.position_metadata[order["data"]["trade_id"]] = metadata + + def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: + """Analyze open positions and emit DTE-based roll recommendations. + + Process: + 1. Return no opinions when DTE checks are disabled. + 2. Skip excluded strategy slugs. + 3. Compute DTE via trade-id parser helper. + 4. Emit ROLL opinion when DTE is below threshold. + + Args: + context: Portfolio snapshot for the current analysis cycle. + + Returns: + CogActions containing roll opinions generated by this cog. + """ + opinions = [] + + if not self.config.dte_limit_enabled: + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) + + positions = context.portfolio.positions + portfolio_meta = context.portfolio_meta + last_updated = context.portfolio.last_updated + t_plus_n = portfolio_meta.t_plus_n + t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) + backtest_start = portfolio_meta.start_date + + for pos_state in positions: + if self._is_excluded_strategy(pos_state.signal_id): + continue + + try: + dte, _ = get_dte_and_moneyness_from_trade_id( + trade_id=pos_state.trade_id, + check_date=pos_state.last_updated, + check_price=pos_state.current_underlier_data.chain_spot["close"], + start=backtest_start, + is_backtest=portfolio_meta.is_backtest, + ) + + if dte < self.config.dte_threshold: + action = ROLL( + trade_id=pos_state.trade_id, + action=Changes(quantity_diff=0, new_quantity=pos_state.quantity), + ) + action.reason = ( + f"DTE {dte} below threshold {self.config.dte_threshold}. Rolling to extend duration." + ) + action.analysis_date = last_updated + action.effective_date = last_updated + t_plus_n_bdays + action.verbose_info = ( + f"Analysis: {action.analysis_date} | Effective: {action.effective_date} | " + f"Trade: {pos_state.trade_id} | DTE: {dte} | Threshold: {self.config.dte_threshold}" + ) + pos_state.action = action + opinions.append(pos_state) + + except Exception as exc: + logger.warning( + f"Error calculating DTE for position {pos_state.trade_id}: {exc}. Skipping roll check.", + exc_info=True, + ) + + return CogActions(date=context.date, source_cog=self.name, opinions=opinions) diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 0a6f0fe..723fdb4 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -72,7 +72,7 @@ from EventDriven.riskmanager.position.base import BaseCog from EventDriven.dataclasses.states import PositionAnalysisContext, CogActions, PositionState from EventDriven.riskmanager.actions import CLOSE, ROLL, HOLD -from EventDriven.configs.core import PnlMonitorConfig +from EventDriven.configs.core import PnlMonitorConfig, PnLMonitorConfigConfigurable logger = setup_logger("EventDriven.riskmanager.position.cogs.pnl_monitor", stream_log_level="INFO") @@ -96,10 +96,10 @@ class PnLMonitorCog(BaseCog): default_config = PnlMonitorConfig() def __init__( - self, - config: Optional[PnlMonitorConfig] = None, - max_trade_dollar_size: Optional[float] = None - ): + self, + config: Optional[PnlMonitorConfig | PnLMonitorConfigConfigurable] = None, + max_trade_dollar_size: Optional[float] = None, + ): """Initialize the PnL monitor cog. Args: @@ -113,11 +113,13 @@ def __init__( branch is opt-in. - `default_config` remains available for framework-level defaults. """ + assert config is None or isinstance(config, (PnlMonitorConfig, PnLMonitorConfigConfigurable)), ( + "Config must be of type PnlMonitorConfig or PnLMonitorConfigConfigurable" + ) if config is None: config = PnlMonitorConfig() super().__init__(config) - self.config = config - self.enable_stop_loss = False # Enable stop loss by default + self.config: PnlMonitorConfig | PnLMonitorConfigConfigurable = config self.max_trade_dollar_size = max_trade_dollar_size def on_new_position(self, new_position_state: NewPositionState) -> None: @@ -164,19 +166,21 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: ## If max_trade_dollar_size is set, cap the tick_cash to prevent excessive allocation if self.max_trade_dollar_size is not None: logger.info( - f"Max trade dollar size is set to ${self.max_trade_dollar_size:.2f}. Original tick cash: ${tick_cash:.2f}." + f"Max trade dollar size is set to ${self.max_trade_dollar_size:.2f}. Original tick cash: ${tick_cash:.2f}." ) ## If tick_cash > max_trade_dollar_size, tick_cash = max_trade_dollar_size ## If tick_cash <= max_trade_dollar_size, tick_cash remains unchanged new_request_state.tick_cash = min(tick_cash, self.max_trade_dollar_size) - new_request_state.is_tick_cash_scaled = True # Mark as scaled since we're treating it as a dollar amount now + new_request_state.is_tick_cash_scaled = ( + True # Mark as scaled since we're treating it as a dollar amount now + ) return ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. if pnl is not None and pnl > 0: additional_tick_cash = ( - pnl * 0.25 + pnl * self.config.profit_lock_in_pct ) ## Add 25% of the profits to the tick cash to scale up the position and lock in profits. ## Undo pnl from tick cash to avoid double counting, then add the additional tick cash to lock in profits. @@ -192,8 +196,9 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: new_request_state.is_tick_cash_scaled = True else: + pnl_str = f"{pnl:.2f}" if pnl is not None else "None" logger.info( - f"No profits to lock in for {new_request_state.symbol}. Tick Cash remains at {tick_cash:.2f}. PnL: {pnl:.2f}" + f"No profits to lock in for {new_request_state.symbol}. Tick Cash remains at {tick_cash:.2f}. PnL: {pnl_str}" ) def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: @@ -224,9 +229,9 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction opinions = [] ## Rules: - ## 1. If PnL is greater than 50% of entry price, close half the position if quantity > 1 to lock in profits. - ## 2. If quantity is 1 and PnL is greater than 150% of entry price, ROLL the position. - ## 2a. If have closed before, and remainding quantity is > 1, ROLL when PnL is greater than 150% of entry price to lock in profits. + ## 1. If PnL is greater than lock_in_profit_threshold of entry price, close half the position if quantity > 1 to lock in profits. + ## 2. If quantity is 1 and PnL is greater than roll_profit_threshold of entry price, ROLL the position. + ## 2a. If have closed before, and remainding quantity is > 1, ROLL when PnL is greater than roll_profit_threshold of entry price to lock in profits. positions = portfolio_context.portfolio.positions bkt_info = portfolio_context.portfolio_meta t_plus_n = bkt_info.t_plus_n @@ -239,21 +244,21 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction if pos_state.entry_price * pos_state.quantity != 0 else 0 ) - if pl_pct > 0.5 and pos_state.quantity > 1: + if pl_pct > self.config.lock_in_profit_threshold and pos_state.quantity > 1: qdiff = -math.ceil(pos_state.quantity / 2) - new_q = pos_state.quantity - qdiff + new_q = pos_state.quantity + qdiff logger.info( f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}" ) action = CLOSE(trade_id=pos_state.trade_id, action={"quantity_diff": qdiff, "new_quantity": new_q}) action.analysis_date = portfolio_context.date - action.reason = f"PnL is {pl_pct:.2%} which is greater than 50% of entry price. Closing half the position to lock in profits." + action.reason = f"PnL is {pl_pct:.2%} which is greater than {self.config.lock_in_profit_threshold:.2%} of entry price. Closing half the position to lock in profits." action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing half the position to lock in profits. Quantity change: {qdiff}, New Quantity: {new_q}" action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action opinions.append(pos_state) - elif pl_pct > 1.0: + elif pl_pct > self.config.roll_profit_threshold: if pos_state.quantity == 1 or ( self._has_closed_before(pos_state.trade_id, pos_state) and pos_state.quantity > 1 ): @@ -264,14 +269,14 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction trade_id=pos_state.trade_id, action={"quantity_diff": 0, "new_quantity": pos_state.quantity} ) action.analysis_date = portfolio_context.date - action.reason = f"PnL is {pl_pct:.2%} which is greater than 150% of entry price. Rolling the position to lock in profits." + action.reason = f"PnL is {pl_pct:.2%} which is greater than {self.config.roll_profit_threshold:.2%} of entry price. Rolling the position to lock in profits." action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits." action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action opinions.append(pos_state) - ## Stop loss branch: close <=-70% - elif pl_pct <= -0.7 and self.enable_stop_loss: + ## Stop loss branch: close <= stop_loss_pct to prevent further losses. This branch is disabled by default and can be enabled by setting enable_stop_loss to True. + elif pl_pct <= self.config.stop_loss_pct and self.config.enable_stop_loss: logger.info( f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." ) @@ -279,7 +284,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction trade_id=pos_state.trade_id, action={"quantity_diff": -pos_state.quantity, "new_quantity": 0} ) action.analysis_date = portfolio_context.date - action.reason = f"PnL is {pl_pct:.2%} which is less than or equal to -70% of entry price. Closing the position to prevent further losses." + action.reason = f"PnL is {pl_pct:.2%} which is less than or equal to {self.config.stop_loss_pct:.2%} of entry price. Closing the position to prevent further losses." action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action @@ -294,9 +299,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction ) opinions.append(pos_state) - return CogActions( - opinions=opinions, strategy_id=self.config.run_name, date=portfolio_context.date, source_cog=self.name - ) + return CogActions(opinions=opinions, date=portfolio_context.date, source_cog=self.name) def _get_current_close_open_quantity(self, pos_state: PositionState) -> Tuple[int, int]: """Aggregate opened and closed quantity from trade entries. diff --git a/EventDriven/riskmanager/position/cogs/vectorized.py b/EventDriven/riskmanager/position/cogs/vectorized.py index 50b34a7..08a8e0e 100644 --- a/EventDriven/riskmanager/position/cogs/vectorized.py +++ b/EventDriven/riskmanager/position/cogs/vectorized.py @@ -104,6 +104,12 @@ def on_new_position(self, new_pos_state: NewPositionState) -> None: f"Cost=${position_cost:.2f}, Available=${tick_cash:.2f}, " f"Quantity={quantity}, Option Price=${option_price:.4f}" ) + if can_afford: + logger.debug(f"Position {order['data']['trade_id']} passed cash validation. Setting quantity to 1") + order["data"]["quantity"] = 1 # Enforce quantity of 1 for new positions in this cog + else: + logger.warning(f"Position {order['data']['trade_id']} failed cash validation. Quantity remains {quantity}") + except Exception as e: logger.warning(f"Error during cash validation for new position: {e}") diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index 4febbb4..cd1f685 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -554,8 +554,8 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat right=option_meta["put_call"], start_date=start, end_date=end, - dividend_type=DivType.CONTINUOUS, - market_model=OptionPricingModel.BSM, + # dividend_type=DivType.DISCRETE, + # market_model=OptionPricingModel.BINOMIAL, ) logger.info(f"Data loading for {opttick} took {time.time() - start_time:.2f} seconds") @@ -571,12 +571,12 @@ def load_position_data_new(opttick, processed_option_data, start, end) -> pd.Dat ## set names properly start_time = time.time() s.name = "s" - y.name = "y" + # y.name = "y" r.name = "r" vol.name = "vol" data = greeks.join(option_spot[["midpoint", "closeask", "closebid"]]) data.columns = data.columns.str.capitalize() - data = data.join(s).join(y).join(r).join(vol) + data = data.join(s).join(r).join(vol)#.join(y) logger.info(f"Data processing for {opttick} took {time.time() - start_time:.2f} seconds") processed_option_data[opttick] = data return data diff --git a/trade/datamanager/option_spot.py b/trade/datamanager/option_spot.py index df95562..d1e2923 100644 --- a/trade/datamanager/option_spot.py +++ b/trade/datamanager/option_spot.py @@ -38,6 +38,7 @@ 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. @@ -281,7 +282,7 @@ def get_option_spot_timeseries( """ if endpoint_source is None: endpoint_source = self.CONFIG.option_spot_endpoint_source - + result = OptionSpotResult() result.symbol = self.symbol result.endpoint_source = endpoint_source @@ -368,6 +369,10 @@ def get_option_spot_timeseries( fetched=fetched_data, valid_start=start_date, valid_end=end_date, + symbol=self.symbol, + strike=float(strike), + right=right, + expiration=expiration, ) logger.info( f"Option spot date classification for key {key}: " @@ -390,6 +395,7 @@ def get_option_spot_timeseries( # If the requested window has only checked-missing dates, add placeholder # NaN rows so sanitization and later cache hits can shape the output range. if classification.checked_missing_dates: + print(f"Adding placeholder rows for {len(classification.checked_missing_dates)} checked-missing dates for key {key}.") checked_missing_idx = pd.DatetimeIndex(to_datetime(classification.checked_missing_dates)) checked_missing_idx = default_timestamp(checked_missing_idx) diff --git a/trade/datamanager/result.py b/trade/datamanager/result.py index f97bfa4..aa46749 100644 --- a/trade/datamanager/result.py +++ b/trade/datamanager/result.py @@ -606,6 +606,9 @@ class ModelResultPack(Result): price: Optional[ModelPrice] = None rt: Optional[bool] = False on_date: Optional[bool] = False + market_model: Optional[OptionPricingModel] = None + vol_model: Optional[VolatilityModel] = None + fallback_option: Optional[RealTimeFallbackOption] = None ## Diagnostic Info time_to_load: Optional[Dict[str, float]] = None diff --git a/trade/datamanager/utils/classification.py b/trade/datamanager/utils/classification.py index e45764a..9865adc 100644 --- a/trade/datamanager/utils/classification.py +++ b/trade/datamanager/utils/classification.py @@ -35,9 +35,58 @@ import pandas as pd -from trade.helpers.helper import get_missing_dates, to_datetime +from trade.helpers.helper import to_datetime, generate_option_tick_new from trade import HOLIDAY_SET +from .date import LIST_DATE_CACHE +from ..vars import get_enable_caching +from dbase.DataAPI.ThetaData import list_dates + +def get_option_dates( + ticker: str, + strike: float, + right: str, + expiration: datetime, +) -> List[datetime]: + """Fetches the list of dates for which the API has option spot data for the given parameters. + Args: + ticker: The underlying symbol. + strike: The option strike price. + right: The option right ('call' or 'put'). + expiration: The option expiration date. + Returns: + A list of datetime objects representing the dates for which option spot data is available. + """ + + option_has_expired = expiration < datetime.now() + opttick = generate_option_tick_new( + symbol=ticker, + strike=strike, + right=right, + exp=expiration, + ) + + ## Only use cache if option has expired, otherwise always fetch from source to capture new data availability + if opttick in LIST_DATE_CACHE and option_has_expired: + return LIST_DATE_CACHE[opttick]["range"] + + available_dates = list_dates( + symbol=ticker, + strike=strike, + right=right, + exp=expiration, + ) + + + ## Only cache if option has expired + if get_enable_caching() and option_has_expired: + LIST_DATE_CACHE[opttick] = { + "range": available_dates, + "last_updated": datetime.now(), + "min_date": min(available_dates) if available_dates else None, + "max_date": max(available_dates) if available_dates else None, + } + return available_dates @dataclass class DateClassification: @@ -57,6 +106,10 @@ class DateClassification: def classify_option_spot_dates( fetched: pd.DataFrame, + symbol: str, + strike: float, + right: str, + expiration: datetime, valid_start: Union[datetime, str], valid_end: Union[datetime, str], ) -> DateClassification: @@ -70,6 +123,10 @@ def classify_option_spot_dates( Args: fetched: DataFrame returned by the API, indexed by DatetimeIndex. May be empty or contain all-NaN rows for some dates. + symbol: The underlying symbol. + strike: The option strike price. + right: The option right ('call' or 'put'). + expiration: The option expiration date. valid_start: First date of the valid query window (already synced by _sync_date). valid_end: Last date of the valid query window (already synced by @@ -83,7 +140,7 @@ def classify_option_spot_dates( >>> import numpy as np >>> idx = pd.bdate_range("2026-01-05", "2026-01-09") >>> df = pd.DataFrame({"close": [10.0, np.nan, 12.0, np.nan, np.nan]}, index=idx) - >>> result = classify_option_spot_dates(df, "2026-01-05", "2026-01-09") + >>> result = classify_option_spot_dates(df, "AAPL", 150.0, "C", datetime(2026, 1, 9), "2026-01-05", "2026-01-09") >>> list(result.observed_dates.strftime("%Y-%m-%d")) ['2026-01-05', '2026-01-07'] >>> sorted(str(d) for d in result.checked_missing_dates) @@ -93,9 +150,18 @@ def classify_option_spot_dates( valid_end_dt = to_datetime(valid_end) # Full set of expected business days in the valid window, excluding holidays. + trading_days = to_datetime(get_option_dates(symbol, strike, right, expiration)) expected_bus_days = pd.bdate_range(start=valid_start_dt, end=valid_end_dt) expected_bus_days = pd.DatetimeIndex([d for d in expected_bus_days if d.strftime("%Y-%m-%d") not in HOLIDAY_SET]) + # Checked missing dates are those expected business days that are not present in the fetched data. + # This means we asked the API for these dates but got no usable data back, so we should cache them as missing. + # FYI: This isn't intended to be actual missing dates in the data. + # It's intention is to identify dates that the API DOESN'T have data. There are instances where v2 data did not exist + # But v3 data does exist. So we want to make sure we don't cache those dates as missing if they are just missing in v2 but + # do exist in v3. Eg: NVDA20220121C375 + checked_missing = list(set(expected_bus_days.date) - set(trading_days.date)) + if fetched is None or fetched.empty: return DateClassification( observed_dates=pd.DatetimeIndex([]), @@ -110,9 +176,9 @@ def classify_option_spot_dates( has_data_mask = ~fetched.isna().all(axis=1) observed_idx = fetched.index[has_data_mask] - # All expected dates not in the observed set are checked-missing. - observed_set = set(observed_idx.normalize()) - checked_missing = [d.date() for d in expected_bus_days if d.normalize() not in observed_set] + # # All expected dates not in the observed set are checked-missing. + # observed_set = set(observed_idx.normalize()) + # checked_missing = [d.date() for d in expected_bus_days if d.normalize() not in observed_set] return DateClassification( observed_dates=observed_idx, diff --git a/trade/datamanager/utils/model.py b/trade/datamanager/utils/model.py index d1a5989..6e6e92d 100644 --- a/trade/datamanager/utils/model.py +++ b/trade/datamanager/utils/model.py @@ -659,6 +659,11 @@ def _load_model_data_timeseries(load_request: LoadRequest) -> ModelResultPack: is_rt=is_rt, check_fallback_option=is_rt or is_as_of, ) + model_data.rt = is_rt + model_data.market_model = load_request.market_model or OptionDataConfig().option_model + model_data.vol_model = load_request.vol_model or OptionDataConfig().volatility_model + model_data.model_price = load_request.model_price or OptionDataConfig().model_price + ## Log what was loaded only if something was actually loaded if load_info: diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index 32f57da..7b93dad 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -48,10 +48,17 @@ def get_enable_caching() -> bool: global ENABLE_CACHING return ENABLE_CACHING +def disable_caching() -> None: + global ENABLE_CACHING + ENABLE_CACHING = False + +def enable_caching() -> None: + global ENABLE_CACHING + ENABLE_CACHING = True -def set_enable_caching(enabled: bool) -> None: +def is_caching_enabled() -> bool: global ENABLE_CACHING - ENABLE_CACHING = bool(enabled) + return ENABLE_CACHING def get_dm_gen_path(is_live: bool = None) -> Path: from .config import OptionDataConfig From fd157e85c5f1a9cab5058b09f9b8d6bd08f6a93e Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Mon, 25 May 2026 23:18:41 -0400 Subject: [PATCH 67/81] chore: update config docs and align portfolio/pnl handling --- EventDriven/configs/core.py | 3 +- EventDriven/configs/export_configs.py | 14 +-- EventDriven/configs/vars.py | 94 +++++++++++++------ EventDriven/new_portfolio.py | 13 ++- .../riskmanager/position/cogs/pnl_monitor.py | 4 +- trade/backtester_/utils/utils.py | 5 +- 6 files changed, 93 insertions(+), 40 deletions(-) diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 535cb21..e905a31 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -384,7 +384,8 @@ class PnLMonitorConfigConfigurable(BaseCogConfig): """ Configurable version of PnLMonitorConfig that allows dynamic adjustment of thresholds. """ - + name: str = "PnLMonitorCog" + enabled: bool = True roll_profit_threshold: float = 1.0 # Default to 100% gain threshold for rolling to lock in profits stop_loss_pct: float = -0.7 # Default to 70% loss threshold for exiting positions profit_lock_in_pct: float = 0.25 # Default to adding 25% of the gain back into cash for next trade diff --git a/EventDriven/configs/export_configs.py b/EventDriven/configs/export_configs.py index b95efc7..ad26b98 100644 --- a/EventDriven/configs/export_configs.py +++ b/EventDriven/configs/export_configs.py @@ -31,6 +31,10 @@ RiskManagerConfig, MeanReversionSizerConfigs, ExecutionHandlerConfig, + PnlMonitorConfig, + PnLMonitorConfigConfigurable, + VectorizedCogConfig, + PlainSizingCogConfig, ) logger = setup_logger("EventDriven.configs.export_configs") @@ -70,16 +74,15 @@ class ConfigsDict(TypedDict, total=False): PositionAnalyzerConfig: "PositionAnalyzerConfig" PortfolioManagerConfig: "PortfolioManagerConfig" LiquidityConfig: "LiquidityConfig" - LiquidityConfig: "LiquidityConfig" BacktesterConfig: "BacktesterConfig" CashAllocatorConfig: "CashAllocatorConfig" RiskManagerConfig: "RiskManagerConfig" MeanReversionSizerConfigs: "MeanReversionSizerConfigs" - ScoringConfigs: "ScoringConfigs" - ExecutionHandlerConfig: "ExecutionHandlerConfig" - MeanReversionSizerConfigs: "MeanReversionSizerConfigs" - ScoringConfigs: "ScoringConfigs" ExecutionHandlerConfig: "ExecutionHandlerConfig" + PnlMonitorConfig: "PnlMonitorConfig" + PnLMonitorConfigConfigurable: "PnLMonitorConfigConfigurable" + VectorizedCogConfig: "VectorizedCogConfig" + PlainSizingCogConfig: "PlainSizingCogConfig" @dataclass @@ -110,7 +113,6 @@ def save_to_yaml(self, filename: str): """ confs = {} for label, cfg in self.configs.items(): - if not isinstance(cfg, dict): # Convert config objects to dicts for YAML serialization confs[label] = _sanitize_for_yaml(cfg) diff --git a/EventDriven/configs/vars.py b/EventDriven/configs/vars.py index 38fbb08..8a4b823 100644 --- a/EventDriven/configs/vars.py +++ b/EventDriven/configs/vars.py @@ -2,27 +2,31 @@ CONFIG_CLASS_DESCRIPTIONS = { "BaseConfigs": "Base configuration class providing common functionality and run tracking for all configuration types.", "_CustomFrozenBaseConfigs": "Frozen configuration base class that prevents modification of attributes after initialization, except for run_name.", - "ChainConfig": "Configuration for filtering and selecting options from the option chain based on spread width and open interest.", - "OrderSchemaConfigs": "Configuration defining the structure and parameters for options orders including strategy type, DTE, and moneyness.", - "OrderPickerConfig": "Configuration for the order picker component that selects optimal orders from available option chains.", - "BaseSizerConfigs": "Base configuration for position sizing modules, defining the type of delta limit calculation.", - "DefaultSizerConfigs": "Standard position sizing configuration using fixed leverage without volatility adjustments.", - "ZscoreSizerConfigs": "Advanced position sizing configuration using volatility-adjusted z-score based limits for dynamic risk management.", - "UndlTimeseriesConfig": "Configuration for underlying asset timeseries data retrieval and interval settings.", - "OptionPriceConfig": "Configuration specifying which price type (bid, ask, midpoint, close) to use for option valuation.", - "SkipCalcConfig": "Configuration for anomaly detection in option data, determining when to skip calculations due to data quality issues.", - "BaseCogConfig": "Base configuration for position analyzer cog components that perform specific analysis tasks.", - "StrategyLimitsEnabled": "Configuration flags controlling which types of risk limits (delta, gamma, vega, theta, DTE, moneyness) are enforced.", - "LimitsEnabledConfig": "Configuration for the limits enforcement cog, managing risk thresholds and position rolling triggers.", - "PositionAnalyzerConfig": "Configuration for the position analyzer orchestrating multiple cogs for comprehensive position analysis.", - "PortfolioManagerConfig": "Configuration for portfolio management including weights haircut adjustments.", - "BacktesterConfig": "Configuration for backtest execution including settlement delays and trade finalization.", - "RiskManagerConfig": "Configuration for the risk manager controlling order and analysis caching behavior.", - "CashAllocatorConfig": "Threshold-based cash bucket allocator for symbols.", - "LiquidityConfig": "Centralized liquidity control for both risk and execution layers, managing spread and liquidity level.", - "MeanReversionSizerConfigs": "Custom mean reversion position sizer using z-score scaling to adjust sizing dynamically around a target DTE.", - "ScoringConfigs": "Configuration for scoring and selecting options based on moneyness, DTE, mid price, spread, and theta burden targets.", - "ExecutionHandlerConfig": "Configuration for the execution handler controlling slippage model and spread percentage parameters.", + "ChainConfig": "Controls which option quotes are allowed into candidate chains before order construction.", + "OrderSchemaConfigs": "Defines the target option structure (strategy, direction, DTE, moneyness, and pricing bounds) used when building an order.", + "OrderPickerConfig": "Defines the date window the order picker is allowed to search when selecting trades.", + "BaseSizerConfigs": "Base sizing controls shared by sizer implementations, including delta-limit mode selection.", + "DefaultSizerConfigs": "Simple leverage-based sizing with fixed-style delta limit behavior.", + "ZscoreSizerConfigs": "Volatility-aware sizing using z-score style normalization and weighted windows.", + "UndlTimeseriesConfig": "Controls underlying price timeseries retrieval granularity.", + "OptionPriceConfig": "Selects which option price field is treated as the canonical execution/valuation price.", + "SkipCalcConfig": "Controls data-quality skip logic that suppresses trading or calculations on anomalous option datapoints.", + "BaseCogConfig": "Shared base settings for all PositionAnalyzer cogs (name + enabled flag).", + "StrategyLimitsEnabled": "Feature flags for which risk checks are actively enforced by limit-aware cogs.", + "LimitsEnabledConfig": "Configures the LimitsEnabledCog that applies delta/DTE/moneyness style constraints.", + "PositionAnalyzerConfig": "Top-level switchboard for PositionAnalyzer and its enabled cogs.", + "PortfolioManagerConfig": "Controls portfolio-level behavior such as weight haircuting and failed-order roll behavior.", + "BacktesterConfig": "Top-level runtime configuration for settlement delay, slippage bounds, commissions, and liquidity policy.", + "RiskManagerConfig": "Controls cache behavior for generated orders, analyses, and request objects.", + "CashAllocatorConfig": "Maps weighted capital to per-symbol cash buckets using threshold rules.", + "LiquidityConfig": "Defines multi-level liquidity enforcement shared by risk sizing and execution-time spread gates.", + "MeanReversionSizerConfigs": "Custom mean-reversion sizing controls for scaling position size between min/max bounds.", + "ScoringConfigs": "Scoring hyperparameters used to rank candidate option structures across spread, moneyness, DTE, and theta burden.", + "ExecutionHandlerConfig": "Execution slippage model selection and spread-alpha settings used when fills are simulated.", + "PnlMonitorConfig": "Non-configurable PnL monitor defaults provided through computed properties.", + "PnLMonitorConfigConfigurable": "Explicit threshold-based PnL monitor config where each trigger is directly user-configurable.", + "VectorizedCogConfig": "Config for a lightweight cog that monitors DTE thresholds using vectorized-friendly checks.", + "PlainSizingCogConfig": "Config for a simple sizing cog with fallback one-lot behavior and optional strategy-token exclusions.", } CONFIG_DEFINITIONS = { @@ -35,8 +39,9 @@ }, "ChainConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", - "max_pct_width": "Maximum abs spread/mid price percentage width for an option to be included in the option chain.", - "min_oi": "Minimum open interest required for an option to be included in the option chain.", + "max_pct_width": "Hard filter: keeps only contracts whose relative spread width is <= this value.", + "min_oi": "Hard filter: keeps only contracts whose open interest is >= this value.", + "enable_delta_filter": "If True, also applies delta-based filtering in chain preselection; if False, no delta pre-filter is applied.", }, "OrderSchemaConfigs": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -48,6 +53,7 @@ "min_moneyness": "Minimum moneyness level for selecting options.", "max_moneyness": "Maximum moneyness level for selecting options.", "min_total_price": "Minimum total price for the option structure.", + "max_attempts": "Maximum retry attempts when trying to construct a valid order from available chain candidates.", }, "OrderPickerConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -132,12 +138,12 @@ "BacktesterConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", "t_plus_n": "Settlement delay for orders in business days (T+N, default 1).", - "finalize_trades": "Flag to enable finalization of trades at end of backtest (default False).", + "finalize_trades": "If True, finalization logic runs at the end of backtest to close/settle remaining trades (default True).", "raise_errors": "Flag to raise errors during backtest execution instead of logging them (default False).", "min_slippage_pct": "Minimum slippage percentage applied to trade execution (default 0.075).", "max_slippage_pct": "Maximum slippage percentage applied to trade execution (default 0.15).", "commission_per_contract_in_units": "Commission charged per contract in dollar units (default 0.0065).", - "liquidity": "LiquidityConfig instance controlling spread and liquidity level for backtest execution.", + "liquidity": "LiquidityConfig object used by both RiskManager and ExecutionHandler for quantity haircuting and spread-based drop/reschedule behavior.", }, "RiskManagerConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -151,8 +157,8 @@ }, "LiquidityConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", - "level": "Liquidity enforcement level (0=none, 1=standard, 2=strict). Clamped to [0, 2] on init.", - "max_spread_pct": "Maximum allowable spread as a percentage of mid price (default 0.25).", + "level": "Liquidity enforcement level, clamped to [0, 2]: 0 disables liquidity adjustments; 1 enables quantity haircuting in RiskManager; 2 also enables execution-time spread gate with silent drop+next-business-day reschedule.", + "max_spread_pct": "Execution spread threshold used by level-2 gate: orders are dropped/rescheduled when computed spread_pct exceeds this value.", }, "MeanReversionSizerConfigs": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", @@ -198,6 +204,40 @@ "slippage_model": "Slippage model to apply: 'randomized', 'fixed', 'spread_pct', or 'none' (default 'randomized').", "pct_alpha": "Fraction of the spread used as slippage when using spread_pct model (default 0.25).", }, + "PnlMonitorConfig": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "name": "Cog name used to register/identify the PnL monitor component (default 'PnLMonitorCog').", + "enabled": "If False, the PnL monitor cog is skipped entirely.", + "enable_stop_loss": "Computed property in this class; currently always returns False unless logic is changed in code.", + "roll_profit_threshold": "Computed property in this class; threshold at which profitable positions are considered for rolling.", + "lock_in_profit_threshold": "Computed property in this class; threshold to partially close and lock in profits.", + "stop_loss_pct": "Computed property in this class; loss threshold that triggers exit behavior.", + "profit_lock_in_pct": "Computed property in this class; fraction of realized gains fed into next-trade cash.", + }, + "PnLMonitorConfigConfigurable": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "roll_profit_threshold": "Directly configurable threshold for rolling a winning position.", + "stop_loss_pct": "Directly configurable threshold for loss-cut exits.", + "profit_lock_in_pct": "Directly configurable fraction of gains to retain for future allocation.", + "lock_in_profit_threshold": "Directly configurable threshold for partial close to secure gains.", + "enable_stop_loss": "Direct switch for enabling stop-loss behavior in this configurable variant.", + }, + "VectorizedCogConfig": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "name": "Cog name used to register/identify this vectorized monitoring cog.", + "enabled": "If False, vectorized DTE checks are skipped.", + "dte_limit_enabled": "If True, DTE threshold checks are evaluated for open positions.", + "dte_threshold": "DTE value below/at which the cog marks positions for DTE-driven handling.", + }, + "PlainSizingCogConfig": { + "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "name": "Cog name used to register/identify the plain sizing cog.", + "enabled": "If False, plain sizing adjustments are not applied.", + "sizing_lev": "Base size multiplier applied by this simple sizing cog.", + "dte_limit_enabled": "If True, this cog also enforces DTE-based checks for monitored positions.", + "dte_threshold": "DTE cutoff used when dte_limit_enabled is True.", + "exclude_strategy_slug_tokens": "Strategy slug tokens that bypass this cog; any match skips plain sizing logic for that strategy.", + }, } diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index 1fe3c00..a4a2f83 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -18,6 +18,7 @@ EventTypes, FillDirection, ResultsEnum, + SignalID, SignalTypes, OrderData, Order, @@ -120,14 +121,16 @@ class OptionSignalPortfolio(Portfolio): "ExitQuantity", "ClosedQuantity", "OpenQuantity", + "TotalEntryCost", + "TotalExitCost", ] # (column_to_average, column_used_as_weight) _WEIGHTED_COLS: list[tuple[str, str]] = [ ("EntryPrice", "EntryQuantity"), ("ExitPrice", "ExitQuantity"), - ("TotalEntryCost", "EntryQuantity"), - ("TotalExitCost", "ExitQuantity"), + # ("TotalEntryCost", "EntryQuantity"), + # ("TotalExitCost", "ExitQuantity"), ] def __init__( @@ -1131,9 +1134,13 @@ def update_positions_on_fill(self, fill_event: FillEvent): entry_price=entry_price, ) # strategy_position = self.eq_strategy.get_strategy(ticker=fill_event.symbol) + parsed_signal_id = SignalID.parse(fill_event.signal_id) self.eq_strategy.open_action( ticker=fill_event.symbol, - current_date=fill_event.datetime, + + ## Current date should be signal entry date, so it stays te same even for a roll + ## Therefore, we parse signal_id, add t_plus_n business days to the entry date and use that as the current date for the strategy. + current_date=parsed_signal_id["date"] + pd.tseries.offsets.BDay(self.t_plus_n), signal_id=fill_event.signal_id, side=1 if fill_event.direction == "BUY" else -1, entry_price=entry_price, diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 723fdb4..91b30b3 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -263,7 +263,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction self._has_closed_before(pos_state.trade_id, pos_state) and pos_state.quantity > 1 ): logger.info( - f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Rolling the position to lock in profits." + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%} which is greater than {self.config.roll_profit_threshold:.2%} of entry price. Rolling the position to lock in profits." ) action = ROLL( trade_id=pos_state.trade_id, action={"quantity_diff": 0, "new_quantity": pos_state.quantity} @@ -291,7 +291,7 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction opinions.append(pos_state) else: - logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken.") + logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken. PnL is less than both lock-in threshold of {self.config.lock_in_profit_threshold:.2%} and roll threshold of {self.config.roll_profit_threshold:.2%}, and stop-loss condition is not met.") pos_state.action = HOLD(trade_id=pos_state.trade_id) pos_state.action.reason = f"PnL is {pl_pct:.2%}. No action taken." pos_state.action.verbose_info = ( diff --git a/trade/backtester_/utils/utils.py b/trade/backtester_/utils/utils.py index 46a4dc8..c4d922e 100644 --- a/trade/backtester_/utils/utils.py +++ b/trade/backtester_/utils/utils.py @@ -1,6 +1,6 @@ -from typing import Union, Dict, Optional, List, Callable from itertools import product from collections.abc import Callable as callable_func +from typing import Union, Dict, Optional, List, Callable, TYPE_CHECKING import random import inspect from typing import Union, Dict, Optional, List, Callable @@ -21,6 +21,9 @@ import inspect from trade._multiprocessing import ensure_global_start_method, PathosPool import threading +if TYPE_CHECKING: + from trade.backtester_.backtester_ import PTBacktester + From b3f168d7a1c862856074a8c26636c6cd85c3be84 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 27 May 2026 20:32:07 -0400 Subject: [PATCH 68/81] Cosmetics --- EventDriven/new_portfolio.py | 9 ++++++++- trade/datamanager/utils/logging.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/EventDriven/new_portfolio.py b/EventDriven/new_portfolio.py index a4a2f83..aed974c 100644 --- a/EventDriven/new_portfolio.py +++ b/EventDriven/new_portfolio.py @@ -473,7 +473,14 @@ def _aggregate_by_level( # meaningful even when the group is not fully closed. total_entry_cost = row.get("TotalEntryCost", 0) closed_pnl = row.get("ClosedPnL", 0) - row["ReturnPct"] = (closed_pnl / total_entry_cost) if total_entry_cost > 0 else 0 + if group_key == "SignalID": + initial_entry_cost = grp[grp["EntryTime"] == row["EntryTime"]]["TotalEntryCost"].iloc[0] + max_total_entry_cost = grp["TotalEntryCost"].max() + row["ReturnPct"] = (closed_pnl / max_total_entry_cost) if initial_entry_cost > 0 else 0 + row["InitialEntryReturnPct"] = (closed_pnl / initial_entry_cost) if initial_entry_cost > 0 else 0 + row["CampaignReturnPct"] = (closed_pnl / total_entry_cost) if total_entry_cost > 0 else 0 + else: + row["ReturnPct"] = (closed_pnl / total_entry_cost) if total_entry_cost > 0 else 0 if is_fully_closed: # Quantity as net open (should be 0 for a fully closed group) diff --git a/trade/datamanager/utils/logging.py b/trade/datamanager/utils/logging.py index 97b8a43..2a99f80 100644 --- a/trade/datamanager/utils/logging.py +++ b/trade/datamanager/utils/logging.py @@ -2,7 +2,7 @@ from git import List from trade.helpers.Logging import setup_logger, find_loggers_by_pattern, change_logger_stream_level -LOGGING_LEVEL = "DEBUG" +LOGGING_LEVEL = "WARNING" logger = setup_logger("trade.datamanager.utils", stream_log_level=LOGGING_LEVEL) FACTOR_DMS = { From 068408f540ca0121d058326a89d0c0f80032d08b Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Fri, 29 May 2026 17:36:59 -0400 Subject: [PATCH 69/81] QuantTools --- .github/agents/chidi_agent.md | 0 .vscode/mcp.json | 46 +++++ EventDriven/configs/core.py | 81 ++++++++- EventDriven/configs/vars.py | 10 +- .../riskmanager/position/cogs/plain_sizing.py | 22 ++- .../riskmanager/position/cogs/pnl_monitor.py | 172 ++++++++++++------ .../option_vectorized_retrieval.py | 18 +- 7 files changed, 275 insertions(+), 74 deletions(-) create mode 100644 .github/agents/chidi_agent.md create mode 100644 .vscode/mcp.json diff --git a/.github/agents/chidi_agent.md b/.github/agents/chidi_agent.md new file mode 100644 index 0000000..e69de29 diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..40a12e9 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,46 @@ +{ + "servers": { + "mysqlReadonly": { + "type": "stdio", + "command": "/usr/local/opt/node@20/bin/node", + "args": [ + "/Users/chiemelienwanisobi/cloned_repos/mcp-server-mysql/dist/index.js" + ], + "env": { + "MYSQL_HOST": "${input:mysql-host}", + "MYSQL_PORT": "${input:mysql-port}", + "MYSQL_USER": "${input:mysql-user}", + "MYSQL_PASS": "${input:mysql-pass}", + "ALLOW_INSERT_OPERATION": "false", + "ALLOW_UPDATE_OPERATION": "false", + "ALLOW_DELETE_OPERATION": "false", + "ALLOW_DDL_OPERATION": "false", + "ENABLE_LOGGING": "false" + } + } + }, + "inputs": [ + { + "type": "promptString", + "id": "mysql-host", + "description": "MySQL host (e.g. localhost or db.example.com)" + }, + { + "type": "promptString", + "id": "mysql-port", + "description": "MySQL port", + "default": "3306" + }, + { + "type": "promptString", + "id": "mysql-user", + "description": "MySQL read-only username" + }, + { + "type": "promptString", + "id": "mysql-pass", + "description": "MySQL read-only password", + "password": true + } + ] +} diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index e905a31..5fb8548 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -370,7 +370,7 @@ def stop_loss_pct(self) -> float: return -0.7 @property - def profit_lock_in_pct(self): + def profit_lock_in_pct(self) -> float: """ This controls the amount of profit locked to be added to cash used in next trade. For example, if a position is closed with a 50% gain and profit_lock_in_pct is 0.5, then an additional 25% of the entry price (which is 50% of the gain) @@ -378,21 +378,98 @@ def profit_lock_in_pct(self): """ return 0.25 + @property + def stop_loss_cash_threshold(self) -> Optional[float]: + """ + This is the cash-based stop loss. It looks at signal-level PnL and determines if the loss exceeds a certain threshold + """ + return None + + @property + def max_trade_dollar_size(self) -> Optional[float]: + """ + This is a dynamic property that can be used to set a maximum dollar size for trades based on current market conditions or portfolio risk. For example, during periods of high volatility, you might want to reduce the maximum trade size to limit potential losses, while in calmer markets, you could allow for larger trade sizes to capitalize on opportunities. The actual logic for determining the max trade dollar size would depend on your specific risk management strategy and could involve factors such as volatility, available capital, or recent performance. + """ + return None + + @property + def profit_lock_in_lvl(self) -> int: + """ + This is a dynamic property that determines the level of profit lock-in based on the current PnL of the position. For example, if the position has achieved a significant gain, you might want to set a higher profit lock-in level to secure more of those gains, while for smaller gains, you could set a lower level to allow for more potential upside. The actual logic for determining the profit lock-in level would depend on your specific risk management strategy and could involve factors such as the percentage gain, volatility, or time to expiration. + + profit lock pct is the amount of tick cash pnl to add to the cash available for the next trade. + For example, cash could start out with $1000, pnl of $200, and profit_lock_in_pct of 0.25. Then, $50 (which is 25% of the $200 gain) would be added to the cash available for the next trade, resulting in $1050 available for the next trade. + + + 1: Enable for only tick cash + 2: Enable for both tick cash & max trade dollar size + """ + return 1 + @pydantic_dataclass(config=ConfigDict(arbitrary_types_allowed=True)) class PnLMonitorConfigConfigurable(BaseCogConfig): """ Configurable version of PnLMonitorConfig that allows dynamic adjustment of thresholds. """ + name: str = "PnLMonitorCog" enabled: bool = True roll_profit_threshold: float = 1.0 # Default to 100% gain threshold for rolling to lock in profits - stop_loss_pct: float = -0.7 # Default to 70% loss threshold for exiting positions + stop_loss_pct: Optional[float] = -0.7 # Default to 70% loss threshold for exiting positions profit_lock_in_pct: float = 0.25 # Default to adding 25% of the gain back into cash for next trade lock_in_profit_threshold: float = ( 0.5 # Default to 50% gain threshold for closing half the position to lock in profits ) enable_stop_loss: bool = False # Default to having stop loss disabled, can be enabled based on position characteristics or market conditions + stop_loss_cash_threshold: Optional[float] = None # Optional cash-based stop loss threshold + max_trade_dollar_size: Optional[float] = ( + None # Optional dynamic property for maximum trade dollar size based on market conditions or portfolio risk + ) + profit_lock_in_lvl: int = 1 # Default to enabling profit lock-in for tick cash only + + def __post_init__(self, ctx=None): + super().__post_init__(ctx) + if self.profit_lock_in_lvl not in (1, 2): + raise ValueError( + "profit_lock_in_lvl must be either 1 (tick cash only) or 2 (tick cash and max trade dollar size)" + ) + + if self.roll_profit_threshold <= 0: + raise ValueError("roll_profit_threshold must be > 0") + + if self.lock_in_profit_threshold <= 0: + raise ValueError("lock_in_profit_threshold must be > 0") + + if not 0 <= self.profit_lock_in_pct <= 1: + raise ValueError("profit_lock_in_pct must be between 0 and 1 inclusive") + + if self.max_trade_dollar_size is not None: + if self.max_trade_dollar_size <= 0: + raise ValueError("max_trade_dollar_size must be a positive number if set") + + if self.profit_lock_in_lvl == 2 and self.max_trade_dollar_size is None: + raise ValueError( + "profit_lock_in_lvl is set to 2 (tick cash and max trade dollar size) but max_trade_dollar_size is not set. Please set max_trade_dollar_size to a positive number." + ) + + if self.stop_loss_pct is not None and self.stop_loss_pct >= 0: + raise ValueError("stop_loss_pct must be negative when provided") + + if self.stop_loss_cash_threshold is not None and self.stop_loss_cash_threshold >= 0: + raise ValueError("stop_loss_cash_threshold must be negative when provided") + + if self.enable_stop_loss: + both_none = self.stop_loss_pct is None and self.stop_loss_cash_threshold is None + both_set = self.stop_loss_pct is not None and self.stop_loss_cash_threshold is not None + if both_none: + raise ValueError( + "At least one of stop_loss_pct or stop_loss_cash_threshold must be set when enable_stop_loss is True" + ) + if both_set: + raise ValueError( + "Only one of stop_loss_pct or stop_loss_cash_threshold can be set when enable_stop_loss is True" + ) @pydantic_dataclass diff --git a/EventDriven/configs/vars.py b/EventDriven/configs/vars.py index 8a4b823..3c4250e 100644 --- a/EventDriven/configs/vars.py +++ b/EventDriven/configs/vars.py @@ -213,14 +213,22 @@ "lock_in_profit_threshold": "Computed property in this class; threshold to partially close and lock in profits.", "stop_loss_pct": "Computed property in this class; loss threshold that triggers exit behavior.", "profit_lock_in_pct": "Computed property in this class; fraction of realized gains fed into next-trade cash.", + "stop_loss_cash_threshold": "Computed property in this class; optional cash-based stop-loss threshold.", + "max_trade_dollar_size": "Computed property in this class; optional cap on per-trade dollar sizing.", + "profit_lock_in_lvl": "Computed property in this class; controls lock-in mode (1=tick cash only, 2=tick cash and max trade dollar size).", }, "PnLMonitorConfigConfigurable": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", + "name": "Cog name used to register/identify the PnL monitor component (default 'PnLMonitorCog').", + "enabled": "If False, the PnL monitor cog is skipped entirely.", "roll_profit_threshold": "Directly configurable threshold for rolling a winning position.", - "stop_loss_pct": "Directly configurable threshold for loss-cut exits.", + "stop_loss_pct": "Directly configurable percent loss threshold for exits; set to None when using cash stop-loss mode.", "profit_lock_in_pct": "Directly configurable fraction of gains to retain for future allocation.", "lock_in_profit_threshold": "Directly configurable threshold for partial close to secure gains.", "enable_stop_loss": "Direct switch for enabling stop-loss behavior in this configurable variant.", + "stop_loss_cash_threshold": "Optional cash-based stop-loss threshold; mutually exclusive with stop_loss_pct when stop-loss is enabled.", + "max_trade_dollar_size": "Optional cap on trade dollar size; with lvl 1 it enforces constant-size capped mode, with lvl 2 it caps base cash before profit increment.", + "profit_lock_in_lvl": "Lock-in mode selector (1=constant-size when max is set, otherwise tick-cash lock-in; 2=requires max_trade_dollar_size and allows capped-base plus profit increment).", }, "VectorizedCogConfig": { "run_name": "A name identifier for this run/session, used to tag and track configuration across backtest runs.", diff --git a/EventDriven/riskmanager/position/cogs/plain_sizing.py b/EventDriven/riskmanager/position/cogs/plain_sizing.py index 2482458..a152016 100644 --- a/EventDriven/riskmanager/position/cogs/plain_sizing.py +++ b/EventDriven/riskmanager/position/cogs/plain_sizing.py @@ -100,10 +100,7 @@ def on_new_position(self, state: NewPositionState) -> None: ) return else: - logger.info( - f"Applying plain sizing for signal {order.signal_id}. " - f"Trade ID: {order['data']['trade_id']}" - ) + logger.info(f"Applying plain sizing for signal {order.signal_id}. Trade ID: {order['data']['trade_id']}") undl_data = state.undl_at_time_data option_chain = state.at_time_data @@ -125,6 +122,22 @@ def on_new_position(self, state: NewPositionState) -> None: delta=delta, delta_limit=limit, ) + logger.info( + "Plain sizing calculated values for trade %s | signal %s | cash_available=%s | " + "tick_cash=%s | tick_cash_scaled=%s | underlier_price=%s | option_price=%s | " + "option_delta=%s | sizing_lev=%s | delta_limit=%s | quantity=%s", + order["data"]["trade_id"], + order["signal_id"], + cash_available, + request.tick_cash, + request.is_tick_cash_scaled, + chain_spot, + opt_price, + delta, + self.config.sizing_lev, + limit, + q, + ) if q == 0 and cash_available >= opt_price * 100: logger.info( @@ -155,6 +168,7 @@ def on_new_position(self, state: NewPositionState) -> None: delta_lmt=limit, new_quantity=q, ) + logger.info(f"Storing plain sizing metadata for trade_id {order['data']['trade_id']}: {metadata}") self.position_metadata[order["data"]["trade_id"]] = metadata def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: diff --git a/EventDriven/riskmanager/position/cogs/pnl_monitor.py b/EventDriven/riskmanager/position/cogs/pnl_monitor.py index 91b30b3..ec913bb 100644 --- a/EventDriven/riskmanager/position/cogs/pnl_monitor.py +++ b/EventDriven/riskmanager/position/cogs/pnl_monitor.py @@ -98,29 +98,24 @@ class PnLMonitorCog(BaseCog): def __init__( self, config: Optional[PnlMonitorConfig | PnLMonitorConfigConfigurable] = None, - max_trade_dollar_size: Optional[float] = None, ): """Initialize the PnL monitor cog. Args: config: Optional runtime configuration. If not provided, a default `PnlMonitorConfig` is created. - max_trade_dollar_size: Optional cap on trade dollar size to prevent excessive allocation when scaling up with profits. If None, no cap is applied. - Notes: - `enable_stop_loss` is initialized to ``False`` so the stop-loss branch is opt-in. - `default_config` remains available for framework-level defaults. """ - assert config is None or isinstance(config, (PnlMonitorConfig, PnLMonitorConfigConfigurable)), ( - "Config must be of type PnlMonitorConfig or PnLMonitorConfigConfigurable" - ) + if config is not None and not isinstance(config, (PnlMonitorConfig, PnLMonitorConfigConfigurable)): + raise TypeError("Config must be of type PnlMonitorConfig or PnLMonitorConfigConfigurable") if config is None: config = PnlMonitorConfig() super().__init__(config) self.config: PnlMonitorConfig | PnLMonitorConfigConfigurable = config - self.max_trade_dollar_size = max_trade_dollar_size def on_new_position(self, new_position_state: NewPositionState) -> None: """Handle a newly created position state. @@ -136,22 +131,34 @@ def on_new_position(self, new_position_state: NewPositionState) -> None: pass def on_new_order_request(self, new_request_state: OrderRequest) -> None: - """Adjust request-level cash when current symbol PnL is positive. + """Adjust request cash using cap and profit-lock rules. + + Processing steps: + 1. Normalize `tick_cash` to dollar units when needed. + 2. If `max_trade_dollar_size` is set, cap cash first. + 3. If symbol pnl is positive and profit-lock mode is active, + add `profit_lock_in_pct * pnl` to a mode-specific base cash. - Process: - 1. Read `symbol_total_pnl` and request `tick_cash`. - 2. Normalize `tick_cash` into scaled-dollar form if required. - 3. If PnL > 0, remove embedded PnL from tick cash to avoid - double-counting. - 4. Add back 25% of PnL as incremental allocation. - 5. Persist updated `tick_cash` and mark `is_tick_cash_scaled=True`. + Profit-lock mode activation: + - Active when max is set and `profit_lock_in_lvl == 2`. + - Active when max is not set and `profit_lock_in_lvl == 1`. + + Effective behavior by combination: + - max=None, lvl=1: profit-lock adjustment runs. + - max=None, lvl=2: no adjustment (invalid by config validation). + - max=set, lvl=1: constant capped size (no profit addition). + - max=set, lvl=2: capped base plus profit increment. + + Notes: + - Profit increment is applied only when `symbol_total_pnl > 0`. + - Updates are applied in-place to `new_request_state`. Args: new_request_state: Incoming order request that may be resized prior to downstream execution and sizing. Returns: - None. Updates are applied in place on `new_request_state`. + None. """ logger.info( f"Received new order request for {new_request_state.symbol} with signal ID {new_request_state.signal_id}. Monitoring PnL for this request." @@ -164,34 +171,46 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: tick_cash = tick_cash * 100 if not new_request_state.is_tick_cash_scaled else tick_cash ## If max_trade_dollar_size is set, cap the tick_cash to prevent excessive allocation - if self.max_trade_dollar_size is not None: + if self.config.max_trade_dollar_size is not None: logger.info( - f"Max trade dollar size is set to ${self.max_trade_dollar_size:.2f}. Original tick cash: ${tick_cash:.2f}." + f"Max trade dollar size is set to ${self.config.max_trade_dollar_size:.2f}. Original tick cash: ${tick_cash:.2f}." ) ## If tick_cash > max_trade_dollar_size, tick_cash = max_trade_dollar_size ## If tick_cash <= max_trade_dollar_size, tick_cash remains unchanged - new_request_state.tick_cash = min(tick_cash, self.max_trade_dollar_size) - new_request_state.is_tick_cash_scaled = ( - True # Mark as scaled since we're treating it as a dollar amount now - ) - return + tick_cash = min(tick_cash, self.config.max_trade_dollar_size) + new_request_state.tick_cash = tick_cash + new_request_state.is_tick_cash_scaled = True + + ## PL adjustment decider + should_adjust = ( + ## If max_trade_dollar_size is set & lvl == 2 + (self.config.max_trade_dollar_size is not None and self.config.profit_lock_in_lvl == 2) + ## Or if max_trade_dollar_size is not set and lvl == 1 (default) + or (self.config.max_trade_dollar_size is None and self.config.profit_lock_in_lvl == 1) + ) ## If have profits, add 25% of the profits to the tick cash to scale up the position and lock in profits. - if pnl is not None and pnl > 0: + if pnl is not None and pnl > 0 and should_adjust: additional_tick_cash = ( pnl * self.config.profit_lock_in_pct ) ## Add 25% of the profits to the tick cash to scale up the position and lock in profits. ## Undo pnl from tick cash to avoid double counting, then add the additional tick cash to lock in profits. undone_pnl_tick_cash = tick_cash - pnl - new_tick_cash = undone_pnl_tick_cash + additional_tick_cash + + ## Either use the undone_pnl_tick_cash for base cash + ## Or use the original tick_cash for base cash if profit_lock_in_lvl is 2 + base_tick_cash = undone_pnl_tick_cash if self.config.profit_lock_in_lvl == 1 else tick_cash + new_tick_cash = base_tick_cash + additional_tick_cash + logger.info( - f"Details: PnL: {pnl:.2f}, Original Tick Cash: {tick_cash:.2f}, Undone PnL Tick Cash: {undone_pnl_tick_cash:.2f}, Additional Tick Cash to Lock in Profits: {additional_tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}" + f"Details: PnL: {pnl:.2f}, Original Tick Cash: {tick_cash:.2f}, Base Tick Cash: {base_tick_cash:.2f}, Additional Tick Cash to Lock in Profits: {additional_tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}" ) logger.info( f"Adding {additional_tick_cash:.2f} to tick cash for {new_request_state.symbol} to lock in profits. Original Tick Cash: {tick_cash:.2f}, New Tick Cash: {new_tick_cash:.2f}, PnL: {pnl:.2f}" ) + new_request_state.tick_cash = new_tick_cash new_request_state.is_tick_cash_scaled = True @@ -202,20 +221,21 @@ def on_new_order_request(self, new_request_state: OrderRequest) -> None: ) def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogActions: - """Run PnL rule evaluation and return action opinions. - - Process: - 1. Iterate over open position states in the portfolio. - 2. Compute per-position PnL percentage: - ``pl_pct = pnl / (entry_price * quantity)`` with zero guards. - 3. Apply decision rules in order: - - ``pl_pct > 0.5`` and quantity > 1 -> partial CLOSE. - - ``pl_pct > 1.0`` and roll criteria pass -> ROLL. - - ``pl_pct <= -0.7`` with stop-loss enabled -> full CLOSE. - - otherwise -> HOLD. - 4. Attach analysis metadata (`analysis_date`, `reason`, - `verbose_info`, `effective_date`) to generated actions. - 5. Return aggregated `CogActions` for downstream arbitration. + """Evaluate each open position and emit HOLD, CLOSE, or ROLL opinions. + + Decision flow per position: + 1. Compute pnl ratio against cost basis. + 2. If pnl ratio is above lock-in threshold and quantity > 1, + partially CLOSE to secure gains. + 3. Else if pnl ratio is above roll threshold and roll conditions + pass, emit ROLL. + 4. Else if stop-loss is enabled and stop condition is hit, + fully CLOSE. + 5. Otherwise emit HOLD. + + Stop-loss supports two mutually exclusive modes: + - pct mode: compare pnl ratio to stop_loss_pct + - cash mode: compare signal-level pnl to stop_loss_cash_threshold Args: portfolio_context: Portfolio snapshot and metadata for the current @@ -238,12 +258,14 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction portfolio_state = portfolio_context.portfolio last_updated = portfolio_state.last_updated t_plus_n_bdays = pd.offsets.BusinessDay(max(t_plus_n, 1)) + is_cash_stop_loss = self.config.stop_loss_cash_threshold is not None for pos_state in positions: pl_pct = ( pos_state.pnl / (pos_state.entry_price * pos_state.quantity) if pos_state.entry_price * pos_state.quantity != 0 else 0 ) + signal_pnl = pos_state.signal_total_pnl if pos_state.signal_total_pnl is not None else 0 if pl_pct > self.config.lock_in_profit_threshold and pos_state.quantity > 1: qdiff = -math.ceil(pos_state.quantity / 2) new_q = pos_state.quantity + qdiff @@ -274,24 +296,66 @@ def _analyze_impl(self, portfolio_context: PositionAnalysisContext) -> CogAction action.effective_date = last_updated + t_plus_n_bdays pos_state.action = action opinions.append(pos_state) + else: + logger.info( + f"Position {pos_state.trade_id} exceeded roll threshold but roll criteria were not met. No action taken." + ) + pos_state.action = HOLD(trade_id=pos_state.trade_id) + pos_state.action.reason = ( + f"PnL is {pl_pct:.2%}. Roll threshold exceeded but roll criteria were not met. No action taken." + ) + pos_state.action.verbose_info = ( + f"Position {pos_state.trade_id} exceeded roll threshold but did not qualify for rolling." + ) + opinions.append(pos_state) ## Stop loss branch: close <= stop_loss_pct to prevent further losses. This branch is disabled by default and can be enabled by setting enable_stop_loss to True. - elif pl_pct <= self.config.stop_loss_pct and self.config.enable_stop_loss: - logger.info( - f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." - ) - action = CLOSE( - trade_id=pos_state.trade_id, action={"quantity_diff": -pos_state.quantity, "new_quantity": 0} - ) - action.analysis_date = portfolio_context.date - action.reason = f"PnL is {pl_pct:.2%} which is less than or equal to {self.config.stop_loss_pct:.2%} of entry price. Closing the position to prevent further losses." - action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." - action.effective_date = last_updated + t_plus_n_bdays - pos_state.action = action - opinions.append(pos_state) + elif self.config.enable_stop_loss: + if not is_cash_stop_loss: + stop_hit = self.config.stop_loss_pct is not None and pl_pct <= self.config.stop_loss_pct + else: + stop_hit = ( + self.config.stop_loss_cash_threshold is not None + and signal_pnl <= self.config.stop_loss_cash_threshold + ) + if stop_hit: + cash_threshold_msg = ( + f"{self.config.stop_loss_cash_threshold:.2f}" + if self.config.stop_loss_cash_threshold is not None + else "N/A" + ) + pct_threshold_msg = ( + f"{self.config.stop_loss_pct:.2%}" if self.config.stop_loss_pct is not None else "N/A" + ) + logger_msg = ( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Signal-level PnL is {signal_pnl:.2f}. " + f"Cash threshold is {cash_threshold_msg}. " + f"pct stop loss threshold is {pct_threshold_msg}. " + ) + logger.info(logger_msg) + action = CLOSE( + trade_id=pos_state.trade_id, action={"quantity_diff": -pos_state.quantity, "new_quantity": 0} + ) + action.analysis_date = portfolio_context.date + if is_cash_stop_loss: + action.reason = ( + f"Signal-level PnL is {signal_pnl:.2f}, below cash stop-loss threshold " + f"{cash_threshold_msg}. Closing the position to prevent further losses." + ) + else: + action.reason = ( + f"PnL is {pl_pct:.2%} which is less than or equal to {pct_threshold_msg} " + f"of entry price. Closing the position to prevent further losses." + ) + action.verbose_info = f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. Closing the position to prevent further losses." + action.effective_date = last_updated + t_plus_n_bdays + pos_state.action = action + opinions.append(pos_state) else: - logger.info(f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken. PnL is less than both lock-in threshold of {self.config.lock_in_profit_threshold:.2%} and roll threshold of {self.config.roll_profit_threshold:.2%}, and stop-loss condition is not met.") + logger.info( + f"Position {pos_state.trade_id} has PnL of {pl_pct:.2%}. No action taken. PnL is less than both lock-in threshold of {self.config.lock_in_profit_threshold:.2%} and roll threshold of {self.config.roll_profit_threshold:.2%}, and stop-loss condition is not met." + ) pos_state.action = HOLD(trade_id=pos_state.trade_id) pos_state.action.reason = f"PnL is {pl_pct:.2%}. No action taken." pos_state.action.verbose_info = ( diff --git a/trade/backtester_/option_vectorized_retrieval.py b/trade/backtester_/option_vectorized_retrieval.py index 035604d..0edd876 100644 --- a/trade/backtester_/option_vectorized_retrieval.py +++ b/trade/backtester_/option_vectorized_retrieval.py @@ -52,7 +52,6 @@ from trade.backtester_._multi_asset_strategy import MultiAssetStrategy from trade.datamanager.market_data import get_timeseries_obj from trade.helpers.helper import to_datetime -from trade.datamanager.option_spot import OptionSpotDataManager DateLike = Union[datetime, str] @@ -566,22 +565,15 @@ def _retrieve_signal_ohlc( end_str = to_datetime(signal.end_date).strftime("%Y-%m-%d") try: - # ohlc = retrieve_eod_ohlc( - # symbol=contract.ticker, - # start_date=start_str, - # end_date=end_str, - # strike=contract.strike, - # exp=contract.expiration, - # right=contract.right, - # print_url=self.print_url, - # ) - ohlc = OptionSpotDataManager(symbol=contract.ticker).get_option_spot_timeseries( + ohlc = retrieve_eod_ohlc( + symbol=contract.ticker, start_date=start_str, end_date=end_str, strike=contract.strike, - expiration=contract.expiration, + exp=contract.expiration, right=contract.right, - ).timeseries + print_url=self.print_url, + ) except Exception as exc: return pd.DataFrame(), UnmatchedSignal( signal_id=signal.signal_id, From 41a9a9596976f421d3b61a288fe4085a2a929eb3 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Fri, 29 May 2026 17:38:15 -0400 Subject: [PATCH 70/81] SMH --- trade/backtester_/option_vectorized_retrieval.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/trade/backtester_/option_vectorized_retrieval.py b/trade/backtester_/option_vectorized_retrieval.py index 0edd876..66ae395 100644 --- a/trade/backtester_/option_vectorized_retrieval.py +++ b/trade/backtester_/option_vectorized_retrieval.py @@ -606,7 +606,7 @@ def _normalize_chain( ) -> Tuple[pd.DataFrame, Optional[str]]: """Normalize chain DataFrame into ranking columns used by selector.""" frame = pd.DataFrame(chain).copy() - strike_col = self._find_col(frame.columns.tolist(), ["strike", "Strike"]) + strike_col = self._find_col(frame.columns.tolist(), ["strike", "Strike"]) right_col = self._find_col(frame.columns.tolist(), ["right", "Right", "put_call", "putcall", "option_type"]) exp_col = self._find_col(frame.columns.tolist(), ["expiration", "Expiration", "exp", "expiry"]) if strike_col is None or right_col is None or exp_col is None: From f14cc4e32efed35083953890fee2372f4d319f89 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 3 Jun 2026 21:21:27 -0400 Subject: [PATCH 71/81] Stuff changed --- .gitignore | 1 + .vscode/settings.json | 44 +++++++++++++------ EventDriven/riskmanager/market_timeseries.py | 2 +- .../riskmanager/position/cogs/vectorized.py | 4 +- trade/backtester_/utils/aggregators.py | 22 +++++----- 5 files changed, 46 insertions(+), 27 deletions(-) diff --git a/.gitignore b/.gitignore index fde0faa..b840b62 100644 --- a/.gitignore +++ b/.gitignore @@ -202,6 +202,7 @@ trade/optionlib/model_issue_notes.txt EventDriven/notebooks/riskmanager_streamline/ EventDriven/tests.py trade/assets/notebooks +EventDriven/riskmanager/notebooks/large_unexplained_pnl_trades.csv # ignore all notebooks diff --git a/.vscode/settings.json b/.vscode/settings.json index 4a2e5d3..22da5ee 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,17 +10,25 @@ }, "editor.formatOnSave": false, "python.terminal.useEnvFile": true, - - // Ruff: Linting only (style, code quality) - "ruff.enable": true, - "ruff.lint.enable": true, - - // Disable pylint (redundant with ruff) - "python.linting.pylintEnabled": false, - "python.linting.enabled": true, - - // Keep Pylance: REQUIRED for IntelliSense and semantic highlighting - "python.languageServer": "Pylance", + + // --- Cursor: Cursor Pyright --- + // "python.languageServer": "None" disables MS Pylance/Jedi so Cursor Pyright runs. + // In VS Code: override to "Pylance" or "Default" in User settings if IntelliSense is missing. + "python.languageServer": "None", + + // 1:1 with python.analysis.* (same values as Pylance block below) + "cursorpyright.analysis.useLibraryCodeForTypes": true, + "cursorpyright.analysis.typeCheckingMode": "off", + "cursorpyright.analysis.diagnosticMode": "openFilesOnly", + "cursorpyright.analysis.autoImportCompletions": true, + + // Pylance-only — no cursorpyright.analysis.* key exists: + // indexing, includeAliasesFromUserFiles, userFileIndexingLimit, packageIndexDepths + + // Optional Cursor Pyright extras (no python.analysis.* twin in your config) + "cursorpyright.analysis.autoSearchPaths": true, + + // --- VS Code: Pylance (python.analysis.*) --- "python.analysis.useLibraryCodeForTypes": true, "python.analysis.typeCheckingMode": "off", "python.analysis.diagnosticMode": "openFilesOnly", @@ -36,6 +44,17 @@ "includeAllSymbols": true } ], + + // Ruff: Linting only (style, code quality) + "ruff.enable": true, + "ruff.lint.enable": true, + + // Disable pylint (redundant with ruff) + "python.linting.pylintEnabled": false, + "python.linting.enabled": true, + + "jupytextSync.pythonExecutable": "/Users/chiemelienwanisobi/miniconda3/envs/openbb_new_use/bin/python", + "python-envs.pythonProjects": [ { "path": ".", @@ -43,5 +62,4 @@ "packageManager": "ms-python.python:conda" } ] - -} \ No newline at end of file +} diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 999f365..2d879d2 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -539,7 +539,7 @@ def calculate_option_data(self, position_id: str, date: Union[datetime, str]) -> return d ## Data not in cache - perform full calculation - logger.critical(f"Position Data for {position_id} not available, calculating greeks. Load time ~5 minutes") + logger.info(f"Position Data for {position_id} not available, calculating greeks. Load time ~5 minutes") ## Initialize the Long and Short Lists long = [] diff --git a/EventDriven/riskmanager/position/cogs/vectorized.py b/EventDriven/riskmanager/position/cogs/vectorized.py index 08a8e0e..a427e91 100644 --- a/EventDriven/riskmanager/position/cogs/vectorized.py +++ b/EventDriven/riskmanager/position/cogs/vectorized.py @@ -72,7 +72,9 @@ def __init__(self, config: Optional[VectorizedCogConfig] = None): """ if config is None: config = VectorizedCogConfig() + super().__init__(config) + self.config: VectorizedCogConfig = config def on_new_position(self, new_pos_state: NewPositionState) -> None: """ @@ -187,7 +189,7 @@ def _analyze_impl(self, context: PositionAnalysisContext) -> CogActions: except Exception as e: logger.warning(f"Error calculating DTE for position {pos_state.trade_id}: {e}. Skipping roll check.") - logger.warning(f"Stack trace: ", exc_info=True) + logger.warning("Stack trace: ", exc_info=True) continue logger.info( diff --git a/trade/backtester_/utils/aggregators.py b/trade/backtester_/utils/aggregators.py index 8b683b7..2ae81d4 100644 --- a/trade/backtester_/utils/aggregators.py +++ b/trade/backtester_/utils/aggregators.py @@ -159,22 +159,20 @@ def buyNhold(port_stats: dict) -> float: def cagr(equity_timeseries: pd.DataFrame) -> float: - """ - Parameters: - equity_timeseries (pd.DataFrame): This is the timeseries of the periodic equity values + ts = equity_timeseries.sort_index() - - Returns: - float: Returns average annualize retruns for the portfolio. Cumulative Annual Growth Rate - """ - ts = equity_timeseries begin_val = ts["Total"].iloc[0] end_val = ts["Total"].iloc[-1] + if isinstance(ts.index, pd.DatetimeIndex): - days = (ts.index.max() - ts.index.min()).days - elif isinstance(ts.index, pd.RangeIndex): - days = ts.index.max() - ts.index.min() - return ((end_val / begin_val) ** (252 / days) - 1) * 100 + elapsed_years = (ts.index[-1] - ts.index[0]).days / 365.25 + else: + raise TypeError("CAGR requires a DatetimeIndex") + + if elapsed_years <= 0: + raise ValueError("Need at least two distinct timestamps") + + return ((end_val / begin_val) ** (1 / elapsed_years) - 1) * 100 def vol_annualized( From 4aa094ab8969db4efecb936ef7bbc77ef9cc0983 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:25:16 -0400 Subject: [PATCH 72/81] Updating configs --- EventDriven/configs/core.py | 14 ++++++++++++++ .../riskmanager/position/cogs/donchian_cog.py | 16 +--------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/EventDriven/configs/core.py b/EventDriven/configs/core.py index 5fb8548..f152a9a 100644 --- a/EventDriven/configs/core.py +++ b/EventDriven/configs/core.py @@ -505,3 +505,17 @@ class PlainSizingCogConfig(BaseCogConfig): dte_limit_enabled: bool = True dte_threshold: int = 30 exclude_strategy_slug_tokens: List[str] = Field(default_factory=list) + + +@pydantic_dataclass( + config=ConfigDict(arbitrary_types_allowed=True), +) +class DonchianMomentumCogConfig(BaseCogConfig): + """Configuration for DonchianMomentumCog.""" + + name: str = "DonchianMomentumCog" + sizing_lev: int = 1 + min_scale: float = 0.75 + max_scale: float = 2.0 + dte_limit_enabled: bool = True + dte_threshold: int = 15 \ No newline at end of file diff --git a/EventDriven/riskmanager/position/cogs/donchian_cog.py b/EventDriven/riskmanager/position/cogs/donchian_cog.py index 13daff8..1b366d1 100644 --- a/EventDriven/riskmanager/position/cogs/donchian_cog.py +++ b/EventDriven/riskmanager/position/cogs/donchian_cog.py @@ -25,9 +25,7 @@ from trade.backtester_._multi_asset_strategy import MultiAssetStrategy from dataclasses import dataclass import pandas as pd -from EventDriven.configs.base import pydantic_dataclass -from EventDriven.configs.core import BaseCogConfig -from pydantic import ConfigDict +from EventDriven.configs.core import DonchianMomentumCogConfig from EventDriven.dataclasses.limits import PositionLimits from trade.helpers.Logging import setup_logger from EventDriven.riskmanager.actions import ROLL, Changes @@ -36,18 +34,6 @@ logger = setup_logger("EventDriven.riskmanager.position.cogs.donchian_cog") -@pydantic_dataclass( - config=ConfigDict(arbitrary_types_allowed=True), -) -class DonchianMomentumCogConfig(BaseCogConfig): - """Configuration for DonchianMomentumCog.""" - - name: str = "DonchianMomentumCog" - sizing_lev: int = 1 - min_scale: float = 0.75 - max_scale: float = 2.0 - dte_limit_enabled: bool = True - dte_threshold: int = 15 @dataclass From 07d610ec4d85abf23683b446a19431d5ee907c47 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:06:10 -0400 Subject: [PATCH 73/81] Fxies --- EventDriven/riskmanager/market_timeseries.py | 31 ++++++++++++++++++-- trade/datamanager/option_spot.py | 6 +++- trade/helpers/threads.py | 2 +- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/EventDriven/riskmanager/market_timeseries.py b/EventDriven/riskmanager/market_timeseries.py index 2d879d2..14e1ee7 100644 --- a/EventDriven/riskmanager/market_timeseries.py +++ b/EventDriven/riskmanager/market_timeseries.py @@ -153,7 +153,13 @@ from trade.helpers.decorators import timeit from trade.helpers.threads import runThreads # noqa from trade.helpers.pools import runProcesses # noqa -from trade.helpers.helper import compare_dates, parse_option_tick, generate_option_tick_new, to_datetime +from trade.helpers.helper import ( + change_to_last_busday, # noqa + compare_dates, + parse_option_tick, + generate_option_tick_new, + to_datetime, +) from EventDriven.configs.core import SkipCalcConfig, UndlTimeseriesConfig, OptionPriceConfig from trade.assets.rates import get_risk_free_rate_helper_v2 from threading import Lock @@ -534,7 +540,7 @@ def calculate_option_data(self, position_id: str, date: Union[datetime, str]) -> ## Check cache first - early return if data exists d = self.get_position_data(position_id) - if not d.empty and pd.to_datetime(date) in d.index: + if not d.empty and self._check_date_present_in_index(d, date): logger.info(f"Position Data for {position_id} already available in cache, returning cached data") return d @@ -896,6 +902,25 @@ def generate_option_data_for_trade(self, opttick, check_date) -> pd.DataFrame: ) ## Perform sanity check to ensure the generated option data includes the check date for all associated option ticks, and if not, clear the relevant cache entries to maintain cache integrity. return final_data + def _check_date_present_in_index( + self, + data: pd.DataFrame, + check_date: Union[datetime, str], + ) -> bool: + """Return True when option data has a row for the requested check date.""" + if data is None or data.empty: + return False + + check_ts = to_datetime(check_date) + if check_ts in data.index: + return True + + resolved = to_datetime(check_date).date() + if resolved in data.index: + return True + + return (data.index.date == check_ts.date()).any() + def _option_data_sanity_check( self, data: pd.DataFrame, @@ -903,7 +928,7 @@ def _option_data_sanity_check( check_date: Union[datetime, str], ) -> None: """Perform sanity checks on the option data for the associated option ticks.""" - if check_date not in data.index: + if not self._check_date_present_in_index(data, check_date): logger.warning(f"Check date {check_date} not found in option data index for associated ticks: {associated_optticks}") ## Delete the cached data for the associated option ticks. diff --git a/trade/datamanager/option_spot.py b/trade/datamanager/option_spot.py index d1e2923..39bef78 100644 --- a/trade/datamanager/option_spot.py +++ b/trade/datamanager/option_spot.py @@ -395,7 +395,11 @@ def get_option_spot_timeseries( # If the requested window has only checked-missing dates, add placeholder # NaN rows so sanitization and later cache hits can shape the output range. if classification.checked_missing_dates: - print(f"Adding placeholder rows for {len(classification.checked_missing_dates)} checked-missing dates for key {key}.") + logger.info( + "Adding placeholder rows for %s checked-missing dates for key %s.", + len(classification.checked_missing_dates), + key, + ) checked_missing_idx = pd.DatetimeIndex(to_datetime(classification.checked_missing_dates)) checked_missing_idx = default_timestamp(checked_missing_idx) diff --git a/trade/helpers/threads.py b/trade/helpers/threads.py index d181f47..3405ce8 100644 --- a/trade/helpers/threads.py +++ b/trade/helpers/threads.py @@ -73,6 +73,6 @@ def runThreads( shutdown_event = True raise except Exception as e: - logger.error("Error occurred: ", e) + logger.error("Error occurred: %s", e) raise return results if block else results From 2f0102a81165e34a287bdf6b917985fe5e1ec551 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:06:23 -0400 Subject: [PATCH 74/81] Changing conventions --- EventDriven/riskmanager/position/live_cogs/limits.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/EventDriven/riskmanager/position/live_cogs/limits.py b/EventDriven/riskmanager/position/live_cogs/limits.py index 7276d3b..8afd060 100644 --- a/EventDriven/riskmanager/position/live_cogs/limits.py +++ b/EventDriven/riskmanager/position/live_cogs/limits.py @@ -222,9 +222,13 @@ logger = setup_logger("EventDriven.riskmanager.position.live_cogs.limits") -def enable_storing_to_db(enable: bool = True): +def enable_storing_to_db(*args, **kwargs): """Utility to enable or disable database storage of limits globally for testing.""" - LiveCOGLimitsAndSizingCog.SAVE_LIMITS_TO_DB = enable + LiveCOGLimitsAndSizingCog.SAVE_LIMITS_TO_DB = True + +def disable_storing_to_db(*args, **kwargs): + """Utility to disable database storage of limits globally for testing.""" + LiveCOGLimitsAndSizingCog.SAVE_LIMITS_TO_DB = False def reset_storing_to_db(): From c5dbf5092ab0eb7b51608f70f55065bfe5cbfc05 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:24:20 -0400 Subject: [PATCH 75/81] cursor: add coding standards rule and commit strategy skill Co-authored-by: Cursor --- .cursor/rules/coding-standards.mdc | 58 +++++++++++ .cursor/skills/commit-strategy/SKILL.md | 122 ++++++++++++++++++++++++ 2 files changed, 180 insertions(+) create mode 100644 .cursor/rules/coding-standards.mdc create mode 100644 .cursor/skills/commit-strategy/SKILL.md diff --git a/.cursor/rules/coding-standards.mdc b/.cursor/rules/coding-standards.mdc new file mode 100644 index 0000000..efa3725 --- /dev/null +++ b/.cursor/rules/coding-standards.mdc @@ -0,0 +1,58 @@ +--- +description: Project coding standards — commits, type hints, dates, docstrings, naming, dataclasses +alwaysApply: true +--- + +# Coding Standards + +## Commit Requests (Always-On) + +When the user asks for a commit: + +- Generate a commit strategy first; separate changes by concern before committing. +- For each planned commit, state what will be committed (files/hunks) and the commit message. +- Keep unrelated concerns in separate commits. +- Only commit when explicitly requested; never commit proactively. +- Follow the detailed workflow in `.cursor/skills/commit-strategy/SKILL.md`. + +## Type Hints + +- 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 from `typing`: `Optional, Union, List, Dict, Tuple, ClassVar`. + +## Date/Time Conversion + +- **Always use `to_datetime` from `trade.helpers.helper`** — never `datetime.strptime()` or `pd.to_datetime()` directly. +- Import: `from trade.helpers.helper import to_datetime` +- Handles single values and iterables; tries `%Y-%m-%d` first, then lets pandas guess; supports optional `format`. + +## Docstrings (Always-On for Python) + +Mandatory for every Python file created or edited. Follow PEP 257 and Google-style sections. + +- Every module, class, function, and method (including `__init__` and private helpers) must have a docstring with a concise summary line. +- Add `Args`, `Returns`, and `Raises` when applicable; include `Examples` with executable usage when practical. +- Keep docstrings factual; update stale docstrings in the same edited scope. +- For detailed templates, use `.github/skills/docstring-standards/SKILL.md`. + +### Module-Level Docstring Format + +1. One-sentence summary ending with a period. +2. Blank line, then a 2–4 line overview paragraph. +3. Blank line, then structured sections with trailing colons: Core Classes, Core Dataclasses, Core Functions, Processing Flow, Risk/Assumptions, Caching Strategy, Usage (as applicable). +4. Usage section: short doctest-style snippet when practical. + +## Naming Conventions + +- **Classes:** Managers end with `Manager`; results end with `Result`; configs end with `Config`. +- **Methods:** `get_*` for retrieval; `_load_*` for private loading; `_compute_*` for calculations. +- **Variables:** `_str` suffix for string dates (`start_str`); `_dt` suffix for date objects (`start_dt`). + +## Dataclasses + +- Prefer `@dataclass` for data containers. +- Use pydantic `@dataclass` for strict validation (`from pydantic.dataclasses import dataclass as pydantic_dataclass`). +- Result classes inherit from base `Result`. +- Use `frozen=True, slots=True` for immutable configs (e.g., `CacheSpec`). diff --git a/.cursor/skills/commit-strategy/SKILL.md b/.cursor/skills/commit-strategy/SKILL.md new file mode 100644 index 0000000..932069c --- /dev/null +++ b/.cursor/skills/commit-strategy/SKILL.md @@ -0,0 +1,122 @@ +--- +name: commit-strategy +description: >- + Plans and executes git commits with concern-based grouping, pre-commit scan, + and safe git workflow. Use when the user asks to commit, create commits, propose + a commit plan, stage changes, or review changes for commit. +--- + +# Commit Strategy + +## Project Context + +Before planning or executing commits, read `.cursor/rules/coding-standards.mdc` for repo-specific coding standards and commit triggers. Match commit message areas and conventions to that rule and recent `git log` output. + +## When to Use + +- User explicitly asks to commit, stage, or propose commits. +- User asks for a commit plan or message review. +- **Do not** commit proactively. **Do not** activate for general git questions unless committing is involved. + +## Objectives + +- Separate changes into the most relevant concerns. +- Produce a clear commit strategy before committing. +- Ensure each commit is cohesive, minimal, and reversible. + +## Required Workflow + +### 1. Inspect (run in parallel) + +From the repo root that has changes: + +```bash +git status +git diff +git log -10 --oneline +``` + +If scoping a branch: `git diff main...HEAD` (or the repo default base). + +Multi-root workspace: confirm which repo has changes before planning. + +### 2. Pre-Commit Quality Scan + +Scan changed files only: + +- Potential bugs and logic failures +- Misspellings (strings, docs, logs) +- Commented-out code blocks (not plain explanatory comments) +- Debug artifacts (`print`, `pdb`, local paths) +- Secrets and credential files — warn and **exclude** from commit + +### 3. Group by Concern + +- One concern per commit: feature, bugfix, refactor, docs, tests, config, tooling +- Split by subsystem when changes are independent +- Keep tests with the behavior they verify +- Isolate formatting, IDE settings, and tooling config when mixed with code +- Order commits: base/refactor first, then dependent changes +- No empty commits — if nothing to commit, say so and stop + +### 4. Present Strategy (before executing) + +Use this template: + +**0. Pre-Commit Scan** +- Potential bugs: \ +- Logic failures: \ +- Misspellings: \ +- Commented-out code blocks: \ +- Excluded files: \ + +**N. Commit N** +- Scope: \ +- Concern: \ +- Why: \ +- Message: \ + +### 5. Execute (only after explicit user approval) + +For each planned commit, sequentially: + +1. Stage only that commit's files +2. Commit via HEREDOC: + +```bash +git commit -m "$(cat <<'EOF' +: + +EOF +)" +``` + +3. Run `git status` to verify +4. Repeat for remaining commits; final `git status` when done + +## Commit Message Rules + +- Imperative mood; concise subject (~50–72 chars) +- Format: `: ` +- Explain intent, not implementation noise +- Match recent repo style from `git log` when unclear + +## Git Safety + +- Never update git config +- Never `--no-verify`, `--no-gpg-sign`, force push, or hard reset unless user explicitly asks +- Never force push to `main`/`master` — warn if requested +- Never push unless explicitly asked +- Never use interactive git (`-i` flags) +- **Amend only if all apply:** user requested amend (or hook auto-modified files after your successful commit); HEAD was created by you this session; commit not pushed +- If commit fails or hook rejects: fix and create a **new** commit — do not amend + +## Examples + +**Bug fix + regression (one commit):** +- Scope: `path/to/module.py`, `path/to/test_module.py` +- Message: `riskmanager: fix quantity math in pnl monitor` + +**Code + IDE config (two commits):** +1. `datamanager: add forward timeseries caching` +2. `config: update vscode python settings` From 89b728406935e14b29786f29deb7ba90bd14a170 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:24:24 -0400 Subject: [PATCH 76/81] EventDriven: fix backtest eq_strategy property and frozen attribution defaults Co-authored-by: Cursor --- EventDriven/attribution.py | 10 +++-- EventDriven/backtest.py | 41 +++++++++---------- .../position/live_cogs/save_utils.py | 9 ++-- 3 files changed, 31 insertions(+), 29 deletions(-) diff --git a/EventDriven/attribution.py b/EventDriven/attribution.py index 48e24b8..efabb1f 100644 --- a/EventDriven/attribution.py +++ b/EventDriven/attribution.py @@ -90,13 +90,15 @@ def __post_init__(self): raise ValueError("daily_qty, quantity_change, and exec_price must have the same index") if self.commission is None: - self.commission = pd.Series(0, index=self.daily_qty.index) + object.__setattr__(self, "commission", pd.Series(0, index=self.daily_qty.index)) if self.slippage is None: - self.slippage = pd.Series(0, index=self.daily_qty.index) + object.__setattr__(self, "slippage", pd.Series(0, index=self.daily_qty.index)) if self.trade_entry is None: - self.trade_entry = self.daily_qty.index.min() + object.__setattr__(self, "trade_entry", self.daily_qty.index.min()) if self.trade_exit is None: - self.trade_exit = self.daily_qty.index.max() + object.__setattr__(self, "trade_exit", self.daily_qty.index.max()) + + super().__post_init__() def __repr__(self) -> str: return f"QuantityTimeSeries(tick={self.tick}, trade_id={self.trade_id})" diff --git a/EventDriven/backtest.py b/EventDriven/backtest.py index 61605d3..6993ba1 100644 --- a/EventDriven/backtest.py +++ b/EventDriven/backtest.py @@ -221,24 +221,6 @@ def __init__( raise ValueError("Trades DataFrame cannot be None or empty when not using an equity strategy.") self.__init__with_trades(trades, initial_capital, symbol_list, end_date=end_date) - @property - def eq_strategy(self) -> Optional[MultiAssetStrategy]: - """Return equity strategy and keep dependent components synchronized.""" - return self._eq_strategy - - @eq_strategy.setter - def eq_strategy(self, strategy: Optional[MultiAssetStrategy]) -> None: - """Set equity strategy and propagate it to risk manager and portfolio if initialized.""" - self._eq_strategy = strategy - self.is_eq_strategy = strategy is not None - - if hasattr(self, "risk_manager") and self.risk_manager is not None: - self.risk_manager.eq_strategy = strategy - - if hasattr(self, "portfolio") and self.portfolio is not None: - self.portfolio.eq_strategy = strategy - self.portfolio.using_eq_strategy = strategy is not None - def __init__with_equity_strategy( self, eq_strategy: MultiAssetStrategy, @@ -341,6 +323,24 @@ def __init__with_trades( def logger(self): return LOGGER + @property + def eq_strategy(self) -> Optional[MultiAssetStrategy]: + """Return equity strategy and keep dependent components synchronized.""" + return self._eq_strategy + + @eq_strategy.setter + def eq_strategy(self, strategy: Optional[MultiAssetStrategy]) -> None: + """Set equity strategy and propagate it to risk manager and portfolio if initialized.""" + self._eq_strategy = strategy + self.is_eq_strategy = strategy is not None + + if hasattr(self, "risk_manager") and self.risk_manager is not None: + self.risk_manager.eq_strategy = strategy + + if hasattr(self, "portfolio") and self.portfolio is not None: + self.portfolio.eq_strategy = strategy + self.portfolio.using_eq_strategy = strategy is not None + def __construct_data(self, trades: pd.DataFrame, initial_capital: int, symbol_list: list) -> None: ## Date range setup ## Move back a day if not business day @@ -623,10 +623,7 @@ def get_all_positions(self) -> pd.DataFrame: """ return timeseries of portfolio positions """ - pos_arr = [] - pos_df = pd.DataFrame(pos_arr) - pos_df.set_index("datetime", inplace=True) - return pos_df + return self.portfolio.get_all_positions() def store_event(self, event: Event): """ diff --git a/EventDriven/riskmanager/position/live_cogs/save_utils.py b/EventDriven/riskmanager/position/live_cogs/save_utils.py index 4b15daf..68491a2 100644 --- a/EventDriven/riskmanager/position/live_cogs/save_utils.py +++ b/EventDriven/riskmanager/position/live_cogs/save_utils.py @@ -422,7 +422,7 @@ def save_limits_from_backtester(bkt: OptionSignalBacktest, date: datetime = None ## LOADERS -def get_position_limit(trade_id: str, strategy_name: str, signal_id: str, risk_measure: str) -> tuple: +def get_position_limit(trade_id: str, strategy_name: str, signal_id: str, risk_measure: str) -> tuple[datetime, float]: df = get_limits_data() assert risk_measure in MEASURES_SET, f"risk_measure must be one of {MEASURES_SET}" row = df[ @@ -435,5 +435,8 @@ def get_position_limit(trade_id: str, strategy_name: str, signal_id: str, risk_m logger.error( f"No limit found for trade_id={trade_id}, strategy_name={strategy_name}, signal_id={signal_id}, risk_measure={risk_measure}" ) - return None - return row["date"].values[0], float(row["value"].values[0]) + return None, None + return ( + row["date"].values[0], + float(row["value"].values[0]) + ) From 96395c62b602357189815caae83beb856793c4d6 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:24:24 -0400 Subject: [PATCH 77/81] datamanager: harden runtime logging and CRR vol fallback Co-authored-by: Cursor --- trade/datamanager/market_data.py | 3 ++- trade/datamanager/vars.py | 14 +++++++++++--- trade/optionlib/vol/implied_vol.py | 3 +-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/trade/datamanager/market_data.py b/trade/datamanager/market_data.py index d07fc67..9e4ac57 100644 --- a/trade/datamanager/market_data.py +++ b/trade/datamanager/market_data.py @@ -213,9 +213,10 @@ _simple_extract_from_cache, # noqa ) from trade import SIGNALS_TO_RUN +from trade.datamanager.utils.logging import get_logging_level -logger = setup_logger("trade.datamanager.market_data", stream_log_level="INFO") +logger = setup_logger("trade.datamanager.market_data", stream_log_level=get_logging_level()) ## TODO: This var is from optionlib. Once ready, import from there. ## TODO: Implement interval handling to have multiple intervals diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index 7b93dad..4931068 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -87,11 +87,19 @@ def send_log_to_disk() -> None: if not _LOG_TO_DISK_BUCKET: logger.info("No logs to write to disk.") return + log_path = DM_GEN_PATH / "dm_runtime_logs.csv" + DM_GEN_PATH.mkdir(parents=True, exist_ok=True) 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) + + if log_path.exists() and log_path.stat().st_size > 0: + try: + df_existing = pd.read_csv(log_path) + if not df_existing.empty: + df = pd.concat([df_existing, df], ignore_index=True) + except pd.errors.EmptyDataError: + logger.warning("Existing runtime log file %s is empty; overwriting.", log_path) + 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() diff --git a/trade/optionlib/vol/implied_vol.py b/trade/optionlib/vol/implied_vol.py index 4110a03..1cb818c 100644 --- a/trade/optionlib/vol/implied_vol.py +++ b/trade/optionlib/vol/implied_vol.py @@ -579,8 +579,7 @@ def binomial_objective_function(sigma: float) -> float: bounds=(0.001, 5.0), # Reasonable bounds for volatility method="bounded", ) - - return result.x if result.success else None + return result.x if result.success else np.nan def estimate_crr_implied_volatility( From fa3e099ec24cf637a3e5778b4b403bd27de6d21e Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:22:28 -0400 Subject: [PATCH 78/81] datamanager: add quote helpers and drop module_test deps Co-authored-by: Cursor --- trade/assets/calculate/xmultiply_attr_v2.py | 14 ++++----- trade/datamanager/utils/helpers.py | 35 +++++++++++++++++++++ trade/datamanager/vars.py | 1 - 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 trade/datamanager/utils/helpers.py diff --git a/trade/assets/calculate/xmultiply_attr_v2.py b/trade/assets/calculate/xmultiply_attr_v2.py index 05e5471..df36158 100644 --- a/trade/assets/calculate/xmultiply_attr_v2.py +++ b/trade/assets/calculate/xmultiply_attr_v2.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Optional, List +from typing import Any, Optional, List from pandas.tseries.offsets import BDay import pandas as pd from pydantic import validate_call, ConfigDict # noqa @@ -15,12 +15,9 @@ ) from trade.helpers.Logging import setup_logger from trade.helpers.decorators import log_time # noqa -from module_test.raw_code.DataManagers.DataManagers import OptionDataManager, set_skip_mysql_query from trade.datamanager.timeseries import TimeseriesDataManager # noqa from trade.optionlib.config.defaults import OPTION_TIMESERIES_START_DATE -##TODO: Take this out once DataManagers has been optimized -set_skip_mysql_query(True) logger = setup_logger("trade.assets.calculate.xmultiply_attr") @@ -172,22 +169,23 @@ def load_option_pnl_data( yesterday: datetime, today: datetime, *, - dm: OptionDataManager = None, opttick: str = None, payload: Optional[OptionPnlPayload] = None, + **kwargs: Any, ) -> OptionPnlPayload: """ Load option data for a given option data manager between yesterday and today. Args: - dm (OptionDataManager): The option data manager to load data from. + yesterday (datetime): The start date for the data. + today (datetime): The end date for the data. opttick (str): The option ticker symbol. payload (Optional[OptionPnlPayload]): Optional payload containing any subset of expected fields. Provided fields are validated; missing fields are loaded. - yesterday (datetime): The start date for the data. - today (datetime): The end date for the data. + **kwargs: Any additional keyword arguments. Returns: OptionPnlPayload: The loaded option data. """ + dm = kwargs.get("dm", None) if dm is None and opttick is None and payload is None: raise ValueError("One of 'opttick' or 'payload' must be provided.") if dm is not None: diff --git a/trade/datamanager/utils/helpers.py b/trade/datamanager/utils/helpers.py new file mode 100644 index 0000000..03d9d4e --- /dev/null +++ b/trade/datamanager/utils/helpers.py @@ -0,0 +1,35 @@ +from trade.datamanager.config import OptionDataConfig, OptionSpotEndpointSource +from typing import Optional +from trade.helpers.Logging import setup_logger +logger = setup_logger("trade.datamanager.utils.helpers", stream_log_level="WARNING") +__PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE: Optional[OptionSpotEndpointSource] = None + +def enable_quotes() -> None: + """Enable quotes for the data manager.""" + global __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE = OptionDataConfig().option_spot_endpoint_source + OptionDataConfig().option_spot_endpoint_source = OptionSpotEndpointSource.QUOTE + +def disable_quotes() -> None: + """Disable quotes for the data manager.""" + global __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + if __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE is not None: + OptionDataConfig().option_spot_endpoint_source = __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + else: + logger.warning("Quotes are not enabled. No previous source to restore.") + +def enable_eod() -> None: + """Enable EOD for the data manager.""" + global __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + if __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE is not None: + OptionDataConfig().option_spot_endpoint_source = __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + else: + logger.warning("EOD is not enabled. No previous source to restore.") + +def disable_eod() -> None: + """Disable EOD for the data manager.""" + global __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + if __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE is not None: + OptionDataConfig().option_spot_endpoint_source = __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE + else: + logger.warning("EOD is not enabled. No previous source to restore.") \ No newline at end of file diff --git a/trade/datamanager/vars.py b/trade/datamanager/vars.py index 4931068..3169a87 100644 --- a/trade/datamanager/vars.py +++ b/trade/datamanager/vars.py @@ -23,7 +23,6 @@ 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 _parse_bool_env(var_name: str, default: bool = True) -> bool: raw = os.getenv(var_name) if raw is None: From 89784e51d3b12221936a62210e5b21a0c9cc0884 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:36:16 -0400 Subject: [PATCH 79/81] datamanager: standardize logger setup on get_logging_level Co-authored-by: Cursor --- trade/datamanager/utils/helpers.py | 9 ++++- trade/datamanager/utils/logging.py | 65 ++++++++++++++++++++---------- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/trade/datamanager/utils/helpers.py b/trade/datamanager/utils/helpers.py index 03d9d4e..f9a9b0b 100644 --- a/trade/datamanager/utils/helpers.py +++ b/trade/datamanager/utils/helpers.py @@ -1,7 +1,12 @@ -from trade.datamanager.config import OptionDataConfig, OptionSpotEndpointSource +"""Runtime helpers for toggling datamanager option spot endpoint sources.""" + from typing import Optional + +from trade.datamanager.config import OptionDataConfig, OptionSpotEndpointSource +from trade.datamanager.utils.logging import get_logging_level from trade.helpers.Logging import setup_logger -logger = setup_logger("trade.datamanager.utils.helpers", stream_log_level="WARNING") + +logger = setup_logger("trade.datamanager.utils.helpers", stream_log_level=get_logging_level()) __PREVIOUS_OPTION_SPOT_ENDPOINT_SOURCE: Optional[OptionSpotEndpointSource] = None def enable_quotes() -> None: diff --git a/trade/datamanager/utils/logging.py b/trade/datamanager/utils/logging.py index 2a99f80..9272b6f 100644 --- a/trade/datamanager/utils/logging.py +++ b/trade/datamanager/utils/logging.py @@ -1,9 +1,28 @@ +"""Central logging configuration for the datamanager package. + +Defines the shared default log level, logger discovery helpers, and runtime +level mutation for all ``trade.datamanager`` loggers. + +Core Functions: + get_logging_level: Return the current datamanager stream log level. + set_logging_level: Update the shared datamanager stream log level. + change_logging_in_all_datamanager_loggers: Apply a level to all datamanager loggers. +""" + import logging +from typing import List -from git import List from trade.helpers.Logging import setup_logger, find_loggers_by_pattern, change_logger_stream_level + LOGGING_LEVEL = "WARNING" -logger = setup_logger("trade.datamanager.utils", stream_log_level=LOGGING_LEVEL) + + +def get_logging_level() -> str: + """Return the current datamanager stream log level.""" + return LOGGING_LEVEL + + +logger = setup_logger("trade.datamanager.utils", stream_log_level=get_logging_level()) FACTOR_DMS = { "trade.datamanager.spot", @@ -23,58 +42,62 @@ UTILS_LOGGER_NAME = "trade.datamanager.utils" -def set_logging_level(level: str): + +def set_logging_level(level: str) -> None: + """Update the shared datamanager stream log level.""" 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'""" + """Retrieve all loggers under 'trade.datamanager'.""" return find_loggers_by_pattern("trade.datamanager") -def change_logging_in_all_datamanager_loggers(level: str = None): + +def change_logging_in_all_datamanager_loggers(level: str = None) -> None: """Change logging level for all loggers under 'trade.datamanager'.""" if level is None: - level = LOGGING_LEVEL + level = get_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): + +def change_datamanager_utils_logging_level(level: str = None) -> 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)) + level = get_logging_level() + utils_logger = logging.getLogger("trade.datamanager.utils") + change_logger_stream_level(utils_logger, getattr(logging, level.upper(), logging.INFO)) -def change_datamanager_factor_loggers_level(level: str = None): + +def change_datamanager_factor_loggers_level(level: str = None) -> None: """Change logging level for all factor loggers under 'trade.datamanager'.""" if level is None: - level = LOGGING_LEVEL + level = get_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'""" +def change_all_optionlib_loggers_level(level: str = None) -> None: + """Change logging level for all loggers under 'trade.optionlib'.""" if level is None: - level = LOGGING_LEVEL + level = get_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 change_all_logger(level: str = None): + +def change_all_logger(level: str = None) -> None: """Change logging level for all loggers under 'trade'.""" if level is None: - level = LOGGING_LEVEL + level = get_logging_level() change_all_optionlib_loggers_level(level) change_logging_in_all_datamanager_loggers(level) -def register_to_factor_list(name:str): +def register_to_factor_list(name: str) -> None: + """Register a datamanager module name for factor logger level changes.""" FACTOR_DMS.add(name) - From 630383de9bdd702b3481532736d16bd829eb5e50 Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:36:16 -0400 Subject: [PATCH 80/81] riskmanager: align chain cache with _should_save_today rules Co-authored-by: Cursor --- EventDriven/riskmanager/utils.py | 37 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/EventDriven/riskmanager/utils.py b/EventDriven/riskmanager/utils.py index cd1f685..2e65334 100644 --- a/EventDriven/riskmanager/utils.py +++ b/EventDriven/riskmanager/utils.py @@ -431,12 +431,32 @@ def populate_cache_with_chain( print_url=True, add_greeks=False, ): + """Fetch option chain data with datamanager-aligned today caching rules. + + Historical chains are cached and reused. Today's chain reloads on each call until + ``_should_save_today`` indicates EOD data is stable (same rule as datamanager cache). + + Args: + tick: Underlying ticker symbol. + date: Chain valuation date. + chain_spot: Optional spot price for moneyness filtering. + print_url: Unused; retained for call-site compatibility. + add_greeks: Unused; retained for call-site compatibility. + + Returns: + Chain DataFrame with opttick, chain_id, dte, and placeholder greek columns. """ - Populate the cache with chain data. - """ - key = (tick, pd.to_datetime(date, format="%Y-%m-%d").strftime("%Y-%m-%d")) - if key in get_persistent_cache(): - chain_clipped = get_persistent_cache()[key] + from datetime import date as date_cls + from trade.datamanager.utils.date import _should_save_today + + date_str = pd.to_datetime(date, format="%Y-%m-%d").strftime("%Y-%m-%d") + key = (tick, date_str) + chain_date = pd.to_datetime(date_str).date() + should_use_cache = chain_date < date_cls.today() or _should_save_today(max_date=chain_date) + cache = get_persistent_cache() + + if should_use_cache and key in cache: + chain_clipped = cache[key].copy() chain_clipped.columns = chain_clipped.columns.str.capitalize() chain_clipped.rename(columns={"Opttick": "opttick"}, inplace=True) drops = ["Datetime", "Dte", "Moneyness"] @@ -445,6 +465,9 @@ def populate_cache_with_chain( chain_clipped.drop(columns=col, inplace=True) else: + if not should_use_cache and key in cache: + del cache[key] + chain = retrieve_chain_bulk(symbol=tick, start_date=date, end_date=date, end_time="16:00", option_type="C", print_url=False, exp = None) logger.info(f"Retrieved chain for {tick} on {date}") @@ -471,8 +494,8 @@ def populate_cache_with_chain( if PATCH_TICKERS: chain_clipped["Root"] = chain_clipped["Root"].apply(swap_ticker) - if (pd.to_datetime(date)).date() != datetime.now().date(): - _PERSISTENT_CACHE[key] = chain_clipped ## Cache the chain data to avoid redundant API calls in the future + if should_use_cache: + cache[key] = chain_clipped.copy() chain_clipped.columns = chain_clipped.columns.str.capitalize() ## Create ID From 04239ae97c48d5717216fa60ba0c027685db1dcc Mon Sep 17 00:00:00 2001 From: Chidifinance <142176703+Chidifinance@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:36:17 -0400 Subject: [PATCH 81/81] docs: update running todo checklist progress Co-authored-by: Cursor --- running_todo.todo | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/running_todo.todo b/running_todo.todo index 8c74c02..56a0e0b 100644 --- a/running_todo.todo +++ b/running_todo.todo @@ -18,22 +18,23 @@ Running Todo: ☐ Aggregator should take trades & equity data only ✔ EVB should use StrategyBase class (Optionally, dataframe, but probably should discourage it) @done(26-02-17 07:17) ☐ Switch to TimeseriesDataManager for pnl attribution - ☐ Discontinue all use of module_test.raw_code.DataManagers + ✔ Discontinue all use of module_test.raw_code.DataManagers @done(26-06-07 21:25) 1. Option.py 2. xmultiply_attr.py + ☐ Discontinue xmultiply_attr.py ✔ Add refresh information to MarketTimeseries to avoid constantly refreshing data when not needed. @done(26-02-17 07:19) ☐ 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 @done(26-02-17 07:19) ✔ Ensure make position id & signal id comes from one place (Signal id from strategy base, position id from create order) @done(26-02-17 07:20) - ☐ Update get_risk_free_rate_helper to use RatesDataManager + ✔ Update get_risk_free_rate_helper to use RatesDataManager @done(26-06-07 21:28) ☐ Look into alternatives for backfilling dividends with 0.0 in get_timeseries ☐ Move strategy files to db. ✔ Set evb default market_model = bsm, div = continuous for speed. Disable logging. @done(26-02-17 07:20) ☐ Replace populate_chain_cache with ChainDataManager which caches none-today data and reloads today data on each call. - ☐ Remind zino about table diffing + ✔ Remind zino about table diffing @done(26-06-07 21:28) ✔ Update info in config dictionary in order picker to reflect new changes (eg min_total_price) @done(26-02-17 07:21) - ☐ streamline all evb stream logging level as done in trade.datamanager + ✔ streamline all evb stream logging level as done in trade.datamanager @done(26-06-07 21:28) ☐ Normalize everywhere to use the following for position: - fyi: This includes: events, signals, positions, etc. Anywhere we need to represent a position, we should use this format to avoid confusion and ensure consistency across the codebase. - Side int: 1 for long, -1 for short @@ -41,6 +42,8 @@ Running Todo: - Position effect: "open" or "close" - It should all come from eventdriven.types.position ☐ Clean up old todos in the codebase and move any relevant ones to this todo list. + ☐ Revisit Binomial Vol Model estimation logic. + ☐ Run an analysis on greek estimation accuracy. Todo before going back to backtesting: @@ -48,8 +51,8 @@ Todo before going back to backtesting: ✔ Allow preset orders in Evb order getter @done(26-02-09 21:14) ✔ Clean up evb order getter + preset orders (speed up essentially) @done(26-02-11 21:44) ✔ Push config change to db (changed min_total_price to 0.5 from 0.95) @done(26-02-13 09:42) - ☐ Move emefiele reports to the reports channel - ☐ Populate get_strategy_instance with has_position (Optional) + ✔ Move emefiele reports to the reports channel @done(26-06-07 21:24) + ✔ Populate get_strategy_instance with has_position (Optional) @done(26-06-07 21:24) ✔ Cache checker should use date range info, with missing dates info saved @done(26-02-14 18:47) ✔ All ts loaders should return the data @done(26-02-15 21:28) ✔ Maybe cache timeseries object and use to return date range & at index to avoid doing multiple cache lookups @done(26-02-15 21:28)