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.
- option_chain_analytics/__init__.py +56 -0
- option_chain_analytics/chain_loader_from_ts.py +239 -0
- option_chain_analytics/chain_ts.py +248 -0
- option_chain_analytics/config.py +83 -0
- option_chain_analytics/data/__init__.py +27 -0
- option_chain_analytics/data/bloomberg.py +42 -0
- option_chain_analytics/data/ccxt_api.py +293 -0
- option_chain_analytics/data/chain_loader_from_ts.py +15 -0
- option_chain_analytics/data/chain_ts.py +5 -0
- option_chain_analytics/data/config.py +19 -0
- option_chain_analytics/data/deribit.py +252 -0
- option_chain_analytics/data/simulated.py +182 -0
- option_chain_analytics/data/yahoo.py +297 -0
- option_chain_analytics/fitters/__init__.py +1 -0
- option_chain_analytics/fitters/chain_fit_report.py +130 -0
- option_chain_analytics/fitters/qp_price_fitter.py +612 -0
- option_chain_analytics/fitters/utils.py +211 -0
- option_chain_analytics/fitters/vol_beta_fitter.py +465 -0
- option_chain_analytics/local_path.py +45 -0
- option_chain_analytics/option_chain.py +988 -0
- option_chain_analytics/ts_loaders.py +369 -0
- option_chain_analytics/utils/__init__.py +0 -0
- option_chain_analytics/utils/implied_forwards.py +136 -0
- option_chain_analytics/utils/numerics.py +115 -0
- option_chain_analytics/utils/portfolio_payoff.py +94 -0
- option_chain_analytics/utils/roll_maturities.py +246 -0
- option_chain_analytics/visuals/__init__.py +3 -0
- option_chain_analytics/visuals/chain_report.py +80 -0
- option_chain_analytics/visuals/plots.py +264 -0
- option_chain_analytics/visuals/slices.py +185 -0
- option_chain_analytics-2.0.0.dist-info/METADATA +204 -0
- option_chain_analytics-2.0.0.dist-info/RECORD +35 -0
- option_chain_analytics-2.0.0.dist-info/WHEEL +5 -0
- option_chain_analytics-2.0.0.dist-info/licenses/LICENSE +21 -0
- 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}')
|