Overall Statistics
Total Orders
570
Average Win
2.44%
Average Loss
-1.12%
Compounding Annual Return
61.779%
Drawdown
53.800%
Expectancy
1.032
Start Equity
100000
End Equity
1676247.61
Net Profit
1576.248%
Sharpe Ratio
1.316
Sortino Ratio
1.667
Probabilistic Sharpe Ratio
65.705%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
2.18
Alpha
0.309
Beta
1.309
Annual Standard Deviation
0.344
Annual Variance
0.118
Information Ratio
1.264
Tracking Error
0.271
Treynor Ratio
0.346
Total Fees
$14189.90
Estimated Strategy Capacity
$3000.00
Lowest Capacity Asset
SENEB R735QTJ8XC9X
Portfolio Turnover
1.14%
# Import all the required libraries and functionalities from QuantConnect
from AlgorithmImports import *
import numpy as np

# Define the main algorithm class that extends QCAlgorithm
class SmallCapNetCurrentAssetValueEffect(QCAlgorithm):
    def Initialize(self) -> None:
        # Initialization and configuration of the algorithm
        self.SetStartDate(2019, 1, 1)  # Set the backtest start date
        self.SetCash(100000)  # Set the initial capital for the algorithm

        # Set universe settings
        self.UniverseSettings.Leverage = 1.5  # Define the leverage for the securities
        self.UniverseSettings.Resolution = Resolution.Daily  # Use daily data resolution

        # Configure universe selection based on a custom fundamental function
        self.AddUniverse(self.FundamentalFunction)

        # Set some portfolio and algorithm settings
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
        self.Settings.daily_precise_end_time = False

        # Variables for filtering and selecting securities
        self.fundamental_count = 3000  # Maximum number of securities to consider
        self.market = 'usa'  # Market filter for USA
        self.country_id = 'USA'  # Country ID filter for USA
        self.fin_sector_code = 103  # Exclude financial sector based on sector code
        self.ncav_threshold = 1.75  # Threshold for Net Current Asset Value

        # Define a range for small-cap market capitalization (between $300 million and $2 billion)
        self.small_cap_lower_threshold = 0.1e9  # $100 million
        self.small_cap_upper_threshold = 0.75e9  # $750 miilio

        # List to store selected long symbols
        self.long_symbols = []

        # Define the months when rebalancing should occur
        self.rebalance_months = [1, 4, 7, 10]  # Rebalance in January, April, July, and October

        # A flag to trigger universe selection
        self.selection_flag = True

        # Add benchmark securities (SPY and GLD)
        self.spy = self.AddEquity('SPY', Resolution.Daily).Symbol  # S&P 500 ETF
        self.gld = self.AddEquity('GLD', Resolution.Daily).Symbol  # Gold

        # Calculate 100-day and 200-day SMAs for SPY
        self.spy_sma_100 = self.SMA(self.spy, 100, Resolution.Daily)
        self.spy_sma_200 = self.SMA(self.spy, 200, Resolution.Daily)
        self.previous_crossover = None  # Variable to track the previous crossover state

        # Warm up the data to initialize indicators
        self.SetWarmUp(200)  # Warm-up period of 200 days to ensure SMAs are ready

        # Schedule events for rebalancing
        self.Schedule.On(
            self.DateRules.MonthStart(self.spy), 
            self.TimeRules.AfterMarketOpen(self.spy), 
            self.Selection
        )  # Schedule rebalancing at the start of the defined months

        # Perform an initial selection at the start of the algorithm
        self.Schedule.On(
            self.DateRules.Today, 
            self.TimeRules.AfterMarketOpen(self.spy), 
            self.Selection
        )

    def FundamentalFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        # Return an unchanged universe if selection is not needed
        if not self.selection_flag:
            return Universe.Unchanged

        # Filter out securities based on various fundamental criteria
        filtered = [
            f for f in fundamental if f.HasFundamentalData
            and f.Market == self.market
            and f.CompanyReference.CountryId == self.country_id
            and f.AssetClassification.MorningstarSectorCode != self.fin_sector_code
            and not np.isnan(f.EarningReports.BasicAverageShares.TwelveMonths)
            and f.EarningReports.BasicAverageShares.TwelveMonths != 0
            and not np.isnan(f.MarketCap)
            and f.MarketCap >= self.small_cap_lower_threshold  # Small-cap lower bound
            and f.MarketCap <= self.small_cap_upper_threshold  # Small-cap upper bound
            and f.ValuationRatios.WorkingCapitalPerShare != 0
            and f.Volume > 0  # Ensure stocks have volume data
        ]

        # Sort the filtered securities by market cap in descending order and take the top fundamental_count
        sorted_by_market_cap = sorted(filtered, key=lambda f: f.MarketCap, reverse=True)[:self.fundamental_count]

        # Select securities with a Net Current Asset Value (NCAV) ratio above the threshold
        self.long_symbols = [
            x.Symbol for x in sorted_by_market_cap 
            if ((x.ValuationRatios.WorkingCapitalPerShare * x.EarningReports.BasicAverageShares.TwelveMonths) / x.MarketCap) > self.ncav_threshold
        ]

        return self.long_symbols

    def OnData(self, slice: Slice) -> None:
        # Ensure SMAs are ready
        if self.IsWarmingUp:
            return

        # Check if SMAs are ready
        if not (self.spy_sma_100.IsReady and self.spy_sma_200.IsReady):
            return

        # Check for SMA crossovers
        current_crossover = self.spy_sma_100.Current.Value < self.spy_sma_200.Current.Value

        # If the 100 SMA crosses below the 200 SMA, invest fully in GLD
        if current_crossover and (self.previous_crossover is None or not self.previous_crossover):
            self.SetHoldings(self.gld, 1.0)

        # If the 100 SMA crosses above the 200 SMA, exit GLD
        elif not current_crossover and (self.previous_crossover is None or self.previous_crossover):
            self.Liquidate(self.gld)

        # Update the previous crossover state
        self.previous_crossover = current_crossover

        # If selection is not needed, exit the method
        if not self.selection_flag:
            return

        # Set the flag to False after selection is processed
        self.selection_flag = False

        # Create portfolio targets for each selected symbol and set holdings
        portfolio = [
            PortfolioTarget(symbol, 1 / len(self.long_symbols))
            for symbol in self.long_symbols if slice.ContainsKey(symbol) and slice[symbol] is not None
        ]
        self.SetHoldings(portfolio, True)  # Adjust holdings in the portfolio
        self.long_symbols.clear()  # Clear the list of long symbols

    def Selection(self) -> None:
        # Rebalance if it's one of the rebalance months or at the start date
        if self.Time.month in self.rebalance_months or self.Time == self.StartDate:
            self.selection_flag = True

    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        # Set leverage and fee model for newly added securities
        for security in changes.AddedSecurities:
            security.SetLeverage(1.5)
            security.SetFeeModel(InteractiveBrokersFeeModel())

# Custom fee model for Interactive Brokers
class InteractiveBrokersFeeModel(FeeModel):
    def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
        # Calculate commission based on Interactive Brokers' fee structure
        commission_per_share = 0.005  # $0.005 per share
        min_commission = 1.00  # Minimum commission per trade
        max_commission = 0.5 / 100 * parameters.Order.AbsoluteQuantity * parameters.Security.Price  # 0.5% of trade value

        # Determine the final commission amount
        commission = max(min_commission, min(commission_per_share * parameters.Order.AbsoluteQuantity, max_commission))
        return OrderFee(CashAmount(commission, "USD"))