Overall Statistics
Total Trades
39555
Average Win
0.12%
Average Loss
-0.10%
Compounding Annual Return
4.581%
Drawdown
43.300%
Expectancy
0.053
Net Profit
86.228%
Sharpe Ratio
0.259
Probabilistic Sharpe Ratio
0.031%
Loss Rate
53%
Win Rate
47%
Profit-Loss Ratio
1.23
Alpha
0.061
Beta
-0.087
Annual Standard Deviation
0.199
Annual Variance
0.04
Information Ratio
-0.2
Tracking Error
0.259
Treynor Ratio
-0.595
Total Fees
$3465.57
Estimated Strategy Capacity
$4000.00
Lowest Capacity Asset
USEC R735QTJ8XC9X
# https://quantpedia.com/strategies/the-positive-similarity-of-company-filings-and-stock-returns/
#
# The investment universe consists of stocks with large market cap covered by the Brain Company, for which stock prices were available to download from Yahoo Finance and had full history during the sample period.
# Firstly, only the similarity of the positive language is considered. The positive similarity score is calculated as the cosine similarity and is provided by the Brain Company.
# Each month, stocks are ranked based on the positive similarity language score of their most recent company filing and sorted into deciles.
# Long the bottom decile and short the top decile. The strategy is equally-weighted and rebalanced monthly.
#
# QC Implementation:
#   - The investment universe consists of stocks 1000 largest US stocks.

# region imports
from AlgorithmImports import *
# endregion

class ThePositiveSimilarityOfCompanyFilingsAndStockReturns(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2009, 1, 1) # first metric data come in 2009
        self.SetCash(100000)
                
        self.leverage:int = 5
        self.quantile:int = 10

        self.metric_symbols:dict[Symbol, Symbol] = {}
        self.positive_similarities:dict[Symbol, float] = {}

        self.market:Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol

        self.coarse_count:int = 1000
        self.pick_largest:bool = True

        self.selection_flag:bool = False
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart(self.market), self.TimeRules.BeforeMarketClose(self.market, 0), self.Selection)

    def OnSecuritiesChanged(self, changes):
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            security.SetLeverage(self.leverage)

    def CoarseSelectionFunction(self, coarse):
        if not self.selection_flag:
            return Universe.Unchanged
        
        if self.coarse_count <= 1000 and not self.pick_largest:
            selected:list = sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'],
                    key=lambda x: x.DollarVolume, reverse=True)[:self.coarse_count]
        else:
            selected:list = [x for x in coarse if x.HasFundamentalData and x.Market == 'usa']

        selected_symbols:list[Symbol] = []

        for stock in selected:
            symbol:Symbol = stock.Symbol

            if symbol not in self.metric_symbols:
                metric_symbol:Symbol = self.AddData(BrainCompanyFilingLanguageMetrics10K, symbol).Symbol
                self.metric_symbols[symbol] = metric_symbol

            selected_symbols.append(symbol)

        return selected_symbols

    def FineSelectionFunction(self, fine):
        if self.coarse_count <= 1000:
            return list(map(lambda stock: stock.Symbol, fine))

        fine:list = list(filter(lambda stock: stock.MarketCap != 0, fine))

        if len(fine) > self.coarse_count or self.pick_largest:
            sorted_by_cap:list = sorted(fine, key=lambda stock: stock.MarketCap)
            fine = sorted_by_cap[-self.coarse_count:]
        
        return list(map(lambda stock: stock.Symbol, fine)) 
        
    def OnData(self, data):
        if self.selection_flag:
            self.selection_flag = False

            filtered_positive_similarity:dict[Symbol, float] = { symbol: pos_sim for symbol, pos_sim in self.positive_similarities.items() \
                if symbol in data and data[symbol] }

            self.positive_similarities.clear()

            if len(filtered_positive_similarity) < self.quantile:
                self.Liquidate()
            else:
                quantile:int = int(len(filtered_positive_similarity) / self.quantile)
                sorted_by_pos_sim:list[Symbol] = [x[0] for x in sorted(filtered_positive_similarity.items(), key=lambda item: item[1])]

                long_leg:list[Symbol] = sorted_by_pos_sim[:quantile]
                short_leg:list[Symbol] = sorted_by_pos_sim[-quantile:]

                invested:list[Symbol] = [x.Key for x in self.Portfolio if x.Value.Invested]
                for symbol in invested:
                    if symbol not in long_leg + short_leg:
                        self.Liquidate(symbol)

                for symbol in long_leg:
                    self.SetHoldings(symbol, 1 / quantile)

                for symbol in short_leg:
                    self.SetHoldings(symbol, -1 / quantile)

        for stock_symbol, metric_symbol in self.metric_symbols.items():
            if metric_symbol in data and data[metric_symbol]:
                positive_similarity:float = data[metric_symbol].ReportSentiment.Similarity.Positive

                if positive_similarity:
                    self.positive_similarities[stock_symbol] = positive_similarity
        
    def Selection(self):
        self.selection_flag = True

# Custom fee model
class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))