Overall Statistics
Total Trades
8
Average Win
0.44%
Average Loss
0%
Compounding Annual Return
10.781%
Drawdown
2.500%
Expectancy
0
Net Profit
6.384%
Sharpe Ratio
1.873
Probabilistic Sharpe Ratio
72.651%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
0.003
Beta
0.317
Annual Standard Deviation
0.058
Annual Variance
0.003
Information Ratio
-2.324
Tracking Error
0.098
Treynor Ratio
0.344
Total Fees
$8.00
Estimated Strategy Capacity
$23000000.00
Lowest Capacity Asset
JNJ R735QTJ8XC9X
from QuantConnect.DataSource import SmartInsiderTransaction
import numpy as np
from scipy.optimize import curve_fit

class SmartInsiderCorporateBuybacksAlgorithm(QCAlgorithm):

    def Initialize(self):
        # parameter: minimal information coefficient accpetable
        self.minIC = 0.05
        # parameter: minimal expected return accpetable
        self.minExpectedReturn = 0.005
        
        self.SetStartDate(2021, 1, 1)
        self.SetCash(100000) 
        
        self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel(timedelta(days=252)))
        self.SetExecution(ImmediateExecutionModel())
        
        # include "MMM", "BA", "GS", "HON", "JNJ", "MSFT", "TRV" as a result of the research
        self.symbols = [self.AddEquity(symbol, Resolution.Minute).Symbol for symbol in ["MMM", "GS", "HON", "JNJ", "MSFT", "TRV", "V"]]
        self.buybackSymbols = {symbol: self.AddData(SmartInsiderTransaction, symbol, Resolution.Daily).Symbol for symbol in self.symbols}
        
        # dict contains rolling windows storing tradebar data
        self.history = {symbol: RollingWindow[TradeBar](252*5) for symbol in self.symbols}
        
        # warm up rolling windows
        data = self.History(self.symbols, 252*5, Resolution.Daily)
        for symbol in self.symbols:
            for time, bar in data.loc[symbol].iterrows():
                tradeBar = TradeBar(time, symbol, bar.open, bar.high, bar.low, bar.close, bar.volume)
                self.history[symbol].Add(tradeBar)
            # set up consolidator for future auto-update
            self.Consolidate(symbol, Resolution.Daily, self.DailyBarHandler)
            
        # schedule daily check for entering position
        self.Schedule.On(self.DateRules.EveryDay("MMM"), self.TimeRules.At(10, 0), self.Entry)
        
    def DailyBarHandler(self, bar):
        self.history[bar.Symbol].Add(bar)
        
    def Entry(self):
        ''' check entry signal '''
        for symbol, tSymbol in self.buybackSymbols.items():
            self.transaction = self.History(tSymbol, timedelta(days=1), Resolution.Daily)
            if self.transaction.empty: continue
            
            # buyback%
            self.transaction = self.transaction.usdvalue.unstack("symbol") / self.transaction.usdmarketcap.unstack("symbol")
            # sum up for daily
            self.transaction.index = self.transaction.index.date
            self.transaction = self.transaction.groupby(self.transaction.index).sum()
            # log
            self.transaction = np.log(self.transaction)
            
            self.GetData(symbol)
            
            # find IC
            ic = self.GetIC(symbol)
            # discontinue if IC too low
            if ic < self.minIC: continue
        
            # find expected return
            expectation = self.GetExpectation(symbol)
            # discontinue if expected return too low
            if expectation < self.minExpectedReturn: continue
            
            # emit insight for entry
            self.EmitInsights(Insight.Price(symbol, timedelta(days=252), InsightDirection.Up, None, None, None, expectation*ic))
    
    def GetData(self, symbol):
        ''' get the processed dataframe
        Args:
            symbol: the symbol that we wish to get the processed dataframe'''
        # get historical close price data
        data = pd.DataFrame(self.history[symbol])[::-1]
        history = data.applymap(lambda bar: bar.Close)
        history.index = data.applymap(lambda bar: bar.EndTime.date()).values.flatten().tolist()
        # 1y forward
        history = history.pct_change(252).shift(-252).dropna()
        
        # get buyback transaction data
        transactionHistory = self.History(self.buybackSymbols[symbol], 252*5, Resolution.Daily)
        if transactionHistory.empty: return -1
        # buyback%
        transactionHistory = transactionHistory.usdvalue.unstack("symbol") / transactionHistory.usdmarketcap.unstack("symbol")
        # sum up for daily
        transactionHistory.index = transactionHistory.index.date
        transactionHistory = transactionHistory.groupby(transactionHistory.index).sum()
        
        # concatenate the dataframes to left only the slices with data
        df = pd.concat([history, np.log(transactionHistory)], axis=1).replace([np.inf, -np.inf], np.nan).dropna()
        
        self.df = df
        
    def GetIC(self, symbol):
        ''' get the correlation coefficient as information coefficient
        Args:
            symbol: the symbol that we wish to get IC between 1y forward return and buyback transaction
        Return:
            (float) the correlation coefficient/IC'''
        return self.df.corr().values[0, 1]
        
    def GetExpectation(self, symbol):
        ''' get expected return in 1 year given the buyback%
        Args:
            symbol: the Symbol that we wish to get ins expected return
        Return:
            (float) expected return%'''
        
        def Function(x, a, b, c):
            ''' the function to be fitted '''
            return a * np.exp(-b * x) + c
        
        try:
            popt, pcov = curve_fit(Function, self.df.iloc[:, 1].values, self.df.iloc[:, 0].values)
        # in case no optimal coefficients can be fit
        except:
            return -1 
        
        return Function(self.transaction, *popt).values