Overall Statistics
Total Trades
16
Average Win
0%
Average Loss
-0.19%
Compounding Annual Return
-12.538%
Drawdown
1.500%
Expectancy
-1
Net Profit
-1.483%
Sharpe Ratio
-7.911
Probabilistic Sharpe Ratio
0.000%
Loss Rate
100%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
-0.128
Beta
-0.014
Annual Standard Deviation
0.016
Annual Variance
0
Information Ratio
-2.006
Tracking Error
0.122
Treynor Ratio
9.437
Total Fees
$78.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
XLE YCZBWHRT5492|XLE RGRPZX100F39
Portfolio Turnover
0.30%
# region imports
from datetime import timedelta
from AlgorithmImports import *
import pandas as pd
from io import StringIO
import pytz

# endregion


class HyperActiveBlueRat(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2023, 9, 29)
        self.SetCash(100000)

        # parameters
        self.CPT = self.GetParameter("CPT", 1000.0)
        self.SLP = self.GetParameter("SLP", 25.0)
        self.EBE = self.GetParameter("EBE", 120.0)
        self.THEP = self.GetParameter("THEP", 33.0)
        self.PMSP = self.GetParameter("PMSP", 50.0)
        self.OTL = self.GetParameter("OTL", 5)
        self.VP = self.GetParameter("VP", 15)

        csv = self.Download(
            "https://www.dropbox.com/scl/fi/nmhib4wgzybsi7klinm73/UOA-Signals_2023_10_27.csv?rlkey=ktunyhjpgqbho6a2fhnsot3xf&dl=1"
        )
        self.df = pd.read_csv(StringIO(csv))

        self.symbols = []
        self.options_symbols = []
        self.signals = {}
        self.entry_order = {}
        self.take_profit = {}
        self.stop_loss = {}
        self.look_for_trigger = []
        self.look_for_validation = []
        self.SMAs = {}

        self.AddEquity("SPY", dataNormalizationMode=DataNormalizationMode.Raw)
        for i in range(len(self.df)):
            if self.df.iloc[i, 2] not in self.symbols:
                try:
                    symbol = self.AddEquity(
                        self.df.iloc[i, 2],
                        Resolution.Minute,
                        dataNormalizationMode=DataNormalizationMode.Raw,
                    ).Symbol
                    self.SMAs[symbol.Value] = self.SMA(
                        symbol, self.VP, Resolution.Minute
                    )
                    options_symbol = self.AddSecurity(
                        SecurityType.Option, symbol, Resolution.Minute
                    ).Symbol
                    self.options_symbols.append(options_symbol)
                    self.symbols.append(self.df.iloc[i, 2])
                except:
                    self.Debug(f"Error adding {self.df.iloc[i, 2]}")

        self.fill_signals()
        self.Schedule.On(
            self.DateRules.EveryDay("SPY"),
            self.TimeRules.BeforeMarketClose("SPY", 1),
            self.market_close,
        )
        # TODO: Add consolidators
        # TODO: Add Warmup

    def OnData(self, data: Slice):
        self.check_for_expiry()
        self.check_for_validation()
        # check for exits
        for order_ticket in list(self.entry_order.values()):
            if (
                order_ticket.Status == OrderStatus.Submitted
                and order_ticket.Time.astimezone(pytz.timezone("US/Eastern")).replace(
                    tzinfo=None
                )
                + timedelta(minutes=self.OTL)
                <= self.Time
            ):
                order_ticket.Cancel("OTL")

        for symbol in self.symbols:
            for signal in self.signals.get(self.Time.strftime("%m/%d/%Y %H:%M"), []):
                if signal["symbol"] == symbol:
                    contracts = self.OptionChainProvider.GetOptionContractList(
                        symbol, data.Time
                    )
                    if contracts != None:
                        calls = [
                            x for x in contracts if x.ID.OptionRight == OptionRight.Call
                        ]
                        puts = [
                            x for x in contracts if x.ID.OptionRight == OptionRight.Put
                        ]
                        expiry = signal["expiration"]

                        if signal["Alert"] in ("Bullish Call Buying", "Bullside Idea"):
                            contract = None
                            current_price = self.Securities[symbol].Price
                            for call in calls:
                                if call.ID.Date.strftime("%d-%b") != expiry:
                                    continue
                                if contract is None:
                                    contract = call
                                elif (
                                    call.ID.StrikePrice > contract.ID.StrikePrice
                                    and call.ID.StrikePrice < current_price
                                ):
                                    contract = call

                        if signal["Alert"] in ("Bearish Put Buying", "Bearside Idea"):
                            contract = None
                            current_price = self.Securities[symbol].Price
                            for put in puts:
                                if put.ID.Date.strftime("%d-%b") != expiry:
                                    continue
                                if contract is None:
                                    contract = put
                                elif (
                                    put.ID.StrikePrice < contract.ID.StrikePrice
                                    and put.ID.StrikePrice > current_price
                                ):
                                    contract = put

                        if contract is not None:
                            self.AddOptionContract(contract, Resolution.Minute)
                            self.look_for_trigger.append((signal, contract))
        self.check_for_trigger()

    def OnOrderEvent(self, orderEvent: OrderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return

        if self.entry_order[orderEvent.Symbol].OrderId == orderEvent.OrderId:
            self.take_profit[orderEvent.Symbol] = self.LimitOrder(
                orderEvent.Symbol,
                -orderEvent.Quantity,
                round(orderEvent.FillPrice + orderEvent.FillPrice * self.THEP / 100, 2),
                "TP",
            )
            self.stop_loss[orderEvent.Symbol] = self.StopMarketOrder(
                orderEvent.Symbol,
                -orderEvent.Quantity,
                round(orderEvent.FillPrice * (1 - self.SLP / 100), 2),
                "SL",
            )
        elif self.take_profit[orderEvent.Symbol].OrderId == orderEvent.OrderId:
            self.stop_loss[orderEvent.Symbol].Cancel()

        elif self.stop_loss[orderEvent.Symbol].OrderId == orderEvent.OrderId:
            self.take_profit[orderEvent.Symbol].Cancel()

    def make_order(self, symbol: OptionSymbol):
        underlying_price = self.Securities[symbol.Value.split(" ")[0]].Price
        if symbol.ID.OptionRight == OptionRight.Put and underlying_price < self.PMSP:
            self.Log(f"Ignoring {symbol.Value} order as the price is < ${self.PMSP}")
            return
        if self.Securities.ContainsKey(symbol) is False:
            self.Log(f"couldn't find security {symbol} ignoring signal")
            return
        if self.Portfolio[symbol.Value].Invested:
            self.Log(f"symbol already invested {symbol.Value} ignoring signal")
            return
        if (
            symbol in self.entry_order
            and self.entry_order[symbol].Status == OrderStatus.Submitted
        ):
            self.Log(f"symbol already in entry order {symbol.Value} ignoring signal")
            return
        bid = self.Securities[symbol.Value].BidPrice
        ask = self.Securities[symbol.Value].AskPrice
        if bid == 0:
            self.Log(f"couldn't find bid/ask for {symbol} ignoring signal")
            return
        lmt_price = round(((bid + ask) / 2 + ask) / 2, 2)
        qty = self.CPT // (lmt_price * 100)
        if qty == 0 or lmt_price == 0:
            return
        self.entry_order[symbol] = self.LimitOrder(symbol, qty, lmt_price)

    def market_close(self):
        self.Liquidate(tag="End of day")
        self.look_for_validation.clear()
        self.look_for_trigger.clear()

    def check_for_expiry(self):
        for position in self.Portfolio.items():
            symbol = position[0]
            qty = position[1].AbsoluteQuantity
            if qty > 0:
                expiry = symbol.ID.Date
                expiry.replace(hour=16)
                if self.Time + timedelta(minutes=self.EBE) >= expiry:
                    self.Liquidate(symbol, "EBE hit")

    def check_for_trigger(self):
        for item in list(self.look_for_trigger):
            if (
                item[1].ID.OptionRight == OptionRight.Call
                and self.Securities[item[0]["symbol"]].High >= item[0]["Trigger price"]
            ) or (
                item[1].ID.OptionRight == OptionRight.Put
                and self.Securities[item[0]["symbol"]].Low <= item[0]["Trigger price"]
            ):
                self.look_for_validation.append((item, self.Time))
                self.look_for_trigger.remove(item)

    def check_for_validation(self):
        for item in list(self.look_for_validation):
            if item[1] + timedelta(minutes=self.VP) <= self.Time and (
                (
                    item[0][1].ID.OptionRight == OptionRight.Call
                    and self.SMAs[item[0][0]["symbol"]].Current.Value
                    >= item[0][0]["Trigger price"]
                )
                or (
                    item[0][1].ID.OptionRight == OptionRight.Put
                    and self.SMAs[item[0][0]["symbol"]].Current.Value
                    <= item[0][0]["Trigger price"]
                )
            ):
                self.make_order(item[0][1])
                self.look_for_validation.remove(item)

    def fill_signals(self):
        for i in range(len(self.df)):
            date = self.df.iloc[i, 1]
            if date not in self.signals:
                self.signals[date] = []
            self.signals[date].append(
                {
                    "symbol": self.df.iloc[i, 2],
                    "Alert": self.df.iloc[i, 3],
                    "Stock price": float(self.df.iloc[i, 5]),
                    "Trigger price": float(self.df.iloc[i, 6]),
                    "expiration": self.df.iloc[i, 8],
                }
            )