option-chain-analytics 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. option_chain_analytics/__init__.py +56 -0
  2. option_chain_analytics/chain_loader_from_ts.py +239 -0
  3. option_chain_analytics/chain_ts.py +248 -0
  4. option_chain_analytics/config.py +83 -0
  5. option_chain_analytics/data/__init__.py +27 -0
  6. option_chain_analytics/data/bloomberg.py +42 -0
  7. option_chain_analytics/data/ccxt_api.py +293 -0
  8. option_chain_analytics/data/chain_loader_from_ts.py +15 -0
  9. option_chain_analytics/data/chain_ts.py +5 -0
  10. option_chain_analytics/data/config.py +19 -0
  11. option_chain_analytics/data/deribit.py +252 -0
  12. option_chain_analytics/data/simulated.py +182 -0
  13. option_chain_analytics/data/yahoo.py +297 -0
  14. option_chain_analytics/fitters/__init__.py +1 -0
  15. option_chain_analytics/fitters/chain_fit_report.py +130 -0
  16. option_chain_analytics/fitters/qp_price_fitter.py +612 -0
  17. option_chain_analytics/fitters/utils.py +211 -0
  18. option_chain_analytics/fitters/vol_beta_fitter.py +465 -0
  19. option_chain_analytics/local_path.py +45 -0
  20. option_chain_analytics/option_chain.py +988 -0
  21. option_chain_analytics/ts_loaders.py +369 -0
  22. option_chain_analytics/utils/__init__.py +0 -0
  23. option_chain_analytics/utils/implied_forwards.py +136 -0
  24. option_chain_analytics/utils/numerics.py +115 -0
  25. option_chain_analytics/utils/portfolio_payoff.py +94 -0
  26. option_chain_analytics/utils/roll_maturities.py +246 -0
  27. option_chain_analytics/visuals/__init__.py +3 -0
  28. option_chain_analytics/visuals/chain_report.py +80 -0
  29. option_chain_analytics/visuals/plots.py +264 -0
  30. option_chain_analytics/visuals/slices.py +185 -0
  31. option_chain_analytics-2.0.0.dist-info/METADATA +204 -0
  32. option_chain_analytics-2.0.0.dist-info/RECORD +35 -0
  33. option_chain_analytics-2.0.0.dist-info/WHEEL +5 -0
  34. option_chain_analytics-2.0.0.dist-info/licenses/LICENSE +21 -0
  35. option_chain_analytics-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,56 @@
1
+
2
+ """Point-in-time option-chain containers and reconstruction analytics."""
3
+
4
+ from option_chain_analytics.chain_loader_from_ts import (
5
+ create_chain_from_from_options_dfs,
6
+ create_chain_timeseries,
7
+ generate_atm_vols_skew,
8
+ generate_vol_delta_ts,
9
+ )
10
+ from option_chain_analytics.chain_ts import FuturesChainTs, OptionsDataDFs
11
+ from option_chain_analytics.config import (
12
+ NearestStrikeOnGrid,
13
+ StrikeSelection,
14
+ compute_time_to_maturity,
15
+ mat_to_timestamp,
16
+ )
17
+ from option_chain_analytics.data.simulated import generate_simulated_options_data
18
+ from option_chain_analytics.option_chain import (
19
+ ExpirySlice,
20
+ SliceColumn,
21
+ SlicesChain,
22
+ get_contract_execution_price,
23
+ )
24
+ from option_chain_analytics.ts_loaders import DataSource, ts_data_loader_wrapper
25
+ from option_chain_analytics.utils.portfolio_payoff import (
26
+ compute_option_portfolio_dt,
27
+ compute_portfolio_payoff,
28
+ )
29
+ from option_chain_analytics.visuals.chain_report import run_chain_report
30
+ from option_chain_analytics.visuals.slices import plot_slice_open_interest, plot_slice_vols
31
+
32
+ __all__ = [
33
+ 'DataSource',
34
+ 'ExpirySlice',
35
+ 'FuturesChainTs',
36
+ 'NearestStrikeOnGrid',
37
+ 'OptionsDataDFs',
38
+ 'SliceColumn',
39
+ 'SlicesChain',
40
+ 'StrikeSelection',
41
+ 'compute_option_portfolio_dt',
42
+ 'compute_portfolio_payoff',
43
+ 'compute_time_to_maturity',
44
+ 'create_chain_from_from_options_dfs',
45
+ 'create_chain_timeseries',
46
+ 'generate_atm_vols_skew',
47
+ 'generate_simulated_options_data',
48
+ 'generate_vol_delta_ts',
49
+ 'get_contract_execution_price',
50
+ 'mat_to_timestamp',
51
+ 'plot_slice_open_interest',
52
+ 'plot_slice_vols',
53
+ 'run_chain_report',
54
+ 'ts_data_loader_wrapper',
55
+ ]
56
+
@@ -0,0 +1,239 @@
1
+ """
2
+ create chain data object with options using time series options data in OptionsDataDFs
3
+ """
4
+ # package
5
+ from enum import Enum
6
+ from typing import Dict, List, Optional, Tuple
7
+
8
+ import matplotlib.pyplot as plt
9
+ import numpy as np
10
+ import pandas as pd
11
+ import qis
12
+ from qis import TimePeriod, timer
13
+ from qis.utils.np_ops import np_nonan_weighted_avg
14
+
15
+ from option_chain_analytics.chain_ts import OptionsDataDFs
16
+
17
+ # analytics
18
+ from option_chain_analytics.option_chain import ExpirySlice, SliceColumn, SlicesChain, UnderlyingColumn
19
+
20
+
21
+ # @timer
22
+ def create_chain_from_from_options_dfs(options_data_dfs: OptionsDataDFs,
23
+ value_time: pd.Timestamp,
24
+ ) -> Optional[SlicesChain]:
25
+ """
26
+ create chain of slices for options_data_dfs
27
+ """
28
+ options_df = options_data_dfs.get_time_slice(timestamp=value_time)
29
+
30
+ if not options_df.empty:
31
+ # options_df[SliceColumn.CONTRACT.value] = options_df.index.to_list()
32
+ mat_slice = options_df.groupby(SliceColumn.MATURITY_ID.value)
33
+ expiry_slices, undelying_datas = {}, {}
34
+ for mat, df in mat_slice:
35
+ if np.any(df[SliceColumn.OPEN_INTEREST].isna() == False):
36
+ # forward is contract weighted avs
37
+ forward = np_nonan_weighted_avg(a=df[SliceColumn.FORWARD_PRICE.value],
38
+ weights=df[SliceColumn.OPEN_INTEREST])
39
+ else:
40
+ # forward is contract weighted avs
41
+ forward = np.nanmean(df[SliceColumn.FORWARD_PRICE.value])
42
+ if not np.isnan(forward):
43
+ undelying_data = {UnderlyingColumn.EXPIRY_ID: str(mat),
44
+ UnderlyingColumn.VALUE_TIME: value_time,
45
+ UnderlyingColumn.EXPIRY: df[SliceColumn.EXPIRY].iloc[0],
46
+ UnderlyingColumn.SPOT_PRICE: forward, # to do
47
+ UnderlyingColumn.UNDERLYING_INDEX: str(mat), # to do
48
+ UnderlyingColumn.FORWARD_PRICE: forward, # to do
49
+ UnderlyingColumn.IR_RATE: 0.0,
50
+ UnderlyingColumn.TTM: df[SliceColumn.TTM].iloc[0]}
51
+ undelying_data = pd.Series(undelying_data)
52
+ expiry_slices[str(mat)] = ExpirySlice(options_df=df, undelying_data=undelying_data)
53
+ undelying_datas[str(mat)] = undelying_data
54
+ undelying_df = pd.DataFrame.from_dict(undelying_datas, orient='index')
55
+ chain = SlicesChain(options_df=options_df,#.set_index(SliceColumn.CONTRACT),
56
+ undelying_df=undelying_df,
57
+ expiry_slices=expiry_slices,
58
+ value_time=value_time)
59
+ else:
60
+ chain = None
61
+ return chain
62
+
63
+
64
+ @timer
65
+ def create_chain_timeseries(options_data_dfs: OptionsDataDFs,
66
+ dates_schedule: pd.DatetimeIndex = None,
67
+ time_period: TimePeriod = None,
68
+ freq: str = 'W-FRI',
69
+ hour_offset: int = 8
70
+ ) -> Dict[pd.Timestamp, SlicesChain]:
71
+ """
72
+ create dictionary of timestamp and corresponding slices
73
+ """
74
+ if dates_schedule is None:
75
+ if time_period is None:
76
+ raise ValueError(f"time_period={time_period} must be non Nons")
77
+ dates_schedule = qis.generate_dates_schedule(time_period=time_period,
78
+ freq=freq,
79
+ hour_offset=hour_offset)
80
+
81
+ chain_data = {}
82
+ for timestamp in dates_schedule:
83
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=timestamp)
84
+ if chain is not None:
85
+ chain_data[timestamp] = chain
86
+ return chain_data
87
+
88
+
89
+ @timer
90
+ def generate_atm_vols_skew(options_data_dfs: OptionsDataDFs,
91
+ time_period: TimePeriod = None,
92
+ freq: str = 'D',
93
+ hour_offset: int = 8,
94
+ days_before_roll: int = 7
95
+ ) -> Tuple[pd.Series, pd.Series]:
96
+
97
+ """
98
+ fetch time series of atm vols and skew from options_data_dfs
99
+ """
100
+ if time_period is None:
101
+ time_period = options_data_dfs.get_start_end_date()
102
+
103
+ chain_data = create_chain_timeseries(options_data_dfs=options_data_dfs,
104
+ time_period=time_period,
105
+ freq=freq,
106
+ hour_offset=hour_offset)
107
+ vols = {}
108
+ skews = {}
109
+ for date, chain in chain_data.items():
110
+ next_date = date + pd.DateOffset(days=days_before_roll)
111
+ slice_id = chain.get_next_slice_after_date(mat_date=next_date)
112
+ vols[date] = chain.get_atm_vol(slice_id=slice_id)
113
+ skews[date] = chain.get_skew(slice_id=slice_id)
114
+ vols = pd.Series(vols, name='atm_vol')
115
+ skews = pd.Series(skews, name='skew')
116
+ return vols, skews
117
+
118
+
119
+ @timer
120
+ def generate_vol_delta_ts(options_data_dfs: OptionsDataDFs,
121
+ days_map: Dict[str, int] = None,
122
+ deltas: List[float] = (-0.10, -0.25, 0.50, 0.25, 0.10),
123
+ freq: str = 'D',
124
+ hour_offset: Optional[int] = 8,
125
+ time_period: TimePeriod = None
126
+ ) -> Tuple[pd.DataFrame, ...]:
127
+ """
128
+ return dataframes of vols with columns = deltas and index = dates
129
+ """
130
+ if time_period is None:
131
+ time_period = options_data_dfs.get_start_end_date()
132
+
133
+ chain_data = create_chain_timeseries(options_data_dfs=options_data_dfs,
134
+ time_period=time_period,
135
+ freq=freq,
136
+ hour_offset=hour_offset)
137
+ if days_map is None:
138
+ days_map = {'1d': 1, '2d': 2, '1w': 7, '2w': 10, '1m': 28, '2m': 56, '3m': 84, '2Q': 168}
139
+
140
+ is_add_tenor_to_delta_name = False if len(days_map.keys()) == 1 else True
141
+ vols, strikes, options, underlying_prices = {}, {}, {}, {}
142
+ for date, chain in chain_data.items():
143
+ delta_vol_matrix = chain.generate_delta_vol_matrix(value_time=date, days_map=days_map, deltas=deltas)
144
+ if delta_vol_matrix is not None:
145
+ vols_, strikes_, options_, index_prices_ = delta_vol_matrix.get_melted_matrix(is_add_tenor_to_delta_name=is_add_tenor_to_delta_name)
146
+ vols[date] = vols_['vols'].rename(date)
147
+ strikes[date] = strikes_['strikes'].rename(date)
148
+ options[date] = options_['options'].rename(date)
149
+ underlying_prices[date] = index_prices_['underlying_prices_matrix'].rename(date)
150
+ vols = pd.DataFrame.from_dict(vols, orient='index')
151
+ strikes = pd.DataFrame.from_dict(strikes, orient='index')
152
+ options = pd.DataFrame.from_dict(options, orient='index')
153
+ underlying_prices = pd.DataFrame.from_dict(underlying_prices, orient='index')
154
+ return vols, strikes, options, underlying_prices
155
+
156
+
157
+ class UnitTests(Enum):
158
+ CREATE_CHAIN_AT_TS = 1
159
+ CREATE_TS_CHAIN_DATA = 2
160
+ CREATE_WEEKLY_ROLLS = 3
161
+ GENERATE_VOL_DELTA_TS = 4
162
+
163
+
164
+ def run_unit_test(unit_test: UnitTests):
165
+
166
+ from option_chain_analytics.ts_loaders import DataSource, ts_data_loader_wrapper
167
+
168
+ ticker = 'ETH'
169
+ options_data_dfs = OptionsDataDFs(**ts_data_loader_wrapper(ticker=ticker, data_source=DataSource.TARDIS_LOCAL))
170
+
171
+ if unit_test == UnitTests.CREATE_CHAIN_AT_TS:
172
+ date = pd.Timestamp('2022-09-19 08:00:00+00:00')
173
+ chain = create_chain_from_from_options_dfs(options_data_dfs=options_data_dfs, value_time=date)
174
+ chain.save_joint_slices(file_name=f"{ticker}_{date.strftime('%Y%m%d_%H_%M_%S')}")
175
+
176
+ next_date = date+pd.DateOffset(days=30)
177
+ slice_id = chain.get_next_slice_after_date(mat_date=next_date)
178
+ print(f"{date}: {slice_id}")
179
+ print(f"{chain.get_atm_put_id(slice_id=slice_id)}, {chain.get_atm_call_id(slice_id=slice_id)}")
180
+ print(f"{chain.get_put_delta_option_id(slice_id=slice_id, delta=-0.25)}")
181
+ print(f"{chain.get_call_delta_option_id(slice_id=slice_id, delta=0.25)}")
182
+
183
+ # days_map = {'1d': 1, '2d': 2, '1w': 7, '2w': 14, '1m': 30, '2m': 60, '3m': 90, '2Q': 120, '3Q': 180}
184
+ # days_map = {'1d': 1, '2d': 2, '1w': 7, '2w': 14, '1m': 30, '2m': 60, '3m': 90, '2Q': 120}
185
+ days_map = {'2d': 2, '1w': 7, '2w': 14, '1m': 30, '2m': 60, '3m': 90}
186
+ delta_vol_matrix = chain.generate_delta_vol_matrix(value_time=date, days_map=days_map)
187
+ delta_vol_matrix.print()
188
+
189
+ vols, strikes, options, index_prices = delta_vol_matrix.get_melted_matrix()
190
+ print(vols)
191
+ print(strikes)
192
+ delta_vol_matrix.plot_vol_in_strike()
193
+
194
+ elif unit_test == UnitTests.CREATE_TS_CHAIN_DATA:
195
+ chain_data = create_chain_timeseries(options_data_dfs=options_data_dfs,
196
+ time_period=options_data_dfs.get_start_end_date(),
197
+ freq='W-FRI',
198
+ hour_offset=8)
199
+ for key, chain in chain_data.items():
200
+ print(f"{key}, {chain.expiry_slices.keys()}")
201
+
202
+ elif unit_test == UnitTests.CREATE_WEEKLY_ROLLS:
203
+ weekly_fridays_rolls = qis.generate_dates_schedule(TimePeriod(pd.Timestamp('2022-05-06 00:00:00+00:00'),
204
+ qis.get_current_time_with_tz(tz='UTC', days_offset=7)),
205
+ freq='W-FRI',
206
+ hour_offset=8)
207
+ weekly_fridays_rolls = weekly_fridays_rolls[:-2]
208
+ print(weekly_fridays_rolls)
209
+
210
+ chain_data = create_chain_timeseries(options_data_dfs=options_data_dfs,
211
+ dates_schedule=weekly_fridays_rolls[:-1])
212
+
213
+ for date, next_date in zip(weekly_fridays_rolls[:-1], weekly_fridays_rolls[1:]):
214
+ print(f"{date}: {chain_data[date].get_next_slice_after_date(mat_date=next_date)}, "
215
+ f"{chain_data[date].get_atm_put_id(mat_date=next_date)}")
216
+
217
+ elif unit_test == UnitTests.GENERATE_VOL_DELTA_TS:
218
+ time_period = TimePeriod('2023-01-01 00:00:00+00:00', '2023-07-14 00:00:00+00:00', tz='UTC')
219
+ vols, strikes, options, index_prices = generate_vol_delta_ts(options_data_dfs=options_data_dfs,
220
+ days_map={'1w': 7, '1m': 30},
221
+ deltas=[-0.10, -0.25, 0.50, 0.25, 0.10],
222
+ freq='B',
223
+ hour_offset=8,
224
+ time_period=time_period)
225
+ print(vols)
226
+
227
+ plt.show()
228
+
229
+
230
+ if __name__ == '__main__':
231
+
232
+ unit_test = UnitTests.GENERATE_VOL_DELTA_TS
233
+
234
+ is_run_all_tests = False
235
+ if is_run_all_tests:
236
+ for unit_test in UnitTests:
237
+ run_unit_test(unit_test=unit_test)
238
+ else:
239
+ run_unit_test(unit_test=unit_test)
@@ -0,0 +1,248 @@
1
+ """
2
+ collector of aligned time series data for option
3
+ options data is split in frames
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from enum import Enum
9
+ from typing import List, Literal, Optional, Tuple, Union
10
+
11
+ import numpy as np
12
+
13
+ # package
14
+ import pandas as pd
15
+ import qis
16
+ from qis import TimePeriod
17
+
18
+ # analytics
19
+ from option_chain_analytics.option_chain import SliceColumn
20
+
21
+
22
+ @dataclass
23
+ class ChainTs:
24
+ """
25
+ df collection for options and futures time series
26
+ chain_ts must contain SliceColumn.EXCHANGE_TIME.value
27
+ the most frequent request will be get df[contracts] at given timestamp
28
+ to expedite request we add groupby[SliceColumn.EXCHANGE_TIME.value]
29
+ """
30
+ chain_ts: pd.DataFrame
31
+ spot_data: pd.DataFrame
32
+ ticker: str
33
+
34
+ def __post_init__(self):
35
+ self.chain_ts_group_by_time = self.chain_ts.groupby(SliceColumn.EXCHANGE_TIME.value)
36
+ self.ewm_vol: Optional[pd.Series] = None
37
+
38
+ def get_contract_data(self, contact: str) -> pd.DataFrame:
39
+ df = self.chain_ts.loc[self.chain_ts[SliceColumn.CONTRACT.value] == contact, :]
40
+ return df
41
+
42
+ def print(self):
43
+ print('chain_ts')
44
+ print(self.chain_ts)
45
+ print('spot_data')
46
+ print(self.spot_data)
47
+
48
+ def get_timeindex(self) -> pd.DatetimeIndex:
49
+ return pd.DatetimeIndex(self.chain_ts_group_by_time.groups.keys())
50
+
51
+ def get_start_end_date(self) -> TimePeriod:
52
+ times = self.get_timeindex()
53
+ return qis.TimePeriod(times.min(), times.max())
54
+
55
+ def get_spot_data(self, time_period: TimePeriod = None) -> pd.DataFrame:
56
+ df = self.spot_data
57
+ if time_period is not None:
58
+ df = time_period.locate(df)
59
+ return df
60
+
61
+ def get_time_slice(self,
62
+ timestamp: pd.Timestamp,
63
+ contracts: List[str] = None
64
+ ) -> pd.DataFrame:
65
+ """
66
+ get non-nan time slice from series
67
+ return is df(index=contracts, colums=[mark_rpice, mark_vol,...])
68
+ """
69
+ if timestamp in self.chain_ts_group_by_time.groups:
70
+ df = self.chain_ts_group_by_time.get_group(timestamp)
71
+ df = df.set_index(SliceColumn.CONTRACT.value, drop=False)
72
+ df.index.name = 'contract'
73
+
74
+ # remove dublicates per timestamp
75
+ df = df.loc[~df.index.duplicated(keep='first')]
76
+
77
+ if contracts is not None:
78
+ df = df.iloc[np.in1d(df.index, contracts, assume_unique=False), :]
79
+ else:
80
+ # print(f"no slice data for {timestamp}")
81
+ df = pd.DataFrame()
82
+ return df
83
+
84
+ def get_spot_price(self,
85
+ value_time: pd.Timestamp = None,
86
+ index: Union[pd.DatetimeIndex, pd.Index] = None
87
+ ) -> Union[float, pd.Series]:
88
+ value = self.spot_data['close'].rename(self.ticker)
89
+ if value_time is not None:
90
+ if value_time in value.index:
91
+ value = value[value_time]
92
+ else:
93
+ raise KeyError(f"in get_spot_price {value_time} not in {value.index}")
94
+ elif index is not None:
95
+ value = value.reindex(index=index, method='ffill').ffill()
96
+ return value
97
+
98
+ def get_spot_and_perp_price(self,
99
+ value_time: pd.Timestamp = None,
100
+ index: Union[pd.DatetimeIndex, pd.Index] = None
101
+ ) -> Tuple[Union[float, pd.Series], Union[float, pd.Series]]:
102
+ """
103
+ specific for cryptocurrency data where both spot and perp prices are used:
104
+ spot_price is the spot price
105
+ perp_mark_price is the mark price of the futures
106
+ """
107
+ value = self.spot_data[['close', 'mark_price']]
108
+ if value_time is not None:
109
+ if value_time in value.index:
110
+ value = value[value_time]
111
+ else:
112
+ raise KeyError(f"in get_spot_price {value_time} not in {value.index}")
113
+ elif index is not None:
114
+ value = value.reindex(index=index, method='ffill').ffill()
115
+
116
+ spot_price = value['close'].rename(self.ticker)
117
+ perp_price = value['mark_price'].rename(self.ticker)
118
+
119
+ return spot_price, perp_price
120
+
121
+ def get_funding_rate(self,
122
+ freq: Optional[Literal['h', 'D']] = 'h',
123
+ index: Union[pd.DatetimeIndex, pd.Index] = None,
124
+ is_rescale_to_one_hour: bool = True,
125
+ time_period: TimePeriod = None
126
+ ) -> pd.Series:
127
+ """
128
+ deribit reports hourly funding rate extrapolated to 8h interval
129
+ for h freq we resample at 8h and return last
130
+ for d freq we resample at 8h and then return the sum
131
+ if freq is Non we return actual rate, it needs to be divided by 8 for comps H or D frequences with is_rescale_to_one_hour = True
132
+ """
133
+ funding_rate_1h = self.spot_data['funding_rate'].rename(self.ticker)
134
+ if freq is not None:
135
+ funding_rate_8h = funding_rate_1h.resample('8h').mean()
136
+ if freq == 'h':
137
+ funding_rate = funding_rate_8h
138
+ if index is not None:
139
+ funding_rate = funding_rate.reindex(index=index).fillna(0.0)
140
+ elif freq == 'D':
141
+ funding_rate = funding_rate_8h.resample('D').sum()
142
+ if index is not None:
143
+ funding_rate = funding_rate.reindex(index=index, method='ffill').fillna(0.0)
144
+ else:
145
+ raise NotImplementedError(f"freq={freq}")
146
+ else:
147
+ # funding_rate = funding_rate_1h.cumsum(0).reindex(index=index, method='ffill').diff(1)
148
+ funding_rate = funding_rate_1h
149
+ if is_rescale_to_one_hour:
150
+ funding_rate = funding_rate / 8.0 # deribit computes funding based on 8h frequency
151
+
152
+ if time_period is not None:
153
+ funding_rate = time_period.locate(funding_rate)
154
+
155
+ return funding_rate
156
+
157
+ def get_ewm_vol(self,
158
+ value_time: pd.Timestamp,
159
+ span: float = 168 # =7*24
160
+ ) -> float:
161
+ if self.ewm_vol is None:
162
+ returns = np.log(self.spot_data['close']).diff()
163
+ self.ewm_vol = qis.compute_ewm_vol(data=returns, span=span, af=24.0 * 365.0)
164
+ idx = self.ewm_vol.index.get_indexer([value_time], method='ffill')
165
+ vol = self.ewm_vol.iloc[idx].to_numpy()[0]
166
+ return vol
167
+
168
+ @classmethod
169
+ def reduce(cls, obj: ChainTs, contracts: List[str]) -> ChainTs:
170
+ """
171
+ reduce chain to contain only given contracts
172
+ """
173
+ chain_ts = obj.chain_ts
174
+ return ChainTs(chain_ts=chain_ts.loc[chain_ts[SliceColumn.CONTRACT.value].isin(contracts), :],
175
+ spot_data=obj.spot_data,
176
+ ticker=obj.ticker)
177
+
178
+
179
+ @dataclass
180
+ class FuturesChainTs(ChainTs):
181
+ """
182
+ implementation of futures time series data
183
+ """
184
+ class FuturesDataColumns(str, Enum):
185
+ MARK_PRICE = 'mark_price'
186
+ OPEN = 'open'
187
+ HIGH = 'high'
188
+ LOW = 'low'
189
+ CLOSE = 'close'
190
+ VOLUME = 'usd_volume'
191
+ VOLUME_CONTRACTS = 'contract_count'
192
+ OI_VOLUME = 'oi_value_usd'
193
+ OI_CONTRACTS = 'oi_contract_count'
194
+
195
+
196
+ @dataclass
197
+ class OptionsDataDFs(ChainTs):
198
+ """
199
+ implementation of options time series data
200
+ fields of SliceColumn are contained in data_dict
201
+ all info (maturity, option type, etc is passed inside option data dfs)
202
+ """
203
+
204
+ @classmethod
205
+ def reduce(cls, obj: OptionsDataDFs, contracts: List[str]) -> OptionsDataDFs:
206
+ """
207
+ reduce chain to contain only given contracts
208
+ """
209
+ chain_ts = obj.chain_ts
210
+ return OptionsDataDFs(chain_ts=chain_ts.loc[chain_ts[SliceColumn.CONTRACT.value].isin(contracts), :],
211
+ spot_data=obj.spot_data,
212
+ ticker=obj.ticker)
213
+
214
+
215
+ class UnitTests(Enum):
216
+ TEST_CHAIN = 1
217
+
218
+
219
+ def run_unit_test(unit_test: UnitTests):
220
+
221
+ from option_chain_analytics import local_path as local_path
222
+
223
+ if unit_test == UnitTests.TEST_CHAIN:
224
+ ticker = 'ETH'
225
+ chain_ts = qis.load_df_from_feather(file_name=f"{ticker}_freq_H",
226
+ index_col=None,
227
+ local_path=f"{local_path.get_resource_path()}\\tardis\\")
228
+ spot_data = qis.load_df_from_feather(file_name=f"{ticker}-spot",
229
+ local_path=f"{local_path.get_resource_path()}\\deribit\\")
230
+
231
+ chain_ts = ChainTs(chain_ts=chain_ts, spot_data=spot_data, ticker=ticker)
232
+ chain_ts.print()
233
+ chain_ts.get_start_end_date().print()
234
+
235
+ df = chain_ts.get_time_slice(timestamp=pd.Timestamp('2019-04-01 08:00:00+00:00'))
236
+ print(df)
237
+
238
+
239
+ if __name__ == '__main__':
240
+
241
+ unit_test = UnitTests.TEST_CHAIN
242
+
243
+ is_run_all_tests = False
244
+ if is_run_all_tests:
245
+ for unit_test in UnitTests:
246
+ run_unit_test(unit_test=unit_test)
247
+ else:
248
+ run_unit_test(unit_test=unit_test)
@@ -0,0 +1,83 @@
1
+ """
2
+ define api and data specific conversions of options and futures tickers
3
+ """
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ import pandas as pd
8
+
9
+ TIME_FMT = '%Y%m%d%H%M%S'
10
+ EXPIRY_DATE_FORMAT = '%d%b%Y'
11
+ SECONDS_PER_DAY = 24*60*60 # hours, minute, seconds
12
+
13
+
14
+ class NearestStrikeOnGrid(Enum):
15
+ """
16
+ for selection of strikes
17
+ this specifies nearest strike from the strikes grid, for put ratios must be strictly below or above
18
+ """
19
+ NEAREST = 0
20
+ MAX_OI = 1
21
+ ABOVE = 2
22
+ BELOW = 3
23
+
24
+
25
+ class StrikeSelection(Enum):
26
+ # strike selection for a strategy
27
+ ATM = 1
28
+ DELTA = 2
29
+
30
+
31
+ def compute_time_to_maturity(maturity_time: pd.Timestamp,
32
+ value_time: pd.Timestamp,
33
+ is_floor_at_zero: bool = True,
34
+ af: float = 365
35
+ ) -> float:
36
+ """
37
+ return annualised difference between mat_date and value_time
38
+ """
39
+ ttm = (maturity_time - value_time).total_seconds() / (af*SECONDS_PER_DAY)
40
+ if is_floor_at_zero and ttm < 0.0:
41
+ ttm = 0.0
42
+ return ttm
43
+
44
+
45
+ def compute_days_to_maturity(maturity_time: pd.Timestamp,
46
+ value_time: pd.Timestamp,
47
+ is_floor_at_zero: bool = True,
48
+ af: float = 365
49
+ ) -> float:
50
+ """
51
+ return annualised difference between mat_date and value_time
52
+ """
53
+ ttm = (maturity_time - value_time).total_seconds() / (af*SECONDS_PER_DAY)
54
+ if is_floor_at_zero and ttm < 0.0:
55
+ ttm = 0.0
56
+ return ttm
57
+
58
+
59
+ def mat_to_timestamp(mat: str,
60
+ date_format: Optional[str] = None, # deribit = "%d%b%y"
61
+ hour_offset: Optional[int] = 8
62
+ ) -> pd.Timestamp:
63
+ """
64
+ from maturity string get timestamp
65
+ """
66
+ mat = pd.Timestamp(pd.to_datetime(mat, format=date_format), tz='UTC')
67
+ if hour_offset is not None:
68
+ mat = mat + pd.to_timedelta(hour_offset, unit='h')
69
+ return mat
70
+
71
+
72
+ def get_file_name(ticker: str, freq: Optional[str], hour_offset: Optional[int]) -> str:
73
+ if freq is not None:
74
+ if freq in ['h', 'H']:
75
+ file_name = f"{ticker}_freq_{freq}"
76
+ else:
77
+ if hour_offset is not None:
78
+ file_name = f"{ticker}_freq_{freq}_hour_{hour_offset}"
79
+ else:
80
+ file_name = f"{ticker}_freq_{freq}"
81
+ else:
82
+ file_name = ticker
83
+ return file_name
@@ -0,0 +1,27 @@
1
+ """Data generators and optional provider integrations.
2
+
3
+ Provider modules are imported lazily so the deterministic simulation path does
4
+ not require credentials or provider-specific dependencies.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from option_chain_analytics.data.simulated import generate_simulated_options_data
10
+
11
+ __all__ = [
12
+ 'fetch_yahoo_options_live_data',
13
+ 'generate_simulated_options_data',
14
+ 'update_deribit_options_data',
15
+ ]
16
+
17
+
18
+ def __getattr__(name: str) -> Any:
19
+ if name == 'update_deribit_options_data':
20
+ from option_chain_analytics.data.deribit import update_deribit_options_data
21
+
22
+ return update_deribit_options_data
23
+ if name == 'fetch_yahoo_options_live_data':
24
+ from option_chain_analytics.data.yahoo import fetch_yahoo_options_live_data
25
+
26
+ return fetch_yahoo_options_live_data
27
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')