Overall Statistics
Total Orders
291
Average Win
11.52%
Average Loss
-7.79%
Compounding Annual Return
9.233%
Drawdown
41.700%
Expectancy
-0.638
Start Equity
100000
End Equity
388092.83
Net Profit
288.093%
Sharpe Ratio
0.333
Sortino Ratio
0.314
Probabilistic Sharpe Ratio
0.228%
Loss Rate
85%
Win Rate
15%
Profit-Loss Ratio
1.48
Alpha
0
Beta
0
Annual Standard Deviation
0.197
Annual Variance
0.039
Information Ratio
0.424
Tracking Error
0.197
Treynor Ratio
0
Total Fees
$9443.53
Estimated Strategy Capacity
$0
Lowest Capacity Asset
UDN YTG30NYDTWPY|UDN TQBX2PUC67OL
Portfolio Turnover
0.06%
#region imports
from AlgorithmImports import *
from abc import ABC, abstractmethod
#endregion

class Charts(ABC):
    def __init__(
        self, 
        algo: QCAlgorithm, 
        chart_name: str, 
        series_list: List[Dict]
    ) -> None:

        self._algo: QCAlgorithm = algo
        self._chart_name: str = chart_name
        self._series_list: List[Dict] = series_list

        self._add_chart()
        self._scheduler()

    def _add_chart(self) -> None:
        chart = Chart(self._chart_name)
        self._algo.add_chart(chart)

        for series in self._series_list:
            chart.AddSeries(Series(series['name'], series['type'], series['unit']))

    def _scheduler(self) -> None:
        algo: QCAlgorithm = self._algo
        algo.schedule.on(
            algo.date_rules.every_day('SPY'), 
            algo.time_rules.before_market_close('SPY', 0), 
            self._update_chart
        )

    @abstractmethod
    def _update_chart(self):
        pass

class Benchmark(Charts):
    def __init__(
        self, 
        algo: QCAlgorithm, 
        benchmark_symbol: Symbol,
        init_cash: int
    ) -> None:

        series_list: List[Dict] = [
            {
            'name': f'Benchmark {benchmark_symbol.value}', 
            'type': SeriesType.LINE, 
            'unit': '$'
            }
        ]

        self._benchmark_values: List[float] = []
        self._benchmark_symbol: Symbol = benchmark_symbol
        self._init_cash: int = init_cash

        super().__init__(algo = algo, chart_name = 'Strategy Equity', series_list = series_list)
    
    def _update_chart(self):
        # wait for available data
        if not self._algo.portfolio.invested:
            return

        # print benchmark in main equity plot
        mkt_price_df: DataFrame = self._algo.history(self._benchmark_symbol, 2, Resolution.DAILY)
        if not mkt_price_df.empty:
            benchmark_price: float = mkt_price_df['close'].unstack(level= 0).iloc[-1]
            if len(self._benchmark_values) == 2:
                self._benchmark_values[-1] = benchmark_price
                benchmark_perf: float = self._init_cash * (self._benchmark_values[-1] / self._benchmark_values[0])
                self._algo.plot(self._chart_name, self._series_list[0]['name'], benchmark_perf)
            else:
                self._benchmark_values.append(benchmark_price)
# region imports
from AlgorithmImports import *
from enum import Enum
from dataclasses import dataclass
from charts import Benchmark
# endregion

class OptionTradeFunction(Enum):
    BUY = 1
    SELL = 2

@dataclass
class OptionLegSetup():
    trade_fn: OptionTradeFunction
    option_right: OptionRight
    dte: int
    otm: float

class OptionHedge(QCAlgorithm):

    _percentage_traded: float = 1.
    _DTE: int = 30
    _OTM: float = 0.01
    _benchmark_ticker: str = 'SPY'
    
    _hedge_ticker_setup: Dict[str, OptionLegSetup] = {
        'UDN' : OptionLegSetup(OptionTradeFunction.BUY, OptionRight.CALL, _DTE, _OTM), 
        'FXB' : OptionLegSetup(OptionTradeFunction.BUY, OptionRight.PUT, _DTE, _OTM) 
    }

    def initialize(self):
        self._init_cash: int = 100_000
        self.set_start_date(2010, 1, 1)
        self.set_cash(self._init_cash)

        self._market: Symbol = self.add_equity('SPY', Resolution.MINUTE).symbol

        # get last known price after subscribing option contract
        self.set_security_initializer(
            CustomSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))
        )
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        self.settings.minimum_order_margin_portfolio_percentage = 0
        self.settings.daily_precise_end_time = False

        # option storage
        self._subscribed_contracts: Dict[Symbol, Optional[Symbol]] = {}

        self._benchmark_symbol: Symbol = self.add_equity(self._benchmark_ticker, Resolution.MINUTE).symbol

        for ticker in self._hedge_ticker_setup:
            eq: Equity = self.add_equity(ticker, Resolution.MINUTE)
            eq.set_data_normalization_mode(DataNormalizationMode.RAW)
            self._subscribed_contracts[eq.symbol] = None

        self._contracts_added: Set = set()
        self._recent_day: int = -1

        # charting
        Benchmark(self, self._benchmark_symbol, self._init_cash)

    def on_data(self, slice: Slice) -> None:
        if self.is_warming_up:
            return
        
        if not self.is_market_open(self._market):
            return
        
        # once a day execution
        if self._recent_day != self.time.day:
            self._recent_day = self.time.day

            # trade options
            self._option_strategy(self._percentage_traded)

    def _option_strategy(self, percentage_traded: float) -> None:
        # contract is assigned and it is about to expire -> remove contract
        for symbol, c in self._subscribed_contracts.items():
            if c is not None and self.time + timedelta(days=1) >= c.id.date:
                self.remove_option_contract(c)
                self._subscribed_contracts[symbol] = None

        for symbol, c in self._subscribed_contracts.items():
            if c is None:
                self._subscribed_contracts[symbol] = self._filter_options(
                    symbol, 
                    self._hedge_ticker_setup[symbol.value].option_right, 
                    self._hedge_ticker_setup[symbol.value].dte, 
                    self._hedge_ticker_setup[symbol.value].otm
                )

            if c is not None and self.current_slice.contains_key(c):
                self._trade_option(c, percentage_traded, self._hedge_ticker_setup[symbol.value].trade_fn, tag='')

                # market buy and hold
                if not self.portfolio[self._benchmark_symbol].invested:
                    self.set_holdings(self._benchmark_symbol, 1.)

    def _filter_options(
        self, 
        symbol: Symbol, 
        option_right: OptionRight, 
        dte: int, 
        moneyness: float
    ) -> Optional[Symbol]:
    
        contracts: Iterable[Symbol] = self.option_chain_provider.get_option_contract_list(symbol, self.time)
        
        underlying_price: float = self.securities[symbol].price
        
        if option_right == OptionRight.CALL:
            options: List[Symbol] = [
                i for i in contracts if i.id.option_right == OptionRight.CALL and   
                i.id.strike_price >= underlying_price * (1 + moneyness) and
                i.id.date >= self.time + timedelta(days=dte)
            ]
        else:
            options: List[Symbol] = [
                i for i in contracts if i.id.option_right == OptionRight.PUT and   
                i.id.strike_price <= underlying_price * (1 - moneyness) and
                i.id.date >= self.time + timedelta(days=dte)
            ]

        if len(options) > 0:
            expiry: datetime.datetime = min(list(map(lambda x: x.id.date, options)))

            index: int = 0 if option_right == OptionRight.PUT else -1
            contract: Symbol = sorted([o for o in options if o.id.date == expiry], key = lambda x: underlying_price - x.id.strike_price)[index]

            # prevent multiple subscriptions
            if contract not in self._contracts_added:
                self._contracts_added.add(contract)
                option = self.add_option_contract(contract, Resolution.MINUTE)
                option.is_tradable = True

            return contract
        else:
            self.log(f'Connot find filtered options for {symbol}')
            return None

    def _trade_option(self, contract: Symbol, percentage_traded: float, option_trade_function: OptionTradeFunction, tag: str) -> None:
        if contract and not self.portfolio[contract].invested:
            trade_direction: int = 1 if option_trade_function == OptionTradeFunction.BUY else -1
            q: float = self._get_amount_as_fraction_of_portfolio(contract, percentage_traded)
            # q: float = self._get_amount_as_fraction_of_cash(contract, percentage_traded)
            # q: float = self.calculate_order_quantity(contract, percentage_traded)

            self.market_order(
                contract, 
                trade_direction * q, 
                tag=tag
            )

    def _get_amount_as_fraction_of_portfolio(self, option_symbol: Symbol, fraction: float) -> float:
        multiplier: int = self.securities[option_symbol].contract_multiplier
        target_notional: float = self.portfolio.total_portfolio_value * fraction
        notional_of_contract: float = multiplier * self.securities[option_symbol.underlying].price
        amount: float = target_notional / notional_of_contract

        return amount

    def _get_amount_as_fraction_of_cash(self, option_symbol: Symbol, fraction: float) -> float:
        holding_value: float = self.portfolio.cash
        multiplier: int = self.securities[option_symbol].contract_multiplier
        target_notional: float = holding_value * fraction
        notional_of_contract: float = multiplier * self.securities[option_symbol.underlying].price
        amount: float = target_notional / notional_of_contract

        return amount

class CustomSecurityInitializer(BrokerageModelSecurityInitializer):
    def __init__(self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder) -> None:
        super().__init__(brokerage_model, security_seeder)    

    def initialize(self, security: Security) -> None:
        super().initialize(security)

        # overwrite the price model
        if security.type == SecurityType.OPTION: # option type
            security.price_model = OptionPriceModels.black_scholes()
            security.set_option_assignment_model(NullOptionAssignmentModel())