Overall Statistics
Total Trades
4825
Average Win
0.12%
Average Loss
-0.12%
Compounding Annual Return
10.768%
Drawdown
33.900%
Expectancy
0.431
Net Profit
282.263%
Sharpe Ratio
0.533
Sortino Ratio
0.47
Probabilistic Sharpe Ratio
5.980%
Loss Rate
27%
Win Rate
73%
Profit-Loss Ratio
0.95
Alpha
0.006
Beta
0.707
Annual Standard Deviation
0.124
Annual Variance
0.015
Information Ratio
-0.228
Tracking Error
0.083
Treynor Ratio
0.093
Total Fees
$294.31
Estimated Strategy Capacity
$390000000.00
Lowest Capacity Asset
LINTA TIIB7Z82AFS5
Portfolio Turnover
0.68%
# https://quantpedia.com/strategies/alpha-cloning-following-13f-fillings/
#
# Create a universe of active mutual fund managers.
# Use 13F filings to identify the “best idea” stocks for each manager.
# Invest in the stocks, which are the “best ideas” for most of the managers.
#
# QC Implementation changes:
#   - Investor preferences was downloaded from https://www.insidermonkey.com/hedge-fund/browse/A/
#   - Investors list consists of first 10 investors in each browse letter and from lists in basic and premium cards on https://www.gurufocus.com/guru/list
#   - Investor preferences are modeled to be known 2 months after announcement.

#region imports
from AlgorithmImports import *
import numpy as np
from dateutil.relativedelta import relativedelta
#endregion

class AlphaCloningFollowing13FFillings(QCAlgorithm):

    def Initialize(self) -> None:
        self.SetStartDate(2011, 1, 1)
        self.SetCash(100_000)

        self.UniverseSettings.Leverage = 5
        self.UniverseSettings.Resolution = Resolution.Daily
        self.AddUniverse(self.FundamentalSelectionFunction)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
        
        self.months_lag: int = 2 # Lag for getting investors preferences report
        self.last_update_date: datetime.date

        self.weights: dict[Symbol, float] = {}
        self.investors_preferences: dict[datetime, dict[str, int]] = self.GetPreferences()
        self.selection_flag: bool = False

        market: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.Schedule.On(self.DateRules.MonthStart(market), self.TimeRules.AfterMarketOpen(market), self.Selection)
        
    def OnSecuritiesChanged(self, changes: SecurityChanges) -> None:
        for security in changes.AddedSecurities:
            security.SetFeeModel(CustomFeeModel())
            
    def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
        if not self.selection_flag:
            return Universe.Unchanged
        
        selected_report: dict[str, int] = None
        min_date: datetime.date = self.Time.date() - relativedelta(months=self.months_lag+1)   # quarterly data
        max_date: datetime.date = self.Time.date()
        
        for date in self.investors_preferences:
            # Get latest report
            if date >= min_date and date <= max_date:
                selected_report = self.investors_preferences[date]
                
        # Report might not be selected, because there are no data for that date
        if selected_report is None:
            return []
        
        # Select universe based on report
        selected: list[Symbol] = [x.Symbol for x in fundamental if x.Symbol.Value in selected_report]
        
        # Calculate total preferences votes for selected report
        total_preferences_votes: int = sum([x[1] for x in selected_report.items()])
        
        # Calculate weight for each stock in selected universe
        for symbol in selected:
            # weight = total stock preferences votes / total investor votes in selected report 
            self.weights[symbol] = selected_report[symbol.Value] / total_preferences_votes

        return selected

    def OnData(self, slice: Slice) -> None:
        if not self.selection_flag:
            return
        self.selection_flag = False
        
        # Trade Execution
        portfolio: List[PortfolioTarget] = [PortfolioTarget(symbol, w) 
                                            for symbol, w in self.weights.items()
                                            if symbol in slice and slice[symbol]]
        self.SetHoldings(portfolio, True)
        
        self.weights.clear()
        
    def Selection(self) -> None:
        if self.Time.month % 3 == 2:
            self.selection_flag = True

    def GetPreferences(self) -> Dict[datetime, Dict[str, int]]:
        preferences: dict[datetime, dict[str, int]] = {}
        csv_string_file = self.Download('data.quantpedia.com/backtesting_data/economic/investors_preferences.csv')
        lines: list[str] = csv_string_file.split('\r\n')

        # Skip csv header in loop
        for line in lines[1:]:
            line_split: list[str] = line.split(';')
            date: datetime.date = datetime.strptime(line_split[0], "%d.%m.%Y").date()
            
            preferences[date]: dict[str, int] = {}
            
            for ticker in line_split[1:]:
                if ticker not in preferences[date]:
                    preferences[date][ticker] = 0
                    
                preferences[date][ticker] += 1

            # Set last update date
            if line == lines[-1]:
                self.last_update_date = date

        return preferences

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