该策略通过分析活跃的共同基金经理的13F文件,识别每位经理最看好的股票,重点投资被多数基金经理视为“最佳想法”的股票。这种方法利用经验丰富的投资专业人士的集体智慧和研究成果,以识别高潜力的投资机会,从而通过整合多位基金经理的专业知识和战略判断实现更优异的投资表现。

策略概述

选择一批活跃的共同基金经理,并通过分析他们的13F文件,找出每位经理最看好的股票。重点投资那些被大多数基金经理视为“最佳想法”的股票。这种方法利用了经验丰富的投资专业人士的集体智慧和研究成果,以识别具有高潜力的投资机会,通过整合多位基金经理的专业知识和战略判断来获得更优异的投资表现。

策略合理性

当前的投资原则提倡多元化投资组合,这往往使共同基金经理不愿过于集中投资于少数资产,通常是为了避免与基准指数偏离过大。然而,被基金经理认为是“最佳想法”或高信心的股票,往往在他们的投资组合中占有较大比重,这反映了经理对这些股票的深刻理解和信心。通过识别并追踪这些“最佳想法”股票,可以更好地利用经理们对其投资选择的高度把握和战略信念。

论文来源

Best Ideas [点击浏览原文]

<摘要>

我们发现,活跃共同基金或对冲基金经理在预先表现出最大信心的股票,即他们的“最佳想法”股票,比市场以及这些经理投资组合中的其他股票的表现优异约2.8%到4.5%(取决于所采用的基准)。大多数经理持有的其他股票并没有显著的超额表现。因此,资产管理行业的组织结构似乎使得经理们引入一些未能跑赢的股票进入其投资组合。我们认为,如果经理们持有更为集中的投资组合,投资者将从中受益。

回测表现

年化收益率20.21%
波动率N/A
Beta0.707
夏普比率0.533
索提诺比率0.47
最大回撤33.9%
胜率73%

完整python代码

from AlgoLib import *
import numpy as np
from dateutil.relativedelta import relativedelta

class ManagerBestPicksFrom13FFilings(XXX):

    def Initialize(self):
        self.SetStartDate(2011, 1, 1)  # Starting date
        self.SetCash(100000)  # Initial capital

        self.UniverseSettings.Leverage = 5  # Leverage
        self.UniverseSettings.Resolution = Resolution.Daily  # Data resolution
        self.AddUniverse(self.SelectStocksBasedOn13F)
        self.Settings.MinimumOrderMarginPortfolioPercentage = 0.0
        
        self.delay_months: int = 2  # Delay in months to consider 13F filings
        self.update_period: datetime.date

        self.stock_weights: dict[Symbol, float] = {}
        self.manager_selections: dict[datetime, dict[str, int]] = self.LoadManagerSelections()
        self.update_flag: bool = False

        reference: Symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
        self.Schedule.On(self.DateRules.MonthStart(reference), self.TimeRules.AfterMarketOpen(reference), self.DecideToUpdate)

    def OnSecuritiesChanged(self, changes: SecurityChanges):
        for security in changes.AddedSecurities:
            security.SetFeeModel(AdjustedFeeModel())
            
    def SelectStocksBasedOn13F(self, fundamentals: List[Fundamental]):
        if not self.update_flag:
            return Universe.Unchanged
        
        target_selection: dict[str, int] = None
        lookback_date: datetime.date = self.Time.date() - relativedelta(months=self.delay_months + 1)
        current_date: datetime.date = self.Time.date()
        
        # Finding the most recent manager selections
        for date, selections in self.manager_selections.items():
            if lookback_date < date <= current_date:
                target_selection = selections
        
        if target_selection is None:
            return []
        
        # Creating the selected universe
        selected_symbols = [f.Symbol for f in fundamentals if f.Symbol.Value in target_selection]
        
        # Calculating weights
        total_votes = sum(target_selection.values())
        for symbol in selected_symbols:
            self.stock_weights[symbol] = target_selection[symbol.Value] / total_votes

        return selected_symbols

    def OnData(self, slice: Slice):
        if not self.update_flag:
            return
        self.update_flag = False
        
        # Executing trades
        targets = [PortfolioTarget(symbol, weight) for symbol, weight in self.stock_weights.items() if symbol in slice and slice[symbol]]
        self.SetHoldings(targets, True)
        self.stock_weights.clear()
        
    def DecideToUpdate(self):
        # Update every quarter
        if self.Time.month % 3 == 2:
            self.update_flag = True

    def LoadManagerSelections(self) -> Dict[datetime, Dict[str, int]]:
        selections: dict[datetime, dict[str, int]] = {}
        data = self.Download('path_to_13F_filings_data.csv')
        for line in data.split('\n')[1:]:
            parts = line.split(',')
            date = datetime.strptime(parts[0], "%Y-%m-%d").date()
            
            if date not in selections:
                selections[date] = {}
            
            for ticker in parts[1:]:
                if ticker not in selections[date]:
                    selections[date][ticker] = 0
                selections[date][ticker] += 1
                
            self.update_period = date  # Last update

        return selections

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading