Overall Statistics |
Total Orders 556 Average Win 1.15% Average Loss -1.11% Compounding Annual Return -6.157% Drawdown 53.300% Expectancy 0.007 Start Equity 100000 End Equity 93880.80 Net Profit -6.119% Sharpe Ratio 0.067 Sortino Ratio 0.046 Probabilistic Sharpe Ratio 17.467% Loss Rate 51% Win Rate 49% Profit-Loss Ratio 1.04 Alpha -0.074 Beta 0.794 Annual Standard Deviation 0.443 Annual Variance 0.196 Information Ratio -0.232 Tracking Error 0.435 Treynor Ratio 0.037 Total Fees $5930.32 Estimated Strategy Capacity $10000.00 Lowest Capacity Asset BCYP XLWJBTIOM7OL Portfolio Turnover 15.48% |
from AlgorithmImports import QCAlgorithm, Resolution import pandas as pd from datetime import timedelta from io import StringIO from pandas.tseries.offsets import BDay import math class RuleBasedEquityLongStrategy(QCAlgorithm): def Initialize(self): # Set the backtest timeframe and starting cash self.SetStartDate(2023, 1, 1) self.SetEndDate(2023, 12, 31) self.SetCash(100000) self.settings.daily_precise_end_time = True # Ensures accurate daily timing # Add benchmark self.AddEquity("SPY", Resolution.Daily) self.SetBenchmark("SPY") # Positions tracking self.positions = {} self.processed_tickers = {} # Tracks processed tickers per date # Load signal data from Object Store if self.ObjectStore.ContainsKey("StrategyA-2012-2023.csv"): try: csv_file = self.ObjectStore.Read("StrategyA-2012-2023.csv") self.signal_data = self.ParseCsv(csv_file) except Exception as e: self.Debug(f"Error reading CSV: {e}") self.signal_data = pd.DataFrame() else: self.Debug("2012-2023.csv not found in the Object Store.") self.signal_data = pd.DataFrame() # Validate signal data if self.signal_data.empty: self.Debug("No valid signal data. Algorithm will run without processing signals.") else: self.Debug(f"Loaded {len(self.signal_data)} signals.") self.capital_per_position = 10000 # Add all tickers in advance for _, signal in self.signal_data.iterrows(): ticker = signal['ticker'] self.AddEquity(ticker, Resolution.Daily) def OnData(self, data): today = self.Time.date() # Initialize processed tickers for today if not already done if today not in self.processed_tickers: self.processed_tickers[today] = set() positions_to_open = 0 # Process signals # Compute the number of signals for the current date daily_signals = self.signal_data[self.signal_data['date'] == pd.Timestamp(today)] positions_to_open = len(daily_signals) self.Debug(f"Number of signals for {today}: {positions_to_open}") if positions_to_open > 0: available_cash = self.Portfolio.Cash capital_per_position_today = available_cash / positions_to_open position_size = min(self.capital_per_position, capital_per_position_today) for _, signal in self.signal_data.iterrows(): ticker = signal['ticker'] signal_date = signal['date'] if today == signal_date.date() and ticker not in self.processed_tickers[today]: ticker_open = self.Securities[ticker].Open if ticker_open == 0 or ticker_open is None: self.Debug(f"Error on {ticker} Open.") if position_size > 0 and ticker_open > 0: qty = math.floor(position_size/ticker_open) self.MarketOnOpenOrder(ticker, qty) self.positions[ticker] = self.Time self.processed_tickers[today].add(ticker) # Mark ticker as processed # Manage exits to_remove = [] for ticker, entry_date in self.positions.items(): # Exit if holding period exceeds 5 business days if self.IsHoldingPeriodExceeded(entry_date, 5): self.Liquidate(ticker) to_remove.append(ticker) # Stop-loss logic current_price = self.Securities[ticker].Price entry_price = self.Portfolio[ticker].AveragePrice if current_price <= entry_price * 0.85: self.Liquidate(ticker) to_remove.append(ticker) # Remove exited positions for ticker in to_remove: try: del self.positions[ticker] except KeyError: continue def IsHoldingPeriodExceeded(self, entry_date, num_business_days): # Calculate the expected exit date exit_date = pd.date_range(entry_date, periods=num_business_days + 1, freq=BDay())[num_business_days] return self.Time.date() >= exit_date.date() def ParseCsv(self, csv_data): try: # Parse CSV df = pd.read_csv(StringIO(csv_data), usecols=['index', 'issuerTicker'], sep=",") df.rename(columns={'index': 'date', 'issuerTicker': 'ticker'}, inplace=True) df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d') df.sort_values(by='date', inplace=True) return df except Exception as e: self.Debug(f"CSV Parsing Error: {e}") return pd.DataFrame()