Overall Statistics
Total Orders
640
Average Win
0.69%
Average Loss
-0.42%
Compounding Annual Return
130.162%
Drawdown
9.500%
Expectancy
0.633
Start Equity
100000
End Equity
228938.58
Net Profit
128.939%
Sharpe Ratio
3.528
Sortino Ratio
4.572
Probabilistic Sharpe Ratio
99.028%
Loss Rate
38%
Win Rate
62%
Profit-Loss Ratio
1.65
Alpha
0.709
Beta
0.463
Annual Standard Deviation
0.218
Annual Variance
0.048
Information Ratio
2.903
Tracking Error
0.22
Treynor Ratio
1.665
Total Fees
$8481.11
Estimated Strategy Capacity
$0
Lowest Capacity Asset
APLT X4CEBSOBHUN9
Portfolio Turnover
11.06%
from AlgorithmImports import *
import json
import math
import pandas as pd
from datetime import timedelta
from io import StringIO

class Form4TradingAlgorithm(QCAlgorithm):
    def Initialize(self):
        # self.csv_file_name = "StrategyA-2012-2023.csv"
        # self.csv_file_name = "StrategyA_2024.csv"
        # self.csv_file_name = "StrategyA_2024_2.csv"
        self.csv_file_name = "StrategyA_allInsiderTrades-2011-2023.csv"
        year = 2023
        self.SetStartDate(year, 1, 1)  # Start backtesting period
        self.SetEndDate(year, 12, 31)  # End backtesting period
        starting_capital = 100000
        self.SetCash(starting_capital)  # Initial capital for each year
        self.maxSimultaneousOpenPositions = 10
        self.maxInitialCapitalPerPosition = starting_capital / self.maxSimultaneousOpenPositions
        self.leverage = 1 # was 3: great results
        
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.Margin)
        self.stopLossThreshold = -0.15  # -15% stop loss

        self.openPositions = []  # Track open positions

        self.allInsiderTrades = self.LoadAllInsiderTrades()

        self.AddEquity("SPY", Resolution.Daily)
        self.SetBenchmark("SPY")

        # Add ticker to universe to receive pricing data
        for _, insiderTrade in self.allInsiderTrades.iterrows():
            ticker = insiderTrade['ticker']
            self.AddEquity(ticker, Resolution.Daily)


        self.Schedule.On(self.DateRules.EveryDay("SPY"), # Align with the trading calendar of SPY, excluding weekends and holidays
                        # SPY: Symbol of the security whose market open time is being used as a reference
                        # 0: This specifies how many minutes after the market opens the function should be triggered.
                        self.TimeRules.AfterMarketOpen("SPY", 1), 
                        self.OnMarketOpen)


    # def OnData(self, data):
    def OnMarketOpen(self):
        # self.Debug(f"Market open logic executed at: {self.Time}")
        # Check open positions
        self.CheckOpenPositions()

        # Get filings for the previous trading day
        todaysInsiderTrades = self.GetTodaysInsiderTrades()

        if len(todaysInsiderTrades) == 0:
            return

        tickersToSkip = todaysInsiderTrades[todaysInsiderTrades['ticker'].apply(lambda ticker: self.Portfolio[ticker].Invested)]
        if len(tickersToSkip) > 0:
            tickersToSkip = tickersToSkip['ticker'].to_list()
            self.Debug(f"{self.Time} - Skipping tickers: {tickersToSkip}")

        # Filter insider trades to only include tickers that are not already 
        # included as long open position in portfolio
        todaysInsiderTrades = todaysInsiderTrades[~todaysInsiderTrades['ticker'].apply(lambda ticker: self.Portfolio[ticker].Invested)]

        if len(todaysInsiderTrades) == 0:
            return

        # Calculate available slots and capital allocation
        availableSlots = self.maxSimultaneousOpenPositions - len(self.openPositions)

        if availableSlots <= 0:
            return

        availableCash = self.Portfolio.Cash
        availableBuyingPower = availableCash + (self.Portfolio.TotalPortfolioValue - availableCash) * (self.leverage - 1)
        positionSize = availableBuyingPower / availableSlots
        positionSize = min(positionSize, self.maxInitialCapitalPerPosition)

        for _, insiderTrade in todaysInsiderTrades[:availableSlots].iterrows():
            # Fetch current stock price
            ticker = insiderTrade['ticker']
            openPrice = self.Securities[ticker].Open

            if openPrice is None or openPrice <= 0:
                continue

            numShares = math.floor(positionSize / openPrice)
            
            if numShares > 0:
                # self.Debug(f"Buying {ticker} at {self.Time} for ${openPrice} ({numShares} shares)")
                # self.MarketOnOpenOrder(ticker, numShares)
                self.MarketOrder(ticker, numShares)

                self.openPositions.append({
                    "Symbol": ticker,
                    "EntryDate": self.Time,
                    "EntryPrice": openPrice,
                    "NumShares": numShares,
                    "StopLossPrice": openPrice * (1 + self.stopLossThreshold)
                })


    def CheckOpenPositions(self):
        for position in self.openPositions[:]:
            symbol = position["Symbol"]
            stopLossPrice = position["StopLossPrice"]
            entryDate = position["EntryDate"]
            numShares = position["NumShares"]

            currentPrice = self.Securities[symbol].Open

            if currentPrice is None:
                continue

            # calender days    
            # daysHeld = (self.Time - entryDate).days
            # business days
            daysHeld = len(pd.date_range(start=entryDate, end=self.Time, freq='B')) - 1

            if currentPrice <= stopLossPrice or daysHeld >= 5:
                # self.Liquidate(symbol) # Market order at the date/time this function runs
                self.MarketOrder(symbol, -1 * numShares)
                self.openPositions.remove(position)


    def GetTodaysInsiderTrades(self):
        today = self.Time.date()
        return self.allInsiderTrades[self.allInsiderTrades['date'] == pd.Timestamp(today)]


    def LoadAllInsiderTrades(self):
        try:
            # data = self.ObjectStore.ReadBytes(self.objectStoreKey)
            # if data:
                # filings = json.loads(data)
                # return [f for f in filings if f['Date'] == (self.Time - timedelta(days=1)).strftime('%Y-%m-%d')]
            csv_data = self.ObjectStore.Read(self.csv_file_name)
            # df = pd.read_csv(StringIO(csv_data), usecols=['index', 'issuerTicker', 'filedAt'], sep=",")
            df = pd.read_csv(StringIO(csv_data), sep=",")
            df.rename(columns={'index': 'date', 'issuerTicker': 'ticker'}, inplace=True)
            df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
            if "filedAt" in df.columns:
                self.Debug("Sorting insiders by 'filedAt'")
                df.sort_values(by='filedAt', inplace=True)
            else:
                self.Debug("Sorting insiders by 'date'")
                df.sort_values(by='date', inplace=True)

            self.Debug(f"Loaded {len(df):,.0f} insider trades")
            return df
        except Exception as e:
            self.Debug(f"Error reading CSV: {e}")
            return