Overall Statistics
Total Orders
3
Average Win
0%
Average Loss
-0.88%
Compounding Annual Return
-10.174%
Drawdown
1.600%
Expectancy
-1
Start Equity
100000
End Equity
99156
Net Profit
-0.844%
Sharpe Ratio
-3.5
Sortino Ratio
-4.639
Probabilistic Sharpe Ratio
14.663%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
-0.054
Beta
0.202
Annual Standard Deviation
0.036
Annual Variance
0.001
Information Ratio
1.775
Tracking Error
0.134
Treynor Ratio
-0.631
Total Fees
$2.00
Estimated Strategy Capacity
$280000.00
Lowest Capacity Asset
SPY Y4D62Z9A4X9I|SPY R735QTJ8XC9X
Portfolio Turnover
0.06%
# region imports
from AlgorithmImports import *
# endregion

class CryingRedOrangeKitten(QCAlgorithm):
    def initialize(self):
        self.set_start_date(2022, 11, 22)
        self.set_end_date(2022, 12, 20)
        self.underlying = self.add_equity("SPY", extended_market_hours=True)
        self.option = self.add_option(self.underlying.symbol)
        self.option.set_filter(lambda u: u.calls_only().strikes(0,1).expiration(0,31))
        self.fixing_price = None
        
        # added for fixing extended hours issue
        self.set_security_initializer(MySecurityInitializer(self))
        self.schedule.on(self.date_rules.every_day("SPY"), self.time_rules.at(16, 0,), self.calling_fixing_price)

    # added for fixing extended hours issue
    def calling_fixing_price(self) -> None:
        
        self.fixing_price = self.underlying.close
        self.Debug(f"fixing price at {self.Time} for {self.fixing_price}")

    def on_data(self, data: Slice):
        if not self.portfolio.invested:
            chain = data.option_chains.get(self.option.symbol)
            if chain:
                self.market_order(list(chain)[0].symbol, 1)

class MySecurityInitializer(BrokerageModelSecurityInitializer):

    def __init__(self, algorithm) -> None:
        super().__init__(algorithm.brokerage_model, FuncSecuritySeeder(algorithm.get_last_known_prices))
        self.algorithm = algorithm

    def initialize(self, security: Security) -> None:
        super().initialize(security)
        if security.type == SecurityType.OPTION:
            # security.set_option_exercise_model(MyOptionExerciseModel(self.algorithm))
            option_exercise_model = MyOptionExerciseModel(self.algorithm)
            option_exercise_model.update_fixing_price(self.algorithm.fixing_price)
            security.set_option_exercise_model(option_exercise_model)

# This custom model implements the default model in LEAN (written in C#)
class MyOptionExerciseModel(DefaultExerciseModel):
    def __init__(self, algorithm):
        self.algorithm = algorithm
        self.fixing_price = None # added for fixing extended hours issue

    def update_fixing_price(self, price):
        self.fixing_price_during_exercise = price # added for fixing extended hours issue
        self.algorithm.Log(f"Updated fixing price to: {price}")

    def option_exercise(self, option: Option, order: OptionExerciseOrder) -> List[OrderEvent]:
        order_events = []

        underlying = option.underlying
        utc_time = Extensions.convert_to_utc(option.local_time, option.exchange.time_zone)

        # added for fixing extended hours issue
        in_the_money = option.is_auto_exercised(underlying.close)
        #in_the_money = (self.fixing_price_during_exercise >= option.strike_price) if option.right == OptionRight.CALL else (self.fixing_price_during_exercise <= option.strike_price)

        self.algorithm.Log(f"Exercising option: {option.symbol}, In-the-money: {in_the_money}, Fixing Price: {self.fixing_price}, Strike Price: {option.strike_price}, Origininal price {underlying.close}")

        is_assignment = in_the_money and option.holdings.is_short
        messages = Messages.DefaultExerciseModel

        order_event = OrderEvent(
            order.id,
            option.symbol,
            utc_time,
            OrderStatus.FILLED,
            Extensions.get_order_direction(order.quantity),
            0,
            order.quantity,
            OrderFee.ZERO,
            messages.contract_holdings_adjustment_fill_tag(in_the_money, is_assignment, option)
        )
        order_event.is_assignment = is_assignment
        order_event.is_in_the_money = in_the_money
        order_events.append(order_event)

        if in_the_money and option.exercise_settlement == SettlementType.PHYSICAL_DELIVERY:
            exercise_quantity = option.get_exercise_quantity(order.quantity);
            order_event =  OrderEvent(
                order.id,
                underlying.symbol,
                utc_time,
                OrderStatus.FILLED,
                Extensions.get_order_direction(exercise_quantity),
                option.strike_price,
                exercise_quantity,
                OrderFee.ZERO,
                messages.option_assignment if is_assignment else messages.option_exercise
            )
            order_event.is_in_the_money = True
            order_events.append(order_event)

        return order_events