Overall Statistics
Total Orders
10
Average Win
0%
Average Loss
0%
Compounding Annual Return
205.756%
Drawdown
0.800%
Expectancy
0
Start Equity
10000
End Equity
10374.27
Net Profit
3.743%
Sharpe Ratio
8.168
Sortino Ratio
37.678
Probabilistic Sharpe Ratio
86.790%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
1.742
Beta
-0.585
Annual Standard Deviation
0.158
Annual Variance
0.025
Information Ratio
2.828
Tracking Error
0.184
Treynor Ratio
-2.209
Total Fees
$23.64
Estimated Strategy Capacity
$160000.00
Lowest Capacity Asset
TLSA WZ7AA7WL40H1
Portfolio Turnover
4.15%
from AlgorithmImports import *

class MarketNeutralPennyStocksAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2024, 6, 1)
        self.set_end_date(2024, 7, 1)
        self.SetCash(10_000)
        self.SetSecurityInitializer(BrokerageModelSecurityInitializer(
            self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices)
        ))

        self.UniverseSettings.Resolution = Resolution.Daily

        self.momentum_indicators = {}
        self.lookback_period = 252
        self.num_coarse = 100
        self.num_fine = 100
        self.num_long = 5
        self.num_short = 5

        self.current_month = -1
        self.rebalance_flag = True
        self.SetWarmUp(self.lookback_period, Resolution.Daily)
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

    def CoarseSelectionFunction(self, coarse):
        if self.current_month == self.Time.month:
            return Universe.Unchanged

        self.rebalance_flag = True
        self.current_month = self.Time.month

        selected = sorted(
            [x for x in coarse if x.HasFundamentalData and x.Price < 1 and x.MarketCap < 1e9],
            key=lambda x: x.DollarVolume, 
            reverse=True
        )
        self.Debug(f"Coarse selected: {len(selected)} securities")
        return [x.Symbol for x in selected[:self.num_coarse]]

    def FineSelectionFunction(self, fine):
        selected = sorted(fine, key=lambda f: f.MarketCap, reverse=True)
        self.Debug(f"Fine selected: {len(selected)} securities")
        return [x.Symbol for x in selected[:self.num_fine]]

    def OnData(self, data):
        for symbol, momentum in self.momentum_indicators.items():
            if symbol in data and data[symbol] is not None:
                momentum.Update(self.Time, data[symbol].Close)

        if not self.rebalance_flag:
            return

        sorted_momentum = sorted(
            [k for k, v in self.momentum_indicators.items() if v.IsReady],
            key=lambda x: self.momentum_indicators[x].Current.Value, 
            reverse=True
        )
        self.Debug(f"Sorted momentum: {len(sorted_momentum)} securities")
        long_selected = sorted_momentum[:self.num_long]
        self.Debug(f"LONG {long_selected}")
        short_selected = sorted_momentum[-self.num_short:]
        self.Debug(f"SHORT {short_selected}")

        for symbol in list(self.Portfolio.Keys):
            if symbol not in long_selected and symbol not in short_selected:
                self.Liquidate(symbol, 'Not selected')

        initial_investment = 500
        additional_investment = 250

        for symbol in long_selected:
            current_investment = self.Portfolio[symbol].Invested
            if current_investment:
                self.MarketOnOpenOrder(symbol, additional_investment / data[symbol].Close)
                stop_price = data[symbol].Close * 1.18
                self.stop_limit_order(symbol, self.Portfolio[symbol].Quantity , stop_price, stop_price)
                self.Debug(f"Long {symbol} Price {data[symbol].Close} Qty {int(additional_investment / data[symbol].Close)} Stop price {stop_price}")
            else:
                self.MarketOnOpenOrder(symbol, initial_investment / data[symbol].Close)
                stop_price = data[symbol].Close * 1.18
                self.stop_limit_order(symbol, self.Portfolio[symbol].Quantity , stop_price, stop_price)
                self.Debug(f"Long {symbol} Price {data[symbol].Close} Qty {int(initial_investment / data[symbol].Close)} Stop price {stop_price}")

        for symbol in short_selected:
            current_investment = self.Portfolio[symbol].Invested
            if current_investment:
                if self.securities[symbol].is_tradable:
                    self.MarketOnOpenOrder(symbol, - int(additional_investment / data[symbol].Close))
                    stop_price = data[symbol].Close * 1.02
                    self.stop_limit_order(symbol, self.Portfolio[symbol].Quantity , stop_price, stop_price)
                    self.Debug(f"Short {symbol} Price {data[symbol].Close} Qty {- int(additional_investment / data[symbol].Close)} Stop price {stop_price}")
            else:
                if self.securities[symbol].is_tradable:
                    self.MarketOnOpenOrder(symbol, - int(initial_investment / data[symbol].Close))
                    stop_price = data[symbol].Close * 1.02
                    self.stop_limit_order(symbol, self.Portfolio[symbol].Quantity , stop_price, stop_price)
                    self.Debug(f"Short {symbol} Price {data[symbol].Close} Qty {- int(initial_investment / data[symbol].Close)} Stop price {stop_price}")
        
        self.rebalance_flag = False

    def OnSecuritiesChanged(self, changes):
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            if symbol in self.momentum_indicators:
                self.momentum_indicators.pop(symbol)
                self.Liquidate(symbol, 'Removed from universe')

        for security in changes.AddedSecurities:
            symbol = security.Symbol
            if symbol not in self.momentum_indicators:
                self.momentum_indicators[symbol] = MomentumPercent(self.lookback_period)
                history = self.History(symbol, self.lookback_period, Resolution.Daily)
                
                # Check if history data is available for the symbol
                if not history.empty and symbol in history.index.levels[0]:
                    for time, row in history.loc[symbol].iterrows():
                        self.momentum_indicators[symbol].Update(time, row['close'])
                else:
                    self.Debug(f"No historical data available for symbol {symbol}")