| Overall Statistics |
|
Total Orders 684 Average Win 0.16% Average Loss -0.14% Compounding Annual Return -31.850% Drawdown 3.500% Expectancy -0.126 Start Equity 100000 End Equity 96995 Net Profit -3.005% Sharpe Ratio -3.36 Sortino Ratio -4.097 Probabilistic Sharpe Ratio 14.070% Loss Rate 59% Win Rate 41% Profit-Loss Ratio 1.12 Alpha -0.114 Beta -0.012 Annual Standard Deviation 0.037 Annual Variance 0.001 Information Ratio -6.217 Tracking Error 0.145 Treynor Ratio 10.13 Total Fees $0.00 Estimated Strategy Capacity $1000000.00 Lowest Capacity Asset SPXW Y59RE4SM15YM|SPX 31 Portfolio Turnover 1.65% |
# region imports
from AlgorithmImports import *
from datetime import timedelta, datetime, time, date
from options import BeIronCondor, Option, OptionType
from dataclasses import dataclass
# endregion
def round_to_nearest_0_05(price):
return round(round(price / 0.05) * 0.05, 1)
# P'_{SS} = P_{SS} + \frac{NSV - (P_{SS} - P_{LS})}{1 - \frac{\Delta_{LS}}{\Delta_{SS}}}
def estimate_pss(short_leg: OptionContract,
short_leg_fill_price: float,
long_leg: OptionContract,
long_leg_fill_price: float,
net_spread_value: float):
return short_leg_fill_price + (net_spread_value - (short_leg_fill_price - long_leg_fill_price)) / (
1 - long_leg.Greeks.Delta / short_leg.Greeks.Delta
)
class IronCondor:
class BeStopPrice:
def __init__(self,
short_leg: OptionContract,
short_leg_fill_price: float,
long_leg: OptionContract,
long_leg_fill_price: float,
this_side_premium: float,
other_side_premium: float,
elapsed_stop_time: timedelta = None,
short_leg_stop_price: float = None,
stop_out_type: OrderType = None,
long_leg_sell_price: float = None,
short_leg_market_price_on_stop: float = None,
long_leg_market_price_on_stop: float = None
):
self.is_call = "C" in short_leg.symbol.value
self.short_leg = short_leg
self.long_leg = long_leg
self.short_leg_fill_price = short_leg_fill_price
self.long_leg_fill_price = long_leg_fill_price
self.this_side_premium = this_side_premium
self.other_side_premium = other_side_premium
# tuning parameter
# set stop price, stop limit price and stop market price
# self.be_stop_price = round_to_nearest_0_05(
# self.long_leg_fill_price + self._get_short_leg_loss())
self.be_stop_price = round_to_nearest_0_05(
estimate_pss(short_leg, short_leg_fill_price, long_leg, long_leg_fill_price, this_side_premium + other_side_premium)
)
# self.be_stop_price = round_to_nearest_0_05(
# 2 * self.short_leg_fill_price)
self.stop_limit_price = round_to_nearest_0_05(
self.be_stop_price + 0.2 if self.is_call else self.be_stop_price + 0.3)
self.stop_market_price = round_to_nearest_0_05(
self.stop_limit_price + 0.3)
self.take_profit = 0.00
self.elpased_stop_time = elapsed_stop_time
self.short_leg_stop_price = short_leg_stop_price
self.stop_out_type = stop_out_type
self.long_leg_sell_price = long_leg_sell_price
self.short_leg_market_price_on_stop = short_leg_market_price_on_stop
self.long_leg_market_price_on_stop = long_leg_market_price_on_stop
def _get_short_leg_loss(self):
if self.this_side_premium < self.other_side_premium:
return 2 * self.this_side_premium
else:
return self.other_side_premium + self.this_side_premium
def get_long_leg_limit_price_on_stop(self):
assert (self.short_leg_stop_price is not None)
slippage = min(self.stop_slippage(), 0.3)
# assume the long leg remains its entry price or is able to moved up a little bit to cover the stop slippage cost
# return round_to_nearest_0_05(self.long_leg_fill_price + slippage)
return round_to_nearest_0_05(self.short_leg_stop_price - self.this_side_premium - self.other_side_premium)
# return round_to_nearest_0_05(self.short_leg_stop_price - self.this_side_premium - self.other_side_premium + slippage)
def is_stopped_out(self):
return self.short_leg_stop_price is not None
def stop_slippage(self):
assert (self.short_leg_stop_price is not None)
return self.short_leg_stop_price - self.be_stop_price
def realized_pl(self):
pl = self.short_leg_fill_price - self.long_leg_fill_price
if self.short_leg_stop_price is not None:
pl -= self.short_leg_stop_price
if self.long_leg_sell_price is not None:
pl += self.long_leg_sell_price
return pl
def debug_messages(self):
type = "CALL" if self.is_call else "PUT"
message = [f"{type} realized p/l: {(self.realized_pl()):+.2f}"]
if self.is_stopped_out():
hours = self.elpased_stop_time.total_seconds() // 3600 # Total hours
minutes = (self.elpased_stop_time.total_seconds() % 3600) // 60 # Remaining minutes
message += [f", Stop-Out {int(hours)}h:{int(minutes)}m by {self.stop_out_type}, expected in [{self.be_stop_price}:{self.stop_limit_price}:{self.stop_market_price}], actual: {self.short_leg_stop_price}, stop_slippage: {(self.stop_slippage()):+.2f}"]
message += [f", short_leg_market_price_on_stop: {self.short_leg_market_price_on_stop}, long_leg_market_price_on_stop: {self.long_leg_market_price_on_stop} "]
if self.long_leg_sell_price is not None:
message += [f", long_leg_sell_price: {self.long_leg_sell_price:.2f}"]
else:
message += [f"UNABLE to sell long leg at {self.get_long_leg_limit_price_on_stop()}!!!"]
return message
def __init__(self, quantity: int,
short_call: OptionContract,
long_call: OptionContract,
short_put: OptionContract,
long_put: OptionContract,
entry_time: datetime,
stop_limit_order_fn,
stop_market_order_fn,
limit_order_fn,
market_order_fn,
get_current_price
):
self.quantity = quantity
self.short_call = short_call
self.long_call = long_call
self.short_put = short_put
self.long_put = long_put
self.entry_time = entry_time
self.stop_limit_order_fn = stop_limit_order_fn
self.stop_market_order_fn = stop_market_order_fn
self.limit_order_fn = limit_order_fn
self.market_order_fn = market_order_fn
self.get_current_price = get_current_price
self.call_stops_oco = []
self.put_stops_oco = []
def __eq__(self, other):
return (
isinstance(other, IronCondor) and
self.short_call == other.short_call and
self.long_call == other.long_call and
self.short_put == other.short_put and
self.long_put == other.long_put and
self.entry_time == other.entry_time
)
def __hash__(self):
return hash((
self.quantity, self.short_call, self.long_call, self.short_put, self.long_put, self.entry_time
))
def get_log_messages(self):
basic = f"{self.entry_time.date()} {self.entry_time.time()} Iron Condor, quantity: {self.quantity} short_call: {self.short_call.strike}, long_call: {self.long_call.strike}, short_put: {self.short_put.strike}, long_put: {self.long_put.strike}"
fill_price = f"Fill Price: short_call {self.be_short_call_stop.short_leg_fill_price}, long_call {self.be_short_call_stop.long_leg_fill_price}, short_put: {self.be_short_put_stop.short_leg_fill_price}, long_put: {self.be_short_put_stop.long_leg_fill_price}"
stop_count = int(self.be_short_call_stop.is_stopped_out()) + \
int(self.be_short_put_stop.is_stopped_out())
status_map = {
0: "**No Stop**",
1: "!Single Stop!",
2: "!!Double Stop!!"
}
return [
basic,
fill_price,
status_map[stop_count],
f"IC realized P/L: {(self.be_short_call_stop.realized_pl() + self.be_short_put_stop.realized_pl()):+.2f}"
] + self.be_short_call_stop.debug_messages() + self.be_short_put_stop.debug_messages()
def limit_mid_price(self, contract: OptionContract):
return round_to_nearest_0_05((contract.ask_price + contract.bid_price) / 2)
def call_premium_est(self):
return self.short_call.last_price - self.long_call.last_price
def put_premium_est(self):
return self.short_put.last_price - self.long_put.last_price
def get_open_limit_legs(self):
return [
Leg.create(self.short_call.symbol, -1,
round_to_nearest_0_05(self.short_call.bid_price)),
Leg.create(self.long_call.symbol, 1,
round_to_nearest_0_05(self.long_call.ask_price)),
Leg.create(self.short_put.symbol, -1,
round_to_nearest_0_05(self.short_put.bid_price)),
Leg.create(self.long_put.symbol, 1,
round_to_nearest_0_05(self.long_put.ask_price))
]
def get_open_market_legs(self):
return [
Leg.create(self.short_call.symbol, -1),
Leg.create(self.long_call.symbol, 1),
Leg.create(self.short_put.symbol, -1),
Leg.create(self.long_put.symbol, 1)
]
def on_ic_filled(self, short_call_fill_price: float, long_call_fill_price: float, short_put_fill_price: float, long_put_fill_price: float):
call_premium = short_call_fill_price - long_call_fill_price
put_premium = short_put_fill_price - long_put_fill_price
self.be_short_call_stop = IronCondor.BeStopPrice(
short_leg=self.short_call, short_leg_fill_price=short_call_fill_price, long_leg=self.long_call, long_leg_fill_price=long_call_fill_price, this_side_premium=call_premium, other_side_premium=put_premium)
self.be_short_put_stop = IronCondor.BeStopPrice(
short_leg=self.short_put, short_leg_fill_price=short_put_fill_price, long_leg=self.long_put, long_leg_fill_price=long_put_fill_price, this_side_premium=put_premium, other_side_premium=call_premium
)
stop_limit_call = self.stop_limit_order_fn(self.short_call.symbol, self.quantity,
stop_price=self.be_short_call_stop.be_stop_price, limit_price=self.be_short_call_stop.stop_limit_price)
stop_market_call = self.stop_market_order_fn(
self.short_call.symbol, self.quantity, stop_price=self.be_short_call_stop.stop_market_price)
stop_limit_put = self.stop_limit_order_fn(self.short_put.symbol, self.quantity,
stop_price=self.be_short_put_stop.be_stop_price, limit_price=self.be_short_put_stop.stop_limit_price)
stop_market_put = self.stop_market_order_fn(
self.short_put.symbol, self.quantity, stop_price=self.be_short_put_stop.stop_market_price)
self.call_stops_oco = [stop_limit_call, stop_market_call]
self.put_stops_oco = [stop_limit_put, stop_market_put]
def tighten_stop_loss_order(self):
current_long_call_price = self.get_current_price(self.long_call.symbol)
current_long_put_price = self.get_current_price(self.long_put.symbol)
def sell_long_leg_on_stop_out(self, order_ticket: OrderTicket, stop_time: datetime, use_market_order=False):
type = order_ticket.order_type
fill_price = order_ticket.average_fill_price
is_call = "C" in order_ticket.symbol.value
# collect current spx price, stop_order_type, stop_price (compared to be_stop_price), short_leg_market_price, long_leg_market_price, current_time - entry_time
if is_call:
self.be_short_call_stop.elpased_stop_time = stop_time - self.entry_time
self.be_short_call_stop.short_leg_stop_price = fill_price
self.be_short_call_stop.stop_out_type = OrderType(type)
self.be_short_call_stop.short_leg_market_price_on_stop = self.get_current_price(self.short_call.symbol)
self.be_short_call_stop.long_leg_market_price_on_stop = self.get_current_price(self.long_call.symbol)
else:
self.be_short_put_stop.elpased_stop_time = stop_time - self.entry_time
self.be_short_put_stop.short_leg_stop_price = fill_price
self.be_short_put_stop.stop_out_type = OrderType(type)
self.be_short_put_stop.short_leg_market_price_on_stop = self.get_current_price(self.short_put.symbol)
self.be_short_put_stop.long_leg_market_price_on_stop = self.get_current_price(self.long_put.symbol)
if type == OrderType.STOP_LIMIT or type == OrderType.STOP_MARKET:
if use_market_order:
if is_call:
return self.market_order_fn(self.long_call.symbol, -self.quantity)
else:
return self.market_order_fn(self.long_put.symbol, -self.quantity)
else:
if is_call:
return self.limit_order_fn(self.long_call.symbol, -self.quantity, self.be_short_call_stop.get_long_leg_limit_price_on_stop())
else:
return self.limit_order_fn(self.long_put.symbol, -self.quantity, self.be_short_put_stop.get_long_leg_limit_price_on_stop())
def on_long_leg_sell_filled(self, order_ticket: OrderTicket, time: datetime):
type = order_ticket.order_type
fill_price = order_ticket.average_fill_price
is_call = "C" in order_ticket.symbol.value
if is_call:
self.be_short_call_stop.long_leg_sell_price = fill_price
else:
self.be_short_put_stop.long_leg_sell_price = fill_price
class BeIronCondorStrategy(QCAlgorithm):
def initialize(self):
first_trade_date = datetime(2023, 1, 4)
last_trade_date = datetime(2023, 1, 31)
self.last_backtest_day = date(
last_trade_date.year, last_trade_date.month, last_trade_date.day)
self.set_start_date(first_trade_date.year,
first_trade_date.month, first_trade_date.day)
end_day = last_trade_date + timedelta(days=1)
self.set_end_date(end_day.year, end_day.month, end_day.day)
self.real_initial_capital = 100000 # 10w usd
self.initial_capital = self.real_initial_capital
self.set_cash(self.initial_capital)
self.option_lookup_days = 0
self.vix = self.add_index_option("VIX").symbol
spx_option = self.add_index_option("SPX", "SPXW", Resolution.MINUTE)
spx_option.set_filter(self._filter)
self.spx = spx_option.symbol
self.daily_traunch_limit = 6
self.start_open_position_time = time(10 + 3, 0)
self.stop_open_position_time = time(12 + 3, 0)
self.target_premium_min = 0.5
self.target_premium_max = 2
self.premium_equal_eplison = 0.5
self.wing_width = 10
self.delta = 0.2
self.allowed_loss = 0.02 # 2% at double stop loss
self.cool_off = timedelta(minutes=30)
# state tracker
self.current_day_trades = 0
self.order_id_to_ic = {}
self.last_ic_trade = None
self.end_day_log_date = None
# be iron condor tuning parameter
def _get_current_price(self, symbol):
return self.securities[symbol].Close
def _filter(self, universe):
return universe.include_weeklys().expiration(0, 7).delta(-0.6, 0.6)
def on_order_event(self, order_event: OrderEvent):
oid = order_event.order_id
if order_event.status == OrderStatus.FILLED:
if oid in self.order_id_to_ic:
order_type = order_event.ticket.order_type
ic = self.order_id_to_ic[oid]
is_call = "C" in order_event.symbol.value
if order_type == OrderType.STOP_LIMIT or order_type == OrderType.STOP_MARKET:
oco = ic.call_stops_oco if is_call else ic.put_stops_oco
for order_ticket in oco:
# cancel rest of the order
if order_ticket.order_id != oid:
order_ticket.cancel()
else:
market_order = False # TODO: Optimize the long leg sell price
if market_order:
sell_long_leg_order = ic.sell_long_leg_on_stop_out(
order_ticket, self.time, True) # use market order
ic.on_long_leg_sell_filled(
sell_long_leg_order, self.time)
else:
sell_long_leg_order = ic.sell_long_leg_on_stop_out(
order_ticket, self.time, False) # use limit order
self.order_id_to_ic[sell_long_leg_order.order_id] = ic
elif order_type == OrderType.LIMIT:
ic.on_long_leg_sell_filled(order_event.ticket, self.time)
def on_data(self, data: Slice):
# no op on last backtest day
if self.time.date() > self.last_backtest_day:
return
# tighten existing IronCondors
# for ic in set(self.order_id_to_ic.values()):
# ic.tighten_stop_loss_order()
cool_off = self.last_ic_trade is None or self.time - \
self.last_ic_trade >= self.cool_off
in_trading_window = self.time.time(
) >= self.start_open_position_time and self.time.time() <= self.stop_open_position_time
if not cool_off or self.current_day_trades > self.daily_traunch_limit or not in_trading_window:
return
chain = data.option_chains.get(self.spx)
if not chain:
return
expiry_date = self.time.date() + timedelta(days=self.option_lookup_days)
put_contracts = sorted([
x for x in chain
if x.expiry.date() == expiry_date and self.securities[x.symbol].is_tradable
and x.right == OptionRight.PUT
and x.Volume > 0
], key=lambda x: -x.strike) # sorted in descending
call_contracts = sorted([
x for x in chain
if x.expiry.date() == expiry_date and self.securities[x.symbol].is_tradable
and x.right == OptionRight.CALL
and x.Volume > 0
], key=lambda x: x.strike) # sorted in ascending
# TODO - reduce risk at 12:30, and constantly tighten the short leg price
put_vertical = None
for i in range(len(put_contracts)):
sell_put = put_contracts[i]
if put_vertical is not None:
break
if abs(sell_put.Greeks.delta) <= self.delta:
for j in range(i+1, len(put_contracts)):
buy_put = put_contracts[j]
if abs(sell_put.strike - buy_put.strike) >= self.wing_width:
put_vertical = (sell_put, buy_put)
break
call_vertical = None
for i in range(len(call_contracts)):
sell_call = call_contracts[i]
if call_vertical is not None:
break
if abs(sell_call.Greeks.delta) <= self.delta:
for j in range(i+1, len(call_contracts)):
buy_call = call_contracts[j]
if abs(sell_call.strike - buy_call.strike) >= self.wing_width:
call_vertical = (sell_call, buy_call)
break
if call_vertical and put_vertical:
quantity = 1
# TODO: don't accidentally close a open short leg
ic = IronCondor(
quantity, call_vertical[0], call_vertical[1], put_vertical[0], put_vertical[1], entry_time=self.time, stop_limit_order_fn=self.stop_limit_order, stop_market_order_fn=self.stop_market_order, limit_order_fn=self.limit_order, market_order_fn=self.market_order, get_current_price=self._get_current_price)
def in_range(
x): return x >= self.target_premium_min and x <= self.target_premium_max
if not in_range(ic.call_premium_est()) and not in_range(ic.put_premium_est()):
return
# don't accept too much difference in put / call side premium
if abs(ic.call_premium_est() - ic.put_premium_est()) >= self.premium_equal_eplison:
return
try:
order_properties = OrderProperties()
order_properties.time_in_force = TimeInForce.DAY
ic_order_tickets = self.combo_market_order(legs=ic.get_open_market_legs(),
quantity=quantity,
order_properties=order_properties)
short_call_fill_price = 0
long_call_fill_price = 0
short_put_fill_price = 0
long_call_fill_price = 0
for ticket in ic_order_tickets:
is_call = "C" in ticket.symbol.value
is_short = ticket.quantity < 0
if is_call:
if is_short:
short_call_fill_price = ticket.average_fill_price
else:
long_call_fill_price = ticket.average_fill_price
else:
if is_short:
short_put_fill_price = ticket.average_fill_price
else:
long_put_fill_price = ticket.average_fill_price
ic.on_ic_filled(short_call_fill_price, long_call_fill_price,
short_put_fill_price, long_put_fill_price)
for order_ticket in ic.call_stops_oco + ic.put_stops_oco:
self.order_id_to_ic[order_ticket.order_id] = ic
self.last_ic_trade = self.time
self.current_day_trades += 1
except Exception as e:
self.log(f"Exception when open IC")
def on_end_of_day(self, symbol):
if (not self.end_day_log_date or self.time.date() != self.end_day_log_date):
total_profit_or_loss = self.portfolio.total_portfolio_value - self.initial_capital
self.debug(
f"end of day {self.time.date()} reset, P/L: {total_profit_or_loss}, symbol: {symbol}")
for ic in set(self.order_id_to_ic.values()):
for m in ic.get_log_messages():
self.debug(m)
self.order_id_to_ic = {}
self.current_day_trades = 0
self.last_ic_trade = None
self.end_day_log_date = self.time.date()
# region imports
from AlgorithmImports import *
# endregion
from enum import Enum
from datetime import datetime, date
from string import Template
from dataclasses import dataclass
UNEXPECTED_MARKET_SLIPPAGE = 0.1
EXPECTED_WIN_RATE = 0.39
DOUBLE_STOP_LOSS_PROB = 0.04
MARKET_SLIPPAGE_PROB = 0.02
SINGLE_STOP_LOSS_PROB = 0.57
EXPECTATNCY_TEMPLATE = Template(
"""Expectation =
EXPECTED_WIN_RATE * (call_rewards + put_rewards)
- (SINGLE_STOP_LOSS_PROB * single_worst_loss + DOUBLE_STOP_LOSS_PROB * double_worst_loss) * MARKET_SLIPPAGE_PROB
- (1 - MARKET_SLIPPAGE_PROB) * (SINGLE_STOP_LOSS_PROB * single_avg_loss + DOUBLE_STOP_LOSS_PROB * double_avg_loss)
$EXPECTED_WIN_RATE * ($call_rewards + $put_rewards)
- ($SINGLE_STOP_LOSS_PROB * $single_worst_loss + $DOUBLE_STOP_LOSS_PROB * $double_worst_loss) * $MARKET_SLIPPAGE_PROB
- (1 - $MARKET_SLIPPAGE_PROB) * ($SINGLE_STOP_LOSS_PROB * $single_avg_loss + $DOUBLE_STOP_LOSS_PROB * $double_avg_loss)
= $expectation_result
"""
)
WORST_LOSS_TEMPLATE = Template(
"Worst Loss (double loss + market slippage on both side) = $worst_loss_result")
def format_date(date: date):
return date.strftime("%d %b %y").upper()
def contract_fee(symbol: str, premium: float, robinhood: bool = True):
fee = 0.57 if premium < 1 else 0.66
if symbol == "SPX":
return fee + 0.5 - 0.15 if robinhood else 0.65 + 0.5
else:
return 0.03
def risk_reward(risk: float, reward: float):
ratio = round(abs(risk) / abs(reward), 2)
return f"{ratio:.1f}:1"
class OptionType(Enum):
CALL = "CALL"
PUT = "PUT"
class Option:
def __init__(self, strike: float, premium: float, type: OptionType, contract_symbol: str, expiration_date: date = datetime.now(), symbol: str = "SPX"):
self.type = type
self.strike = strike
self.premium = premium
self.contract_symbol = contract_symbol
self.expiration_date = expiration_date
self.symbol = symbol
self.fee = contract_fee(symbol, premium, True)
def __eq__(self, other):
return (
isinstance(other, Option) and
self.type == other.type and
self.strike == other.strike and
self.premium == other.premium and
self.symbol == other.symbol and
self.contract_symbol == other.contract_symbol and
self.expiration_date == other.expiration_date
)
def __hash__(self):
# Combine the hash of significant attributes
return hash((self.type, self.strike, self.premium, self.quantity, self.expiration_date, self.symbol, self.contract_symbol))
def _tos_code(self):
suffix = "W" if self.symbol.upper() == "SPX" else ""
d = self.expiration_date.strftime("%y%m%d")
return f".{self.symbol.upper()}{suffix}{d}{self.type.name[0]}{self.strike}"
def __str__(self):
return "{}, {} strike: {}, type: {}, premium: {}, expiration_date: {}, fee: {}".format(self._tos_code(), self.symbol, self.strike, self.type.name, self.premium, format_date(self.expiration_date), round(self.fee, 2))
class CloseOrder:
def __init__(self,
short_leg: Option, long_leg: Option,
quantity: int, acceptable_total_loss: float, profit_target=0):
self.short_strike = short_leg.strike
self.short_premium = short_leg.premium
self.long_strike = long_leg.strike
self.long_cost = long_leg.premium
self.quantity = quantity
self.acceptable_total_loss = acceptable_total_loss
self.type = short_leg.type
self.symbol = short_leg.symbol
self.expiration_date = short_leg.expiration_date
self.fees = short_leg.fee + long_leg.fee
# take profit when vertical price is 0
self.profit_target = profit_target
self.first_stop_trigger_price = self.short_premium - \
self.long_cost + self.acceptable_total_loss - 0.1
self.stop_limit_price = self.first_stop_trigger_price + \
0.4 if type == OptionType.PUT else self.first_stop_trigger_price + 0.3
self.second_stop_trigger_price = self.stop_limit_price + 0.3
def expected_reward(self):
return self.quantity * \
(self.short_premium - self.long_cost -
self.profit_target) * 100 - self.fees
def _get_loss(self, price):
return self.quantity * (price - (self.short_premium - self.long_cost)) * 100 + self.fees
def expected_loss(self):
return 0.5 * self.loss_on_first_stop_price() + 0.3 * self.loss_on_stop_limit_price() + 0.15 * self.loss_on_market_slippage() + 0.05 * self.loss_on_second_stop_price()
def loss_on_first_stop_price(self):
return self._get_loss(self.first_stop_trigger_price)
def loss_on_stop_limit_price(self):
return self._get_loss(self.stop_limit_price)
def loss_on_second_stop_price(self):
return self._get_loss(self.second_stop_trigger_price)
def loss_on_market_slippage(self):
return self._get_loss(self.second_stop_trigger_price + UNEXPECTED_MARKET_SLIPPAGE)
def _trim0(self, price):
return f"{price:.2f}".lstrip('0')
BUY_TO_CLOSE_VERTICAL_STOP_MARKET = Template(
"BUY +$quantity VERTICAL $SYMBOL 100 (Weeklys) $exp_date $short_leg_strike/$long_leg_strike $type STP $last_stop_trigger_price MARK OCO")
BUY_TO_CLOSE_VERTICAL_STOP_LIMIT = Template(
"BUY +$quantity VERTICAL $SYMBOL 100 (Weeklys) $exp_date $short_leg_strike/$long_leg_strike $type @$stop_limit_price STPLMT $first_stop_trigger_price MARK OCO")
BUY_TO_CLOSE_VERTICAL_PROFIT_TEMPLATE = Template(
"""BUY +$quantity VERTICAL $SYMBOL 100 (Weeklys) $exp_date $short_leg_strike/$long_leg_strike $type @$profit_target LMT MARK OCO""")
def close_vertical_tos_oco(self):
data = {
"quantity": abs(self.quantity),
"SYMBOL": self.symbol,
"short_leg_strike": self.short_strike,
"long_leg_strike": self.long_strike,
"type": self.type.name,
"last_stop_trigger_price": self._trim0(self.second_stop_trigger_price),
"first_stop_trigger_price": self._trim0(self.first_stop_trigger_price),
"stop_limit_price": self._trim0(self.stop_limit_price),
"profit_target": self._trim0(self.profit_target),
"exp_date": format_date(self.expiration_date)
}
return [
CloseOrder.BUY_TO_CLOSE_VERTICAL_STOP_MARKET.substitute(data),
CloseOrder.BUY_TO_CLOSE_VERTICAL_STOP_LIMIT.substitute(data),
CloseOrder.BUY_TO_CLOSE_VERTICAL_PROFIT_TEMPLATE.substitute(data),
]
SINGLE_LEG_STOP_LOSS_TEMPLATE_0 = Template(
"BUY +$quantity $SYMBOL 100 (Weeklys) $exp_date $short_strike $type STP $last_stop_trigger_price OCO")
# BUY +10 SPX 100 (Weeklys) 29 NOV 24 6025 CALL @1.50 STPLMT 1.49 OCO
SINGLE_LEG_STOP_LOSS_TEMPLATE_1 = Template(
"BUY +$quantity $SYMBOL 100 (Weeklys) $exp_date $short_strike $type @$stop_limit_price STPLMT $first_stop_trigger_price OCO")
SINGLE_LEG_STOP_LOSS_TEMPLATE_2 = Template(
"(Manual) Shorterly after the short leg is stopped out, close the long leg no less than $long_leg_sell_price.")
SINGLE_LEG_STOP_LOSS_TEMPLATE_3 = Template(
"SELL -$quantity $SYMBOL 100 (Weeklys) $exp_date $long_strike $type $long_leg_sell_price LMT MARK")
def _round_to_nearest_0_05(self, value):
return round(value / 0.05) * 0.05
def close_short_leg_only_tos(self):
stop_limit_price = self.short_premium + self.acceptable_total_loss
# fee = contract_fee(self.symbol, stop_limit_price, True)
# stop_limit_price -= fee / 100
last_stop_trigger_price = stop_limit_price + 0.05
first_stop_trigger_price = stop_limit_price - 0.05
long_leg_sell_price = self.long_cost
data = {
"quantity": abs(self.quantity),
"SYMBOL": self.symbol,
"short_strike": self.short_strike,
"long_strike": self.long_strike,
"type": self.type.name,
"exp_date": format_date(self.expiration_date),
"last_stop_trigger_price": self._trim0(self._round_to_nearest_0_05(last_stop_trigger_price)),
"first_stop_trigger_price": self._trim0(self._round_to_nearest_0_05(first_stop_trigger_price)),
"stop_limit_price": self._trim0(self._round_to_nearest_0_05(stop_limit_price)),
"long_leg_sell_price": self._trim0(self._round_to_nearest_0_05(long_leg_sell_price))
}
return [
CloseOrder.SINGLE_LEG_STOP_LOSS_TEMPLATE_0.substitute(data),
CloseOrder.SINGLE_LEG_STOP_LOSS_TEMPLATE_1.substitute(data),
CloseOrder.SINGLE_LEG_STOP_LOSS_TEMPLATE_2.substitute(data),
CloseOrder.SINGLE_LEG_STOP_LOSS_TEMPLATE_3.substitute(data)
]
class ChartData:
def __init__(self, x_prices, y_total_pls, break_even_prices):
self.x_prices = x_prices
self.y_total_pls = y_total_pls
self.break_even_prices = break_even_prices
def max_profit(self):
return round(max(self.y_total_pls), 2)
def max_loss(self):
return round(min(self.y_total_pls), 2)
def risk_reward(self):
ratio = abs(round(self.max_loss() / self.max_profit(), 4))
return f"{ratio:.1f}:1"
class BeIronCondor:
def __init__(self, quantity, short_call: Option, long_call: Option, short_put: Option, long_put: Option):
# verify
assert (short_call.strike <
long_call.strike and short_call.premium > long_call.premium)
assert (short_put.strike >
long_put.strike and short_put.premium > long_put.premium)
legs_verifier = [short_put, long_put, long_call, short_call]
symbols = set([l.symbol for l in legs_verifier])
assert (len(symbols) == 1)
expiration_dates = set([l.expiration_date for l in legs_verifier])
assert (len(expiration_dates) == 1)
self.quantity = quantity
self.short_call = short_call
self.long_call = long_call
self.short_put = short_put
self.long_put = long_put
self.put_premium = self.short_put.premium - \
self.long_put.premium
self.call_premium = self.short_call.premium - \
self.long_call.premium
self.close_call_order = CloseOrder(
short_leg=self.short_call,
long_leg=self.long_call,
quantity=self.quantity,
acceptable_total_loss=self.put_premium
)
self.close_put_order = CloseOrder(
short_leg=self.short_put,
long_leg=self.long_put,
quantity=self.quantity,
acceptable_total_loss=self.call_premium
)
def __str__(self):
open_positions = [self.short_call,
self.long_call, self.short_put, self.long_put]
return "\n".join(["-" + str(opt) for opt in open_positions])
STATUS_TEMPALTE = Template(
"""Call Net Credit: $call_credit
Put Net Credit: $put_credit
Total Net Credit: $total_credit
Open Positions:
$open_positions
BreakEven Prices: $break_even_prices
Max Profit: $max_profit
Max Loss: $max_loss
Risk-Reward: $risk_reward
Adjusted Risk-Reward: $adjusted_risk_reward
TOS Vertical Stop Loss Orders:
$tos_vertical_stop_loss_orders
TOS Short Leg Only Stop Loss Orders:
$tos_short_leg_only_stop_loss_orders""")
def position_status(self):
chart_data = self.get_chart_data()
be_prices = [f"{p:.2f}" for p in chart_data.break_even_prices]
open_positions = [self.short_call,
self.long_call, self.short_put, self.long_put]
call_vertical_close_orders = "\n".join(
self.close_call_order.close_vertical_tos_oco())
put_vertical_close_orders = "\n".join(
self.close_put_order.close_vertical_tos_oco())
call_short_leg_only_close_orders = "\n".join(
self.close_call_order.close_short_leg_only_tos())
put_short_leg_only_close_orders = "\n".join(
self.close_put_order.close_short_leg_only_tos())
data = {
"call_credit": round(self.call_premium, 2),
"put_credit": round(self.put_premium, 2),
"total_credit": round(self.put_premium + self.call_premium, 2),
"break_even_prices": ", ".join(be_prices),
"open_positions": "\n".join(["-" + str(opt) for opt in open_positions]),
"max_profit": round(self.max_profit(), 2),
"max_loss": round(self.max_loss(), 2),
"risk_reward": risk_reward(chart_data.max_loss(), chart_data.max_profit()),
"adjusted_risk_reward": risk_reward(self.max_loss(), self.max_profit()),
"tos_vertical_stop_loss_orders": call_vertical_close_orders + "\n\n" + put_vertical_close_orders,
"tos_short_leg_only_stop_loss_orders": call_short_leg_only_close_orders + "\n\n" + put_short_leg_only_close_orders,
"expectation": round(self.expectation(), 2)
}
return BeIronCondor.STATUS_TEMPALTE.substitute(data)
def get_chart_data(self):
"""Get chart data
Returns:
ChartData: x axis, total p/l at x, and list of breakeven prices
"""
min_price = self.long_put.strike * 0.8
max_price = self.long_call.strike * 1.2
prices = []
while min_price <= max_price:
prices.append(min_price)
min_price += 0.01
total_pl = [0] * len(prices)
for opt in [self.short_call, self.long_call, self.long_put, self.short_put]:
for i, price in enumerate(prices):
payoff = (
max(price - opt.strike, 0) if opt.type == OptionType.CALL
else max(opt.strike - price, 0)
)
pl = (payoff - abs(opt.premium)) * self.quantity
total_pl[i] += pl
premium = self.call_premium + self.put_premium
break_even_prices = [self.short_put.strike -
premium, self.short_call.strike + premium]
return ChartData(prices, total_pl, break_even_prices)
def max_profit(self):
return self.close_call_order.expected_reward() + self.close_put_order.expected_reward()
def max_loss(self):
return self.close_call_order.loss_on_market_slippage() + self.close_put_order.loss_on_market_slippage()
def expectation(self):
return EXPECTED_WIN_RATE * self.max_profit() - SINGLE_STOP_LOSS_PROB * max(self.close_call_order.expected_loss(), self.close_put_order.expected_loss()) - DOUBLE_STOP_LOSS_PROB * (self.close_call_order.expected_loss() + self.close_put_order.expected_loss())