Overall Statistics |
Total Trades 37 Average Win 11.35% Average Loss -0.37% Compounding Annual Return 35060889.198% Drawdown 17.700% Expectancy 28.815 Net Profit 687.565% Sharpe Ratio 36135439.506 Probabilistic Sharpe Ratio 96.694% Loss Rate 7% Win Rate 93% Profit-Loss Ratio 31.11 Alpha 237755899.74 Beta -0.481 Annual Standard Deviation 6.58 Annual Variance 43.291 Information Ratio 36108036.215 Tracking Error 6.585 Treynor Ratio -494265227.049 Total Fees $126.36 |
from System.Collections.Generic import List from QuantConnect.Data.UniverseSelection import * import operator from math import ceil,floor from scipy import stats import numpy as np from datetime import timedelta class Piotroski(QCAlgorithm): def Initialize(self): ''' Backtesting Parameters ''' self.SetStartDate(2007, 12, 3) self.SetEndDate(2008, 1, 30) self.SetCash(50000) ''' Universe (Updated Monthly) and Brokerage Settings ''' self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage) self.benchmark = Symbol.Create("SPY", SecurityType.Equity, Market.USA) self.UniverseSettings.Resolution = Resolution.Minute self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) self.topScoreSymbolsCoarse = 10000 self.topScoreSymbolsFine = 100 ''' Schedule Settings ''' self.AddEquity("SPY", Resolution.Minute) self.SetBenchmark("SPY") self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY"), Action(self.Rebalance)) self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 1), Action(self.Daily)) ''' Other Settings ''' self.month = -1 self.symbols = [] self.qualityStocks = [] self.lookback = 15 self.initiated = -1 def CoarseSelectionFunction(self, coarse): if self.month != self.Time.month: sortedCoarse = [x for x in coarse if x.HasFundamentalData and x.Price > 5] sortedDollarVolume = sorted(sortedCoarse, key=lambda x: x.DollarVolume, reverse=True) topCoarse = sortedDollarVolume[:self.topScoreSymbolsCoarse] return [x.Symbol for x in topCoarse] else: return self.symbols def FineSelectionFunction(self, fine): if self.month != self.Time.month: self.month = self.Time.month ''' Retrieve all stocks that have the valid variation ratios that we want ''' filteredFine = [x for x in fine if x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths and x.FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.TwelveMonths and x.OperationRatios.ROA.ThreeMonths and x.OperationRatios.ROA.OneYear and x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths and x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths and x.OperationRatios.GrossMargin.ThreeMonths and x.OperationRatios.GrossMargin.OneYear and x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths and x.OperationRatios.LongTermDebtEquityRatio.OneYear and x.OperationRatios.CurrentRatio.ThreeMonths and x.OperationRatios.CurrentRatio.OneYear and x.OperationRatios.AssetsTurnover.ThreeMonths and x.OperationRatios.AssetsTurnover.OneYear and x.ValuationRatios.NormalizedPERatio and x.EarningReports.BasicAverageShares.ThreeMonths and x.EarningReports.BasicEPS.TwelveMonths and x.ValuationRatios.PayoutRatio > 0] ''' Using the FScore class, retrieve the stocks that have a score of X or higher ''' sortedByFScore = [x for x in filteredFine if FScore(x.FinancialStatements.IncomeStatement.NetIncome.TwelveMonths, x.FinancialStatements.CashFlowStatement.CashFlowFromContinuingOperatingActivities.TwelveMonths, x.OperationRatios.ROA.ThreeMonths, x.OperationRatios.ROA.OneYear, x.FinancialStatements.BalanceSheet.ShareIssued.ThreeMonths, x.FinancialStatements.BalanceSheet.ShareIssued.TwelveMonths, x.OperationRatios.GrossMargin.ThreeMonths, x.OperationRatios.GrossMargin.OneYear, x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths, x.OperationRatios.LongTermDebtEquityRatio.OneYear, x.OperationRatios.CurrentRatio.ThreeMonths, x.OperationRatios.CurrentRatio.OneYear, x.OperationRatios.AssetsTurnover.ThreeMonths, x.OperationRatios.AssetsTurnover.OneYear).ObjectiveScore() > 6 ] self.qualityStocks = sortedByFScore ''' The Piotroski score ranks quality stocks, but we still need to determine value by filtering it more ''' sortedByNormalizedPE = sorted(sortedByFScore, key=lambda x: (x.ValuationRatios.NormalizedPERatio), reverse = False) topFine = sortedByNormalizedPE[:self.topScoreSymbolsFine] self.symbols = [i.Symbol for i in topFine] return self.symbols else: return self.symbols def Daily(self): ' Rebalance once if we were to start the algorithm in the middle of the month ' if self.initiated < 0: self.Rebalance() self.initiated = 1 def OnData(self, data): for kvp in data.Dividends: div_ticker = kvp.Key div_distribution = kvp.Value.Distribution if self.Portfolio[div_ticker].Quantity > 0: div_total_value = div_distribution * self.Portfolio[div_ticker].Quantity self.Log("DIVIDENDS >> " + str(div_ticker) + ", $" + str(div_total_value)) def Rebalance(self): ''' Fetch the historical data to perform the linear regression ''' history = self.History( self.symbols + [self.benchmark], self.lookback, Resolution.Hour).close.unstack(level=0) symbols = self.SelectSymbols(history) ''' Liquidate positions that are not "quality" stocks (score of > 6) ''' for holdings in self.Portfolio.Values: symbol = holdings.Symbol if symbol not in self.qualityStocks and holdings.Invested: self.Liquidate(symbol) self.Log("SELLING >> " + str(symbol) + ", $ " + str(self.Securities[symbol].Price) + " * " + str(self.Portfolio[symbol].Quantity) + " = " + str(self.Securities[symbol].Price * self.Portfolio[symbol].Quantity)) ''' Invest 100% in the selected symbols ''' for symbol in symbols: if self.Securities[symbol].Price > 0: buyingPower = self.Portfolio.MarginRemaining * .5 orderSize = buyingPower / self.Securities[symbol].Price self.Securities[symbol].SetDataNormalizationMode(DataNormalizationMode.Raw) self.MarketOrder(symbol, orderSize) self.Log("BUYING >> " + str(symbol) + ", $ " + str(self.Securities[symbol].Price) + " * " + str(self.Portfolio[symbol].Quantity) + " = " + str(self.Securities[symbol].Price * self.Portfolio[symbol].Quantity)) def SelectSymbols(self, history): '''Select symbols with the highest intercept/alpha to the benchmark''' alphas = dict() ''' Get the benchmark returns ''' benchmark = history[self.benchmark].pct_change().dropna() ''' Conducts linear regression for each symbol and save the intercept/alpha ''' for symbol in self.symbols: ''' Get the security returns ''' if not symbol in history: continue returns = history[symbol].pct_change().dropna() returns = np.vstack([returns, np.ones(len(returns))]).T if len(returns) != len(benchmark): continue ''' Simple linear regression function in Numpy ''' result = np.linalg.lstsq(returns, benchmark) alphas[symbol] = result[0][1] ''' Select symbols with the highest intercept/alpha to the benchmark ''' selected = sorted(alphas.items(), key=lambda x: x[1], reverse=True)[:25] return [x[0] for x in selected] class FScore(object): def __init__(self, netincome, operating_cashflow, roa_current, roa_past, issued_current, issued_past, grossm_current, grossm_past, longterm_current, longterm_past, curratio_current, curratio_past, assetturn_current, assetturn_past): self.netincome = netincome self.operating_cashflow = operating_cashflow self.roa_current = roa_current self.roa_past = roa_past self.issued_current = issued_current self.issued_past = issued_past self.grossm_current = grossm_current self.grossm_past = grossm_past self.longterm_current = longterm_current self.longterm_past = longterm_past self.curratio_current = curratio_current self.curratio_past = curratio_past self.assetturn_current = assetturn_current self.assetturn_past = assetturn_past def ObjectiveScore(self): ''' The Piotroski score is broken down into profitability; leverage, liquidity, and source of funds; and operating efficiency categories, as follows: ''' fscore = 0 ''' Profitability Criteria ''' fscore += np.where(self.netincome > 0, 1, 0) # Positive Net Income (X Months?) fscore += np.where(self.operating_cashflow > 0, 1, 0) # Positive Operating Cash Flow fscore += np.where(self.roa_current > self.roa_past, 1, 0) # Positive Return on Assets fscore += np.where(self.operating_cashflow > self.roa_current, 1, 0) # Cash flow from operations being greater than net income (quality of earnings) ''' Leverage, Liquidity, and Source of Dunds Criteria ''' fscore += np.where(self.longterm_current <= self.longterm_past, 1, 0) # Lower ratio of long term debt in the current period, compared to the previous year (decreased leverage) fscore += np.where(self.curratio_current >= self.curratio_past, 1, 0) # Higher current ratio this year compared to the previous year (more liquidity) fscore += np.where(self.issued_current <= self.issued_past, 1, 0) # No new shares were issued in the last year ''' Operating Efficiency Criteria ''' # A higher gross margin compared to the previous year fscore += np.where(self.grossm_current >= self.grossm_past, 1, 0) # A higher gross margin compared to the previous year fscore += np.where(self.assetturn_current >= self.assetturn_past, 1, 0) # A higher asset turnover ratio compared to the previous year (1 point) return fscore