Skip to content

Commit 67426df

Browse files
authored
Merge pull request #14 from options-testing-org/create-butterfly-position
Create butterfly position
2 parents ca5aed9 + 4753a17 commit 67426df

24 files changed

+842
-317
lines changed

src/options_framework/data/data_loader.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def __init__(self, *, start: datetime.datetime | datetime.date, end: datetime.da
2626
def next_option_chain(self, quote_datetime: datetime.datetime | datetime.date):
2727
if self.last_loaded_date < quote_datetime:
2828
self.load_cache(quote_datetime)
29-
self.get_next_option_chain(quote_datetime)
29+
self.get_option_chain(quote_datetime)
3030

3131
@abstractmethod
3232
def load_cache(self, quote_datetime: datetime.datetime):
@@ -37,7 +37,11 @@ def on_option_chain_loaded_loaded(self, quote_datetime: datetime.datetime | date
3737
self.emit('option_chain_loaded', quote_datetime=quote_datetime, option_chain=option_chain)
3838

3939
@abstractmethod
40-
def get_next_option_chain(self, quote_datetime: datetime.datetime):
40+
def get_option_chain(self, quote_datetime: datetime.datetime):
41+
pass
42+
43+
@abstractmethod
44+
def get_expirations(self):
4145
pass
4246

4347
@abstractmethod

src/options_framework/data/file_data_loader.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from options_framework.option import Option
88
from options_framework.option_types import OptionType, SelectFilter
99

10-
1110
def map_data_file_fields() -> dict:
1211
columns_idx = settings.COLUMN_ORDER
1312
column_field_mapping = settings.FIELD_MAPPING
@@ -32,7 +31,7 @@ def __init__(self, *, start: datetime.datetime, end: datetime.datetime, select_f
3231
self.data_root_folder = settings.DATA_IMPORT_FILE_PROPERTIES.data_files_folder
3332
self.last_loaded_date = start
3433

35-
def get_next_option_chain(self, quote_datetime: datetime.datetime):
34+
def get_option_chain(self, quote_datetime: datetime.datetime):
3635
filename = settings.DATA_IMPORT_FILE_PROPERTIES.data_file_name_format.replace('{year}', str(quote_datetime.year)) \
3736
.replace('{month}', str(quote_datetime.month).zfill(2)) \
3837
.replace('{day}', str(quote_datetime.day).zfill(2)) \
@@ -55,6 +54,9 @@ def get_next_option_chain(self, quote_datetime: datetime.datetime):
5554
def on_options_opened(self, options: list[Option]) -> None:
5655
pass
5756

57+
def get_expirations(self):
58+
pass
59+
5860
def _load_data_generator(self, f: io.TextIOWrapper):
5961
line = f.readline()
6062

@@ -83,25 +85,36 @@ def _load_data_generator(self, f: io.TextIOWrapper):
8385
expiration = datetime.datetime.strptime(values[self.field_mapping['expiration']],
8486
settings.DATA_IMPORT_FILE_PROPERTIES.expiration_date_format).date()
8587
strike = float(values[self.field_mapping['strike']])
86-
87-
if self.select_filter.expiration_range.low and expiration < self.select_filter.expiration_range.low:
88-
line = f.readline()
89-
continue
90-
if self.select_filter.expiration_range.high and expiration > self.select_filter.expiration_range.high:
91-
line = f.readline()
92-
continue
93-
if self.select_filter.strike_range.low and strike < self.select_filter.strike_range.low:
94-
line = f.readline()
95-
continue
96-
if self.select_filter.strike_range.high and strike > self.select_filter.strike_range.high:
97-
line = f.readline()
98-
continue
9988
quotedate = datetime.datetime.strptime(values[self.field_mapping['quote_datetime']],
10089
settings.DATA_IMPORT_FILE_PROPERTIES.quote_date_format)
10190
spot_price = float(values[self.field_mapping['spot_price']])
10291
bid = float(values[self.field_mapping['bid']])
10392
ask = float(values[self.field_mapping['ask']])
104-
price = ((ask - bid)/2) + bid
93+
price = ((ask - bid) / 2) + bid
94+
95+
if self.select_filter.expiration_dte:
96+
if self.select_filter.expiration_dte.low:
97+
low_date = quotedate + datetime.timedelta(days=self.select_filter.expiration_dte.low)
98+
if expiration < low_date.date():
99+
line = f.readline()
100+
continue
101+
if self.select_filter.expiration_dte.high:
102+
high_date = quotedate + datetime.timedelta(days=self.select_filter.expiration_dte.high)
103+
if expiration > high_date.date():
104+
line = f.readline()
105+
continue
106+
107+
if self.select_filter.strike_offset:
108+
if self.select_filter.strike_offset.low:
109+
low_strike = spot_price - self.select_filter.strike_offset.low
110+
if strike < low_strike:
111+
line = f.readline()
112+
continue
113+
high_strike = spot_price + self.select_filter.strike_offset.high
114+
if strike > high_strike:
115+
line = f.readline()
116+
continue
117+
105118
if 'delta' in self.fields_list:
106119
delta = float(values[self.field_mapping['delta']]) if 'delta' in settings.FIELD_MAPPING else None
107120
if self.select_filter.delta_range.low and self.select_filter.delta_range.high:

src/options_framework/data/sql_data_loader.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,18 @@ def __init__(self, *, start: datetime.datetime, end: datetime.datetime, select_f
2121
password = settings.PASSWORD
2222
self.connection_string = 'DRIVER={ODBC Driver 17 for SQL Server};SERVER=' + server + ';DATABASE=' + database \
2323
+ ';UID=' + username + ';PWD=' + password
24-
self.last_loaded_date = start
24+
self.last_loaded_date = start - datetime.timedelta(days=1)
2525
self.start_load_date = start
2626
self.datetimes_list = self._get_datetimes_list()
27+
expirations = self._get_expirations_list()
28+
self.expirations = [x.to_pydatetime().date() for x in list(expirations['expiration'])]
29+
#first_date = self.datetimes_list['col'].iloc[0].to_pydatetime()
30+
#self.load_cache(first_date)
2731

2832
def load_cache(self, start: datetime.datetime):
2933
self.start_load_date = start
3034
start_loc = self.datetimes_list.index.get_loc(str(start))
31-
end_loc = start_loc + 10_000
35+
end_loc = start_loc + settings.SQL_DATA_LOADER_SETTINGS.buffer_size
3236
end_loc = end_loc if end_loc < len(self.datetimes_list) else len(self.datetimes_list)-1
3337
query_end_date = self.datetimes_list.iloc[end_loc].name.to_pydatetime()
3438
query = self._build_query(start, query_end_date)
@@ -38,9 +42,9 @@ def load_cache(self, start: datetime.datetime):
3842
df.index = pd.to_datetime(df.index)
3943
self.data_cache = df
4044
connection.close()
41-
self.last_loaded_date = query_end_date # set to end of data loaded
45+
self.last_loaded_date = df.iloc[-1].name.to_pydatetime() # set to end of data loaded
4246

43-
def get_next_option_chain(self, quote_datetime):
47+
def get_option_chain(self, quote_datetime):
4448

4549
df = self.data_cache.loc[str(quote_datetime)]
4650

@@ -69,7 +73,7 @@ def get_next_option_chain(self, quote_datetime):
6973
def on_options_opened(self, options: list[Option]) -> None:
7074
option_ids = [str(o.option_id) for o in options]
7175
open_date = options[0].trade_open_info.date
72-
print(f"options {'.'.join(option_ids)} were opened on {open_date}")
76+
#print(f"options {','.join(option_ids)} were opened on {open_date}")
7377
fields = ['option_id'] + self.fields_list
7478
field_mapping = ','.join([db_field for option_field, db_field in settings.FIELD_MAPPING.items() \
7579
if option_field in fields])
@@ -87,6 +91,9 @@ def on_options_opened(self, options: list[Option]) -> None:
8791
cache = df.loc[df['option_id'] == option.option_id]
8892
option.update_cache = cache
8993

94+
def get_expirations(self):
95+
return self.expirations
96+
9097
def _build_query(self, start_date: datetime.datetime, end_date: datetime.datetime):
9198
fields = ['option_id'] + self.fields_list
9299
field_mapping = ','.join([db_field for option_field, db_field in settings.FIELD_MAPPING.items() \
@@ -109,22 +116,21 @@ def _build_query(self, start_date: datetime.datetime, end_date: datetime.datetim
109116
if high_val:
110117
query += f' and expiration <= DATEADD(day, {high_val}, quote_datetime)'
111118

112-
if self.select_filter.strike_range:
113-
low_val, high_val = self.select_filter.strike_range.low, self.select_filter.strike_range.high
119+
if self.select_filter.strike_offset:
120+
low_val, high_val = self.select_filter.strike_offset.low, self.select_filter.strike_offset.high
114121
if low_val:
115122
query += f' and strike >= {settings.SELECT_OPTIONS_QUERY.spot_price_field}-{low_val}'
116123
if high_val:
117124
query += f' and strike <= {settings.SELECT_OPTIONS_QUERY.spot_price_field}+{high_val}'
118125

119126
for fltr in [f for f in filter_dict.keys() if 'range' in f]:
120127
name, low_val, high_val = fltr[:-6], filter_dict[fltr]['low'], filter_dict[fltr]['high']
121-
if name == "strike": continue
122128
for val in [low_val, high_val]:
123129
if val is not None:# and high_val is not None:
124130
operator = ">=" if val is low_val else "<="
125131
query += f' and {name} {operator} {val}'
126132

127-
query += ' order by \'quote_datetime\', \'expiration\', \'strike\''
133+
query += ' order by quote_datetime, expiration, strike'
128134

129135
return query
130136

@@ -138,3 +144,19 @@ def _get_datetimes_list(self):
138144
df.index = pd.to_datetime(df.index)
139145
connection.close()
140146
return df
147+
148+
def _get_expirations_list(self):
149+
symbol = self.select_filter.symbol
150+
start_date = self.start_datetime
151+
end_date = self.end_datetime
152+
query = "select distinct expiration "
153+
query += settings.SELECT_OPTIONS_QUERY['from']
154+
query += (settings.SELECT_OPTIONS_QUERY['where']
155+
.replace('{symbol}', self.select_filter.symbol)
156+
.replace('{start_date}', str(start_date))
157+
.replace('{end_date}', str(end_date)))
158+
query += " order by expiration"
159+
connection = pyodbc.connect(self.connection_string)
160+
df = pd.read_sql(query, connection, parse_dates=True)
161+
connection.close()
162+
return df

src/options_framework/option.py

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,17 @@ class Option(Dispatcher):
3636
"""The expiration date"""
3737
option_type: OptionType = field(compare=True)
3838
"""Put or Call"""
39-
40-
# mutable fields, but must be updated with update method instead of directly
41-
quote_datetime: Optional[datetime.datetime] = field(default=None, compare=False)
39+
quote_datetime: datetime.datetime = field(default=None, compare=False)
4240
"""The date of the current price information: spot_price, bid, ask, and price"""
43-
spot_price: Optional[int | float] = field(default=None, compare=False)
41+
spot_price: int | float = field(default=None, compare=False)
4442
"""The current price of the underlying asset"""
45-
bid: Optional[float | int] = field(default=None, compare=False)
43+
bid: float | int = field(default=None, compare=False)
4644
"""The current bid price of the option"""
47-
ask: Optional[float | int] = field(default=None, compare=False)
45+
ask: float | int = field(default=None, compare=False)
4846
"""The current ask price of the option"""
49-
price: Optional[float | int] = field(default=None, compare=False)
47+
price: float | int = field(default=None, compare=False)
5048
"""The price is the mid-point between the bid and ask"""
51-
status: OptionStatus = field(init=False, default=None, compare=False)
49+
status: OptionStatus = field(init=False, default=OptionStatus.INITIALIZED, compare=False)
5250
"""
5351
The option status tracks the life cycle of an option. This is a flag enum, so there can be
5452
more than one status. To find out about a status, such as if the option is currently
@@ -106,25 +104,21 @@ def __post_init__(self):
106104
raise ValueError("expiration cannot be None")
107105
if self.option_type is None:
108106
raise ValueError("option_type cannot be None")
109-
110-
self.status = OptionStatus.CREATED
111-
112-
if (any([self.quote_datetime is not None, self.spot_price is not None, self.bid is not None,
113-
self.ask is not None,
114-
self.price is not None])
115-
and not all([self.quote_datetime is not None, self.spot_price is not None,
116-
self.bid is not None, self.ask is not None, self.price is not None])):
117-
raise ValueError(
118-
"All the required option quote values must be passed to set any quote values: "
119-
+ "quote date, spot price, bid, ask, price.")
107+
if self.quote_datetime is None:
108+
raise ValueError("quote_datetime cannot be None")
109+
if self.spot_price is None:
110+
raise ValueError("spot_price cannot be None")
111+
if self.bid is None:
112+
raise ValueError("bid cannot be None")
113+
if self.ask is None:
114+
raise ValueError("ask cannot be None")
115+
if self.price is None:
116+
raise ValueError("price cannot be None")
120117

121118
# make sure the quote date is not past the expiration date
122-
if self.quote_datetime is not None and self.quote_datetime.date() > self.expiration:
119+
if self.quote_datetime.date() > self.expiration:
123120
raise ValueError("Cannot create an option with a quote date past its expiration date")
124121

125-
if self.quote_datetime:
126-
self.status = OptionStatus.INITIALIZED
127-
128122
def __repr__(self) -> str:
129123
return f'<{self.option_type.name}({self.option_id}) {self.symbol} {self.strike} ' \
130124
+ f'{datetime.datetime.strftime(self.expiration, "%Y-%m-%d")}>'
@@ -151,7 +145,7 @@ def _check_expired(self):
151145
Assumes the option is PM settled. Add the status OptionStatus.EXPIRED flag if the
152146
option is expired.
153147
"""
154-
if OptionStatus.EXPIRED in self.status or OptionStatus.INITIALIZED not in self.status:
148+
if OptionStatus.EXPIRED in self.status:
155149
return
156150
quote_date, expiration_date = self.quote_datetime.date(), self.expiration
157151
quote_time, exp_time = self.quote_datetime.time(), datetime.time(16, 15)
@@ -161,6 +155,8 @@ def _check_expired(self):
161155
self.emit("option_expired", self.option_id)
162156

163157
def next_update(self, quote_datetime: datetime.datetime):
158+
if self.expiration > quote_datetime.date():
159+
return
164160
update_values = self.update_cache.loc[quote_datetime]
165161
self.quote_datetime = quote_datetime
166162
update_fields = [f for f in update_values.index if f not in ['option_id', 'symbol', 'strike', 'expiration', 'option_type']]
@@ -183,8 +179,7 @@ def next_update(self, quote_datetime: datetime.datetime):
183179
self.open_interest = update_values['open_interest']
184180
if 'implied_volatility' in update_fields:
185181
self.implied_volatility = update_values['implied_volatility']
186-
self.status &= ~OptionStatus.CREATED
187-
self.status |= OptionStatus.INITIALIZED
182+
188183
self._check_expired()
189184

190185

@@ -267,8 +262,6 @@ def open_trade(self, *, quantity: int, **kwargs: dict) -> TradeOpenInfo:
267262
268263
additional keyword arguments are added to the user_defined list of values
269264
"""
270-
if OptionStatus.CREATED in self.status:
271-
raise ValueError("Cannot open a position that does not have price data")
272265
if OptionStatus.TRADE_IS_OPEN in self.status:
273266
raise ValueError("Cannot open position. A position is already open.")
274267
if (quantity is None) or not (isinstance(quantity, int)) or (quantity == 0):
@@ -351,7 +344,6 @@ def close_trade(self, *, quantity: int, **kwargs: dict) -> TradeCloseInfo:
351344
if self.quantity == 0:
352345
self.status &= ~OptionStatus.TRADE_IS_OPEN
353346
self.status |= OptionStatus.TRADE_IS_CLOSED
354-
self.update_cache = None
355347
else:
356348
self.status |= OptionStatus.TRADE_PARTIALLY_CLOSED
357349

@@ -457,25 +449,22 @@ def _calculate_trade_close_info(self) -> None:
457449

458450
self.trade_close_info = trade_close
459451

460-
def dte(self) -> int | None:
452+
def dte(self) -> int:
461453
"""
462454
DTE is "days to expiration"
463455
:return: The number of days to the expiration for the current quote
464456
:rtype: int
465457
"""
466-
if OptionStatus.INITIALIZED not in self.status:
467-
return None
458+
468459
dt_date = self.quote_datetime.date()
469460
time_delta = self.expiration - dt_date
470461
return time_delta.days
471462

472463
@property
473464
def current_value(self) -> float:
474-
if self.quantity == 0:
475-
return 0.0
476465
current_price = decimalize_2(self.price)
477-
open_quantity = decimalize_0(self.quantity)
478-
current_value = current_price * 100 * open_quantity
466+
quantity = decimalize_0(self.quantity)
467+
current_value = current_price * 100 * quantity
479468
return float(current_value)
480469

481470
def get_unrealized_profit_loss(self) -> float:
@@ -568,7 +557,7 @@ def get_days_in_trade(self) -> int:
568557
time_delta = dt_date - self.trade_open_info.date.date()
569558
return time_delta.days
570559

571-
def itm(self) -> bool | None:
560+
def itm(self) -> bool :
572561
"""
573562
In the Money
574563
A call option is in the money when the current price is higher than or equal to the strike price
@@ -577,25 +566,20 @@ def itm(self) -> bool | None:
577566
:return: Returns a boolean value indicating whether the option is currently in the money.
578567
:rtype: bool
579568
"""
580-
if OptionStatus.INITIALIZED not in self.status:
581-
return None
582569

583570
if self.option_type == OptionType.CALL:
584571
return True if self.spot_price >= self.strike else False
585572
elif self.option_type == OptionType.PUT:
586573
return True if self.spot_price <= self.strike else False
587574

588-
def otm(self) -> bool | None:
575+
def otm(self) -> bool:
589576
"""
590577
Out of the Money
591578
A call option is out of the money when the current price is lower than the spot price.
592579
A put option is out of the money when the current price is greater than the spot price.
593580
:return: Returns a boolean value indicating whether the option is currently out of the money.
594581
:rtype: bool
595582
"""
596-
if OptionStatus.INITIALIZED not in self.status:
597-
return None
598-
599583
if self.option_type == OptionType.CALL:
600584
return True if self.spot_price < self.strike else False
601585
elif self.option_type == OptionType.PUT:

src/options_framework/option_chain.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ def on_option_chain_loaded(self, quote_datetime: datetime.datetime, option_chain
1818
self.expiration_strikes = {e: list(distinct([strike for strike in [option.strike
1919
for option in option_chain if
2020
option.expiration == e]])) for e in
21-
self.expirations}
21+
self.expirations}
22+
print(f'option chain loaded {quote_datetime}')
2223

2324
def get_option_by_id(self, option_id: str) -> Option:
2425
option = [option for option in self.option_chain if option.option_id == option_id]

0 commit comments

Comments
 (0)