Overall Statistics |
Total Trades 38 Average Win 0.88% Average Loss -0.86% Compounding Annual Return 86.673% Drawdown 9.400% Expectancy -0.145 Net Profit 6.108% Sharpe Ratio 2.126 Sortino Ratio 2.896 Probabilistic Sharpe Ratio 61.610% Loss Rate 58% Win Rate 42% Profit-Loss Ratio 1.02 Alpha 0.41 Beta 0.911 Annual Standard Deviation 0.274 Annual Variance 0.075 Information Ratio 1.512 Tracking Error 0.26 Treynor Ratio 0.638 Total Fees $20.00 Estimated Strategy Capacity $800000000.00 Lowest Capacity Asset QQQ 32E37ONCZE7TY|QQQ RIWIV7K5Z9LX Portfolio Turnover 163.41% |
from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Data.Custom import * from QuantConnect.Orders import * from QuantConnect.Securities.Option import OptionPriceModels from datetime import timedelta, datetime, date, timedelta import csv import pytz import io from io import StringIO import pandas as pd from QuantConnect import OptionRight from QuantConnect import Resolution from QuantConnect import DataNormalizationMode ''' REFERENCE: https://www.quantconnect.com/docs/v2/writing-algorithms/trading-and-orders/order-types/combo-leg-limit-orders https://www.quantconnect.com/docs/v2/writing-algorithms/securities/asset-classes/us-equity/handling-data ''' class SPXWTradingAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2023, 12, 29) #self.SetEndDate(2024, 1, 31) self.SetCash(10000) self.UniverseSettings.Asynchronous = True # spx = self.AddIndex("SPX").Symbol # option = self.AddIndexOption(spx, "SPXW") # SPXW is the target non-standard contract self.resolution = Resolution.Minute self.equity = self.AddEquity('QQQ', self.resolution) self.equity.SetDataNormalizationMode(DataNormalizationMode.Raw) self.option = self.AddOption(self.equity.Symbol, self.resolution) self.option.SetFilter(self.option_chain_filter) #.SetFilter(-5, 5, 0, 10) self.symbol = self.option.Symbol # trigger every 60 secs - during trading hours self.seconds_delta = 60 # self.Schedule.On(self.DateRules.EveryDay(self.symbol), # self.TimeRules.Every(timedelta(seconds=self.seconds_delta)), # self.Trade) self.is_backtest = True self.relevant_row = None self.sheet_url = 'https://docs.google.com/spreadsheets/d/1wwadCU8msu6FEUJt1ANoZS2qMO2MWiheARrdm7zaQlM/export?format=csv' self.full_sheet = None self.last_trade_date = None # self.tz = pytz.timezone('America/New_York') def option_chain_filter(self, option_chain): return option_chain.IncludeWeeklys()\ .Strikes(-100, 100)\ .Expiration(timedelta(0), timedelta(1)) def retry_with_backoff(self, fn, retries=10, min_backoff=5): x = 0 while True: try: return fn() except: if x == retries: tprint("raise") raise sleep = min_backoff * x tprint(f"sleep: {sleep}") time.sleep(sleep) x += 1 def download_sheet_data(self): csv_string = self.Download(self.sheet_url) df_sheet = pd.read_csv(StringIO(csv_string), sep=",") return df_sheet def fetch_sheet_data_update(self, current_time): """Download google sheet data and return row for the requested date""" # csv_string = self.Download(self.sheet_url) # df_sheet = pd.read_csv(StringIO(csv_string), sep=",") if (self.full_sheet is None) or (not self.is_backtest): self.full_sheet = self.retry_with_backoff(self.download_sheet_data) self.Debug(f'Downloaded Sheet has {len(self.full_sheet)} rows') self.full_sheet['trigger_datetime'] = self.full_sheet['Trigger Time'].apply(lambda x: datetime.fromtimestamp(int(x))) # , tz=self.tz prev_cutoff = current_time - timedelta(seconds=self.seconds_delta) mask = self.full_sheet['trigger_datetime'].between(prev_cutoff, current_time, inclusive='left') next_trade = self.full_sheet[mask] if len(next_trade) == 1: self.Debug(f'Found a trade between {prev_cutoff} and {current_time}') return next_trade.squeeze().to_dict() elif len(next_trade) > 1: self.Debug(f'Multiple trades in sheet between {prev_cutoff} and {current_time}') return else: self.Debug(f'No trades found in sheet') return def get_right_for_option_type(self, option_type): """Map option type strings to QC `right` [C -> 0; P -> 1]""" if option_type =='C': return 0 elif option_type == 'P': return 1 else: self.Debug("Invalid option type: " + option_type) def get_nearest_contract(self, opt_chain, strike_threshold, option_type): """Select the contract with the largest strike less than the threshold for the option type """ right = self.get_right_for_option_type(option_type) chosen_contract = None for x in opt_chain: if x.Right == right and x.Strike <= strike_threshold: if chosen_contract is None or chosen_contract.Strike < strike_threshold: chosen_contract = x if chosen_contract is not None: self.Debug(f"For {strike_threshold=} {option_type=}: using {chosen_contract.Strike}") else: self.Debug(f"Could not find any contract for {strike_threshold=} {option_type=}") return chosen_contract def compute_mid(self, sec): return 0.5 * (sec.BidPrice + sec.AskPrice) def get_side_multiplier(self, side: str) -> int: if side in ['BUY', 'B']: return 1 elif side in ['SELL', 'S']: return -1 else: self.Debug("Unknown side: " + side) return def create_leg(self, opt_chain, trade_details, leg_num) -> float: assert leg_num in (1, 2) # Only two legs supported right now contract = self.get_nearest_contract(opt_chain, trade_details[f'Strike {leg_num}'], trade_details[f'Right {leg_num}']) if contract is None: return None, None mult = self.get_side_multiplier(trade_details[f'Action {leg_num}']) contribution_to_limit = self.compute_mid(contract) * mult leg = Leg.Create(contract.Symbol, mult) # TODO: allow for different qty across legs return leg, contribution_to_limit def GenerateTrade(self, slice): if self.IsWarmingUp: return self.Debug(f'Triggered at {self.Time}') optionchain = slice.OptionChains.get(self.symbol) if optionchain is None: self.Debug(f"Current option chain does not contain {self.symbol}. Skipping.") return trade_details = self.fetch_sheet_data_update(self.Time) if trade_details is None: return # expiry is same as current date for 0DTE - check timezone of remote host expiry = datetime.strptime(str(trade_details['TWS Contract Date']), '%Y%m%d') contracts = [i for i in optionchain if i.Expiry == expiry] if len(contracts) == 0: self.Debug(f"Not enough option contracts for {self.symbol} and {expiry=}") return quantity = int(trade_details['Order Quantity']) legs = [] limit_prc = 0 for leg_num in (1, 2): this_leg, this_limit_prc = self.create_leg(contracts, trade_details, leg_num) if this_leg is None: self.Debug(f'Skipping because no leg found for {leg_num=}') return legs.append(this_leg) limit_prc += this_limit_prc self.ComboLimitOrder(legs, quantity=quantity, limitPrice=limit_prc) self.last_trade_date = self.Time.date() self.Debug(f'Generated order with {limit_prc=}') # TODO: make limit price competeitive by modifying the combo order limit price every few seconds def OnOrderEvent(self, orderEvent): if orderEvent.Status == OrderStatus.Filled: self.Debug("Order filled: " + str(orderEvent)) def OnData(self, slice): if self.last_trade_date != self.Time.date(): self.GenerateTrade(slice) else: open_orders = self.Transactions.GetOpenOrders() if len(open_orders) > 0: self.refresh_order_price(open_orders) # TODO: increase frequency for refresh def refresh_order_price(self, open_orders): pass # TODO