Skip to content

Commit

Permalink
Iron Condor support, fixed inaccurate bid/ask prices for market orders (
Browse files Browse the repository at this point in the history
#18)

* fixed internal call spread ratio issue, various code improvments

* Implemented Iron Condor Strategy

* updated gitignore, renamed sample strategy and fixed incorrect option prices

* refactored option_strategies.py

* fixed inaccurate bid/ask prices for market orders

* Added numpy to requirements.txt

* Updated version to 1.0.3
  • Loading branch information
michaelchu authored Nov 22, 2018
1 parent c3ff033 commit 31c8c60
Show file tree
Hide file tree
Showing 15 changed files with 1,072 additions and 198 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ MANIFEST
/tests/.pytest_cache/
/strategies/data/
/strategies/results/
/strategies/private/

41 changes: 37 additions & 4 deletions optopsy/backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"exit_opt_price",
"entry_price",
"exit_price",
"profit",
"cost",
]


Expand All @@ -83,8 +83,36 @@ def _filter_data(data, filters):
return pd.concat(_apply_filters(data, filters))


def create_spread(data, leg_structs, filters):
def _do_dedupe(spread, groupby, col, mode):
# dedupe delta dist ties
if groupby is None:
groupby = [
"quote_date",
"expiration",
"underlying_symbol",
"ratio",
"option_type",
]

on = groupby + [col]

if mode == "min":
return spread.groupby(groupby)[col].min().to_frame().merge(spread, on=on)
else:
return spread.groupby(groupby)[col].max().to_frame().merge(spread, on=on)


def _dedup_rows_by_cols(spreads, cols, groupby=None, mode="max"):
return reduce(lambda i, c: _do_dedupe(spreads, groupby, c, mode), cols, spreads)


def create_spread(data, leg_structs, filters, sort_by, ascending):
legs = [_create_legs(data, leg) for leg in leg_structs]
sort_by = (
["quote_date", "expiration", "underlying_symbol", "strike"]
if sort_by is None
else sort_by
)

# merge and apply leg filters to create spread
filters = {**default_entry_filters, **filters}
Expand All @@ -97,7 +125,12 @@ def create_spread(data, leg_structs, filters):

# apply spread level filters to spread
spread_filters = {f: filters[f] for f in filters if f.startswith("entry_spread")}
return _filter_data(spread, spread_filters)
return (
_filter_data(spread, spread_filters)
.pipe(_dedup_rows_by_cols, ["delta", "strike"])
.sort_values(sort_by, ascending=ascending)
.pipe(assign_trade_num, ["quote_date", "expiration", "underlying_symbol"])
)


# this is the main function that runs the backtest engine
Expand All @@ -113,7 +146,7 @@ def run(data, trades, filters, init_balance=10000, mode="midpoint"):
.pipe(calc_pnl)
.rename(columns=output_cols)
.sort_values(["entry_date", "expiration", "underlying_symbol", "strike"])
.pipe(assign_trade_num)
.pipe(assign_trade_num, ["entry_date", "expiration", "underlying_symbol"])
)

return calc_total_profit(res), res[output_format]
80 changes: 80 additions & 0 deletions optopsy/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import datetime

filters = {
"start_date": datetime.date,
"end_date": datetime.date,
"std_expr": bool,
"contract_size": int,
"entry_dte": (int, tuple),
"entry_days": int,
"leg1_delta": (int, float, tuple),
"leg2_delta": (int, float, tuple),
"leg3_delta": (int, float, tuple),
"leg4_delta": (int, float, tuple),
"leg1_strike_pct": (int, float, tuple),
"leg2_strike_pct": (int, float, tuple),
"leg3_strike_pct": (int, float, tuple),
"leg4_strike_pct": (int, float, tuple),
"entry_spread_price": (int, float, tuple),
"entry_spread_delta": (int, float, tuple),
"entry_spread_yield": (int, float, tuple),
"exit_dte": int,
"exit_hold_days": int,
"exit_leg_1_delta": (int, float, tuple),
"exit_leg_1_otm_pct": (int, float, tuple),
"exit_profit_loss_pct": (int, float, tuple),
"exit_spread_delta": (int, float, tuple),
"exit_spread_price": (int, float, tuple),
"exit_strike_diff_pct": (int, float, tuple),
}


def _type_check(filter):
return all([isinstance(filter[f], filters[f]) for f in filter])


def singles_checks(filter):
return "leg1_delta" in filter and _type_check(filter)


def _sanitize(filters, f):
return filters[f][1] if isinstance(filters[f], tuple) else filters[f]


def call_spread_checks(f):
return (
"leg1_delta" in f
and "leg2_delta" in f
and _type_check(f)
and (_sanitize(f, "leg1_delta") > _sanitize(f, "leg2_delta"))
)


def put_spread_checks(f):
return (
"leg1_delta" in f
and "leg2_delta" in f
and _type_check(f)
and (_sanitize(f, "leg1_delta") < _sanitize(f, "leg2_delta"))
)


def iron_condor_checks(f):
return (
"leg1_delta" in f
and "leg2_delta" in f
and "leg3_delta" in f
and "leg4_delta" in f
and _type_check(f)
and (_sanitize(f, "leg1_delta") < _sanitize(f, "leg2_delta"))
and (_sanitize(f, "leg3_delta") > _sanitize(f, "leg4_delta"))
)


def iron_condor_spread_check(ic):
return (
ic.assign(d_strike=lambda r: ic.duplicated(subset="strike", keep=False))
.groupby(ic.index)
.filter(lambda r: (r.d_strike == False).all())
.drop(columns="d_strike")
)
110 changes: 63 additions & 47 deletions optopsy/option_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,76 +16,92 @@

from .enums import OptionType, OrderAction
from .backtest import create_spread
from .checks import (
singles_checks,
call_spread_checks,
put_spread_checks,
iron_condor_checks,
iron_condor_spread_check,
)
from datetime import datetime


def _add_date_range(s, e, f):
f["start_date"] = s
f["end_date"] = e
return f


def _dedup_legs(spreads):
sort_by = ["quote_date", "expiration", "underlying_symbol", "strike"]
groupby = ["quote_date", "expiration", "underlying_symbol", "ratio", "option_type"]
on = groupby + ["delta"]

return (
spreads.groupby(groupby)["delta"]
.max()
.to_frame()
.merge(spreads, on=on)
.sort_values(sort_by)
)
def _process_legs(data, legs, filters, check_func, sort_by=None, asc=True):
if _filter_checks(filters, check_func):
return create_spread(data, legs, filters, sort_by=sort_by, ascending=asc)
else:
raise ValueError(
"Invalid filter values provided, please check the filters and try again."
)


def _filter_check(filters):
return True
def _filter_checks(filter, func=None):
return True if func is None else func(filter)


def _date_checks(start, end):
return isinstance(start, datetime) and isinstance(end, datetime)
def _merge(filters, start, end):
return {**filters, **{"start_date": start, "end_date": end}}


def _process_legs(data, start, end, legs, filters):
filters = _add_date_range(start, end, filters)
if _filter_check(filters) and _date_checks(start, end):
return _dedup_legs(create_spread(data, legs, filters))
else:
raise ValueError("Invalid filters, or date types provided!")
def long_call(data, start, end, filters):
legs = [(OptionType.CALL, 1)]
return _process_legs(data, legs, _merge(filters, start, end), singles_checks)


def long_call(data, start_date, end_date, filters):
return _process_legs(data, start_date, end_date, [(OptionType.CALL, 1)], filters)
def short_call(data, start, end, filters):
legs = [(OptionType.CALL, -1)]
return _process_legs(data, legs, _merge(filters, start, end), singles_checks)


def short_call(data, start_date, end_date, filters):
return _process_legs(data, start_date, end_date, [(OptionType.CALL, -1)], filters)
def long_put(data, start, end, filters):
legs = [(OptionType.PUT, 1)]
return _process_legs(data, legs, _merge(filters, start, end), singles_checks)


def long_put(data, start_date, end_date, filters):
return _process_legs(data, start_date, end_date, [(OptionType.PUT, 1)], filters)
def short_put(data, start, end, filters):
legs = [(OptionType.PUT, -1)]
return _process_legs(data, legs, _merge(filters, start, end), singles_checks)


def short_put(data, start_date, end_date, filters):
return _process_legs(data, start_date, end_date, [(OptionType.PUT, -1)], filters)
def long_call_spread(data, start, end, filters):
legs = [(OptionType.CALL, 1), (OptionType.CALL, -1)]
return _process_legs(data, legs, _merge(filters, start, end), call_spread_checks)


def long_call_spread(data, start_date, end_date, filters):
def short_call_spread(data, start, end, filters):
legs = [(OptionType.CALL, -1), (OptionType.CALL, 1)]
return _process_legs(data, start_date, end_date, legs, filters)


def short_call_spread(data, start_date, end_date, filters):
legs = [(OptionType.CALL, 1), (OptionType.CALL, -1)]
return _process_legs(data, start_date, end_date, legs, filters)
return _process_legs(data, legs, _merge(filters, start, end), call_spread_checks)


def long_put_spread(data, start_date, end_date, filters):
def long_put_spread(data, start, end, filters):
legs = [(OptionType.PUT, -1), (OptionType.PUT, 1)]
return _process_legs(data, start_date, end_date, legs, filters)
return _process_legs(data, legs, _merge(filters, start, end), put_spread_checks)


def short_put_spread(data, start_date, end_date, filters):
def short_put_spread(data, start, end, filters):
legs = [(OptionType.PUT, 1), (OptionType.PUT, -1)]
return _process_legs(data, start_date, end_date, legs, filters)
return _process_legs(data, legs, _merge(filters, start, end), put_spread_checks)


def long_iron_condor(data, start, end, filters):
legs = [
(OptionType.PUT, 1),
(OptionType.PUT, -1),
(OptionType.CALL, -1),
(OptionType.CALL, 1),
]
return _process_legs(
data, legs, _merge(filters, start, end), iron_condor_checks
).pipe(iron_condor_spread_check)


def short_iron_condor(data, start, end, filters):
legs = [
(OptionType.PUT, -1),
(OptionType.PUT, 1),
(OptionType.CALL, 1),
(OptionType.CALL, -1),
]
return _process_legs(
data, legs, _merge(filters, start, end), iron_condor_checks
).pipe(iron_condor_spread_check)
41 changes: 29 additions & 12 deletions optopsy/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,54 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.

import numpy as np


def _calc_opt_px(data, action):
ask = f"ask_{action}"
bid = f"bid_{action}"

if action == "entry":
return np.where(data["ratio"] > 0, data[ask], data[bid])
elif action == "exit":
return np.where(data["ratio"] > 0, data[bid], data[ask])
return data


def _assign_opt_px(data, mode, action):
if mode == "midpoint":
data[f"{action}_opt_price"] = data[[f"bid_{action}", f"ask_{action}"]].mean(axis=1)
bid_ask = [f"bid_{action}", f"ask_{action}"]
data[f"{action}_opt_price"] = data[bid_ask].mean(axis=1)
elif mode == "market":
data[f"{action}_opt_price"] = data[f"ask_{action}"]
data[f"{action}_opt_price"] = _calc_opt_px(data, action)
return data


def assign_trade_num(data):
groupby = ["entry_date", "expiration", "underlying_symbol"]
def assign_trade_num(data, groupby):
data["trade_num"] = data.groupby(groupby).ngroup()
data.set_index("trade_num", inplace=True)
return data


def calc_entry_px(data, mode="midpoint"):
return _assign_opt_px(data, mode,'entry')
return _assign_opt_px(data, mode, "entry")


def calc_exit_px(data, mode="midpoint"):
return _assign_opt_px(data, mode, 'exit')
return _assign_opt_px(data, mode, "exit")


def calc_pnl(data):
# calculate the p/l for the trades
data["entry_price"] = data["entry_opt_price"] * data["ratio"] * data["contracts"]
data["exit_price"] = data["exit_opt_price"] * data["ratio"] * data["contracts"]
data["profit"] = data["exit_price"] - data["entry_price"]
return data
data["entry_price"] = (
data["entry_opt_price"] * data["ratio"] * data["contracts"] * 100
)
data["exit_price"] = (
data["exit_opt_price"] * data["ratio"] * -1 * data["contracts"] * 100
)
data["cost"] = data["exit_price"] + data["entry_price"]
return data.round(2)


def calc_total_profit(data):
return data["profit"].sum().round(2)
return data["cost"].sum().round(2)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ pandas>=0.23.1
enum34; python_version <= '2.7'
pathlib2; python_version <= '2.7'
pytest>=3.10.0
numpy>=1.14.3
pyprind>=2.11.2
Loading

0 comments on commit 31c8c60

Please sign in to comment.