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())