Skip to content

Commit bb73524

Browse files
Copilotmichaelchu
andauthored
Run Black formatter on codebase (#68)
* Initial plan * Add type hints, docstrings, and improve error handling - Added comprehensive type hints to all functions in core.py, checks.py, datafeeds.py, strategies.py, and rules.py - Added docstrings to all public functions explaining parameters and return values - Improved error handling in csv_data() with specific exception types - Removed IDE suppression comment in datafeeds.py - Updated setup.py to reflect correct Python version requirement (3.8+) - All tests pass successfully Co-authored-by: michaelchu <540510+michaelchu@users.noreply.github.com> * Fix code review comments: improve type hints consistency and error handling - Fixed inconsistent use of List[tuple] to List[Tuple] throughout core.py - Improved docstring accuracy for _remove_invalid_evaluated_options - Enhanced error handling in csv_data to catch IndexError along with KeyError - Updated docstrings to accurately reflect all exception types raised - All tests continue to pass Co-authored-by: michaelchu <540510+michaelchu@users.noreply.github.com> * Run Black formatter on codebase Applied Black formatter to ensure consistent code style across the repository. Formatting changes: - Split long function signatures across multiple lines for better readability - Removed trailing whitespace from docstrings - Applied consistent spacing throughout codebase Files reformatted: - optopsy/rules.py - optopsy/checks.py - optopsy/datafeeds.py - optopsy/strategies.py - optopsy/core.py - tests/test_strategies.py All 33 tests pass successfully after formatting. Co-authored-by: michaelchu <540510+michaelchu@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: michaelchu <540510+michaelchu@users.noreply.github.com>
1 parent f6a68a1 commit bb73524

File tree

7 files changed

+373
-80
lines changed

7 files changed

+373
-80
lines changed

optopsy/checks.py

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
expected_types = {
1+
from typing import Any, Callable, Dict, Tuple
2+
import pandas as pd
3+
4+
expected_types: Dict[str, Tuple[str, ...]] = {
25
"underlying_symbol": ("object", "str"),
36
"underlying_price": ("int64", "float64"),
47
"option_type": ("object", "str"),
@@ -10,44 +13,69 @@
1013
}
1114

1215

13-
def _run_checks(params, data):
16+
def _run_checks(params: Dict[str, Any], data: pd.DataFrame) -> None:
17+
"""
18+
Run all validation checks on parameters and data.
19+
20+
Args:
21+
params: Dictionary of strategy parameters
22+
data: DataFrame containing option chain data
23+
24+
Raises:
25+
ValueError: If any validation check fails
26+
"""
1427
for k, v in params.items():
1528
if k in param_checks:
1629
param_checks[k](k, v)
1730
_check_data_types(data)
1831

1932

20-
def _check_positive_integer(key, value):
33+
def _check_positive_integer(key: str, value: Any) -> None:
34+
"""Validate that value is a positive integer."""
2135
if value <= 0 or not isinstance(value, int):
2236
raise ValueError(f"Invalid setting for {key}, must be positive integer")
2337

2438

25-
def _check_positive_integer_inclusive(key, value):
39+
def _check_positive_integer_inclusive(key: str, value: Any) -> None:
40+
"""Validate that value is a non-negative integer (zero allowed)."""
2641
if value < 0 or not isinstance(value, int):
2742
raise ValueError(f"Invalid setting for {key}, must be positive integer, or 0")
2843

2944

30-
def _check_positive_float(key, value):
45+
def _check_positive_float(key: str, value: Any) -> None:
46+
"""Validate that value is a positive float."""
3147
if value <= 0 or not isinstance(value, float):
3248
raise ValueError(f"Invalid setting for {key}, must be positive float type")
3349

3450

35-
def _check_side(key, value):
51+
def _check_side(key: str, value: Any) -> None:
52+
"""Validate that value is either 'long' or 'short'."""
3653
if value != "long" and value != "short":
3754
raise ValueError(f"Invalid setting for '{key}', must be only 'long' or 'short'")
3855

3956

40-
def _check_bool_type(key, value):
57+
def _check_bool_type(key: str, value: Any) -> None:
58+
"""Validate that value is a boolean."""
4159
if not isinstance(value, bool):
4260
raise ValueError(f"Invalid setting for {key}, must be boolean type")
4361

4462

45-
def _check_list_type(key, value):
63+
def _check_list_type(key: str, value: Any) -> None:
64+
"""Validate that value is a list."""
4665
if not isinstance(value, list):
4766
raise ValueError(f"Invalid setting for {key}, must be a list type")
4867

4968

50-
def _check_data_types(data):
69+
def _check_data_types(data: pd.DataFrame) -> None:
70+
"""
71+
Validate that DataFrame has required columns with correct data types.
72+
73+
Args:
74+
data: DataFrame to validate
75+
76+
Raises:
77+
ValueError: If required column is missing or has incorrect type
78+
"""
5179
df_type_dict = data.dtypes.astype(str).to_dict()
5280
for k, et in expected_types.items():
5381
if k not in df_type_dict:
@@ -58,7 +86,7 @@ def _check_data_types(data):
5886
)
5987

6088

61-
param_checks = {
89+
param_checks: Dict[str, Callable[[str, Any], None]] = {
6290
"dte_interval": _check_positive_integer,
6391
"max_entry_dte": _check_positive_integer,
6492
"exit_dte": _check_positive_integer_inclusive,

optopsy/core.py

Lines changed: 114 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import Any, Callable, Dict, List, Optional, Tuple
12
import pandas as pd
23
import numpy as np
34
from functools import reduce
@@ -8,44 +9,57 @@
89
pd.set_option("display.max_rows", None, "display.max_columns", None)
910

1011

11-
def _assign_dte(data):
12+
def _assign_dte(data: pd.DataFrame) -> pd.DataFrame:
13+
"""Assign days to expiration (DTE) to the dataset."""
1214
return data.assign(dte=lambda r: (r["expiration"] - r["quote_date"]).dt.days)
1315

1416

15-
def _trim(data, col, lower, upper):
17+
def _trim(data: pd.DataFrame, col: str, lower: float, upper: float) -> pd.DataFrame:
18+
"""Filter dataframe rows where column value is between lower and upper bounds."""
1619
return data.loc[(data[col] >= lower) & (data[col] <= upper)]
1720

1821

19-
def _ltrim(data, col, lower):
22+
def _ltrim(data: pd.DataFrame, col: str, lower: float) -> pd.DataFrame:
23+
"""Filter dataframe rows where column value is greater than or equal to lower bound."""
2024
return data.loc[data[col] >= lower]
2125

2226

23-
def _rtrim(data, col, upper):
27+
def _rtrim(data: pd.DataFrame, col: str, upper: float) -> pd.DataFrame:
28+
"""Filter dataframe rows where column value is less than or equal to upper bound."""
2429
return data.loc[data[col] <= upper]
2530

2631

27-
def _get(data, col, val):
32+
def _get(data: pd.DataFrame, col: str, val: Any) -> pd.DataFrame:
33+
"""Filter dataframe rows where column equals specified value."""
2834
return data.loc[data[col] == val]
2935

3036

31-
def _remove_min_bid_ask(data, min_bid_ask):
37+
def _remove_min_bid_ask(data: pd.DataFrame, min_bid_ask: float) -> pd.DataFrame:
38+
"""Remove options with bid or ask prices below minimum threshold."""
3239
return data.loc[(data["bid"] > min_bid_ask) & (data["ask"] > min_bid_ask)]
3340

3441

35-
def _remove_invalid_evaluated_options(data):
42+
def _remove_invalid_evaluated_options(data: pd.DataFrame) -> pd.DataFrame:
43+
"""Keep evaluated options where entry DTE is greater than exit DTE."""
3644
return data.loc[
3745
(data["dte_exit"] <= data["dte_entry"])
3846
& (data["dte_entry"] != data["dte_exit"])
3947
]
4048

4149

42-
def _cut_options_by_dte(data, dte_interval, max_entry_dte):
50+
def _cut_options_by_dte(
51+
data: pd.DataFrame, dte_interval: int, max_entry_dte: int
52+
) -> pd.DataFrame:
53+
"""Categorize options into DTE intervals for grouping."""
4354
dte_intervals = list(range(0, max_entry_dte, dte_interval))
4455
data["dte_range"] = pd.cut(data["dte_entry"], dte_intervals)
4556
return data
4657

4758

48-
def _cut_options_by_otm(data, otm_pct_interval, max_otm_pct_interval):
59+
def _cut_options_by_otm(
60+
data: pd.DataFrame, otm_pct_interval: float, max_otm_pct_interval: float
61+
) -> pd.DataFrame:
62+
"""Categorize options into out-of-the-money percentage intervals."""
4963
# consider using np.linspace in future
5064
otm_pct_intervals = [
5165
round(i, 2)
@@ -61,7 +75,10 @@ def _cut_options_by_otm(data, otm_pct_interval, max_otm_pct_interval):
6175
return data
6276

6377

64-
def _group_by_intervals(data, cols, drop_na):
78+
def _group_by_intervals(
79+
data: pd.DataFrame, cols: List[str], drop_na: bool
80+
) -> pd.DataFrame:
81+
"""Group options by intervals and calculate descriptive statistics."""
6582
# this is a bottleneck, try to optimize
6683
grouped_dataset = data.groupby(cols)["pct_change"].describe()
6784

@@ -73,8 +90,17 @@ def _group_by_intervals(data, cols, drop_na):
7390
return grouped_dataset
7491

7592

76-
def _evaluate_options(data, **kwargs):
93+
def _evaluate_options(data: pd.DataFrame, **kwargs: Any) -> pd.DataFrame:
94+
"""
95+
Evaluate options by filtering, merging entry and exit data, and calculating costs.
7796
97+
Args:
98+
data: DataFrame containing option chain data
99+
**kwargs: Configuration parameters including max_otm_pct, min_bid_ask, exit_dte
100+
101+
Returns:
102+
DataFrame with evaluated options including entry and exit prices
103+
"""
78104
# trim option chains with strikes too far out from current price
79105
data = data.pipe(_calculate_otm_pct).pipe(
80106
_trim,
@@ -103,7 +129,17 @@ def _evaluate_options(data, **kwargs):
103129
)[evaluated_cols]
104130

105131

106-
def _evaluate_all_options(data, **kwargs):
132+
def _evaluate_all_options(data: pd.DataFrame, **kwargs: Any) -> pd.DataFrame:
133+
"""
134+
Complete pipeline to evaluate all options with DTE and OTM percentage categorization.
135+
136+
Args:
137+
data: DataFrame containing option chain data
138+
**kwargs: Configuration parameters for evaluation and categorization
139+
140+
Returns:
141+
DataFrame with evaluated and categorized options
142+
"""
107143
return (
108144
data.pipe(_assign_dte)
109145
.pipe(_trim, "dte", kwargs["exit_dte"], kwargs["max_entry_dte"])
@@ -117,21 +153,25 @@ def _evaluate_all_options(data, **kwargs):
117153
)
118154

119155

120-
def _calls(data):
156+
def _calls(data: pd.DataFrame) -> pd.DataFrame:
157+
"""Filter dataframe for call options only."""
121158
return data[data.option_type.str.lower().str.startswith("c")]
122159

123160

124-
def _puts(data):
161+
def _puts(data: pd.DataFrame) -> pd.DataFrame:
162+
"""Filter dataframe for put options only."""
125163
return data[data.option_type.str.lower().str.startswith("p")]
126164

127165

128-
def _calculate_otm_pct(data):
166+
def _calculate_otm_pct(data: pd.DataFrame) -> pd.DataFrame:
167+
"""Calculate out-of-the-money percentage for each option."""
129168
return data.assign(
130169
otm_pct=lambda r: round((r["strike"] - r["underlying_price"]) / r["strike"], 2)
131170
)
132171

133172

134-
def _apply_ratios(data, leg_def):
173+
def _apply_ratios(data: pd.DataFrame, leg_def: List[Tuple]) -> pd.DataFrame:
174+
"""Apply position ratios (long/short multipliers) to entry and exit prices."""
135175
for idx in range(1, len(leg_def) + 1):
136176
entry_col = f"entry_leg{idx}"
137177
exit_col = f"exit_leg{idx}"
@@ -142,7 +182,10 @@ def _apply_ratios(data, leg_def):
142182
return data
143183

144184

145-
def _assign_profit(data, leg_def, suffixes):
185+
def _assign_profit(
186+
data: pd.DataFrame, leg_def: List[Tuple], suffixes: List[str]
187+
) -> pd.DataFrame:
188+
"""Calculate total profit/loss and percentage change for multi-leg strategies."""
146189
data = _apply_ratios(data, leg_def)
147190

148191
# determine all entry and exit columns
@@ -155,29 +198,48 @@ def _assign_profit(data, leg_def, suffixes):
155198

156199
data["pct_change"] = np.where(
157200
data["total_entry_cost"].abs() > 0,
158-
(data["total_exit_proceeds"] - data["total_entry_cost"]) / data["total_entry_cost"].abs(),
159-
np.nan
201+
(data["total_exit_proceeds"] - data["total_entry_cost"])
202+
/ data["total_entry_cost"].abs(),
203+
np.nan,
160204
)
161205

162206
return data
163207

164208

165-
def _strategy_engine(data, leg_def, join_on=None, rules=None):
209+
def _strategy_engine(
210+
data: pd.DataFrame,
211+
leg_def: List[Tuple],
212+
join_on: Optional[List[str]] = None,
213+
rules: Optional[Callable] = None,
214+
) -> pd.DataFrame:
215+
"""
216+
Core strategy execution engine that constructs single or multi-leg option strategies.
217+
218+
Args:
219+
data: DataFrame containing evaluated option data
220+
leg_def: List of tuples defining strategy legs (side, filter_function)
221+
join_on: Columns to join on for multi-leg strategies
222+
rules: Optional filtering rules to apply after joining legs
223+
224+
Returns:
225+
DataFrame with constructed strategy and calculated profit/loss
226+
"""
166227
if len(leg_def) == 1:
167228
data["pct_change"] = np.where(
168229
data["entry"].abs() > 0,
169230
(data["exit"] - data["entry"]) / data["entry"].abs(),
170-
np.nan
231+
np.nan,
171232
)
172233
return leg_def[0][1](data)
173234

174-
def _rule_func(d, r, ld):
235+
def _rule_func(
236+
d: pd.DataFrame, r: Optional[Callable], ld: List[Tuple]
237+
) -> pd.DataFrame:
175238
return d if r is None else r(d, ld)
176239

177240
partials = [leg[1](data) for leg in leg_def]
178241
suffixes = [f"_leg{idx}" for idx in range(1, len(leg_def) + 1)]
179242

180-
# noinspection PyTypeChecker
181243
return (
182244
reduce(
183245
lambda left, right: pd.merge(
@@ -190,7 +252,17 @@ def _rule_func(d, r, ld):
190252
)
191253

192254

193-
def _process_strategy(data, **context):
255+
def _process_strategy(data: pd.DataFrame, **context: Any) -> pd.DataFrame:
256+
"""
257+
Main entry point for processing option strategies.
258+
259+
Args:
260+
data: DataFrame containing raw option chain data
261+
**context: Dictionary containing strategy parameters, leg definitions, and formatting options
262+
263+
Returns:
264+
DataFrame with processed strategy results
265+
"""
194266
_run_checks(context["params"], data)
195267
return (
196268
_evaluate_all_options(
@@ -217,7 +289,24 @@ def _process_strategy(data, **context):
217289
)
218290

219291

220-
def _format_output(data, params, internal_cols, external_cols):
292+
def _format_output(
293+
data: pd.DataFrame,
294+
params: Dict[str, Any],
295+
internal_cols: List[str],
296+
external_cols: List[str],
297+
) -> pd.DataFrame:
298+
"""
299+
Format strategy output as either raw data or grouped statistics.
300+
301+
Args:
302+
data: DataFrame with strategy results
303+
params: Parameters including 'raw' and 'drop_nan' flags
304+
internal_cols: Columns to include in raw output
305+
external_cols: Columns to group by for statistics output
306+
307+
Returns:
308+
Formatted DataFrame with either raw data or descriptive statistics
309+
"""
221310
if params["raw"]:
222311
return data[internal_cols].reset_index(drop=True)
223312

0 commit comments

Comments
 (0)