| Overall Statistics |
|
Total Trades 4327 Average Win 1.02% Average Loss -0.39% Compounding Annual Return 66.926% Drawdown 54.800% Expectancy 0.322 Net Profit 364.479% Sharpe Ratio 1.413 Probabilistic Sharpe Ratio 53.902% Loss Rate 64% Win Rate 36% Profit-Loss Ratio 2.63 Alpha 0.778 Beta -0.101 Annual Standard Deviation 0.541 Annual Variance 0.292 Information Ratio 1.068 Tracking Error 0.586 Treynor Ratio -7.578 Total Fees $19937.62 |
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):
'''----------------''' # Backtesting parameters
''' Initialization ''' # Creates EMA indicators for sectors
'''----------------'''
def Initialize(self):
self.SetStartDate(2009, 1, 1)
self.SetEndDate(2012, 1, 1)
self.SetCash(30000)
self.UniverseSettings.Resolution = Resolution.Daily
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.maxStocks = 30
self.volumeMin = 100000
self.piotroskiMin = 5
self.inMarket = True
self.AddEquity("TMF", Resolution.Daily)
self.symbols, self.topSectors = [], []
self.sectorFast, self.sectorSlow = {}, {}
self.sectorStocks = {"XLV": MorningstarSectorCode.Healthcare,
"XLK": MorningstarSectorCode.Technology,
"XLI": MorningstarSectorCode.Industrials,
"XLU": MorningstarSectorCode.Utilities,
"XLF": MorningstarSectorCode.FinancialServices,
"XLP": MorningstarSectorCode.ConsumerDefensive,
"XLY": MorningstarSectorCode.ConsumerCyclical,
"XLB": MorningstarSectorCode.BasicMaterials,
"XLE": MorningstarSectorCode.Energy,
"PSR": MorningstarSectorCode.RealEstate,
"IYZ": MorningstarSectorCode.CommunicationServices}
for x in self.sectorStocks:
symbol = Symbol.Create(x, SecurityType.Equity, Market.USA)
self.AddEquity(symbol, Resolution.Daily)
self.sectorFast[x] = self.EMA(symbol, 5, Resolution.Daily)
self.sectorSlow[x] = self.EMA(symbol, 13, Resolution.Daily)
self.SetWarmUp(timedelta(30))
'''------------------'''
''' Helper Functions '''
'''------------------'''
def Length(self):
invested = [x.Key for x in self.Portfolio if x.Value.Invested]
return len(invested)
'''-----------------''' # Is only updated if we do not have all our holdings allocated/stocks have been sold.
''' Coarse Universe ''' # Otherwise the universe is unchanged and not calculated to decrease workload.
'''-----------------''' # Also determines which sectors are in an uptrend
def CoarseSelectionFunction(self, coarse):
if self.Length() < self.maxStocks:
sortedCoarse = [x for x in coarse if x.HasFundamentalData and x.Volume > self.volumeMin]
self.topSectors = []
for x in self.sectorStocks:
if self.sectorFast[x].IsReady and self.sectorSlow[x].IsReady:
if self.sectorFast[x].Current.Value > self.sectorSlow[x].Current.Value:
self.topSectors.append(self.sectorStocks[x])
return [x.Symbol for x in sortedCoarse]
else: return Universe.Unchanged
'''---------------''' # Filters companies that are of quality (Piotroski Score)
''' Fine Universe ''' # Filters companies that are within the uptrending sectors
'''---------------''' # Sorts by descending PE Ratio to find undervalued stocks
def FineSelectionFunction(self, fine):
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]
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() > self.piotroskiMin
]
sortBySector = [x for x in sortedByFScore if x.AssetClassification.MorningstarSectorCode in self.topSectors and x.MarketCap < 2000000000]
sortByPERatio = sorted(sortBySector, key=lambda x: x.ValuationRatios.PERatio , reverse = False)
uptrend = []
stocksNeeded = self.maxStocks - self.Length()
for x in sortByPERatio:
symbol = x.Symbol
history = self.History(symbol, 13, Resolution.Daily)
history['fast'] = history['close'].ewm(span = 5, min_periods = 0, adjust = False, ignore_na = False).mean()
history['slow'] = history['close'].ewm(span = 13, min_periods = 0, adjust = False, ignore_na = False).mean()
slow = history.iloc[len(history.index)-1]['slow']
fast = history.iloc[len(history.index)-1]['fast']
if fast > slow:
uptrend.append(symbol)
if len(uptrend) >= stocksNeeded:
break
self.symbols = uptrend
return self.symbols
'''--------''' # Sells any held stocks that are no longer in an uptrend
''' OnData ''' # Buys any stocks if we have available stocks slots/unallocated stocks
'''--------'''
def OnData(self, data):
''' Buying '''
if self.Length() < self.maxStocks and len(self.symbols) > 0:
self.inMarket = True
buyingPower = (self.Portfolio.MarginRemaining * 2)/len(self.symbols) * .995
for symbol in self.symbols:
orderSize = buyingPower / self.ActiveSecurities[symbol].Price
self.MarketOrder(symbol, orderSize)
if len(self.symbols) <= 0:
self.Liquidate
''' Selling '''
for holdings in self.Portfolio.Values:
symbol = holdings.Symbol
if holdings.Invested:
history = self.History(symbol, 13, Resolution.Daily)
history['fast'] = history['close'].ewm(span = 5, min_periods = 0, adjust = False, ignore_na = False).mean()
history['slow'] = history['close'].ewm(span = 13, min_periods = 0, adjust = False, ignore_na = False).mean()
slow = history.iloc[len(history.index)-1]['slow']
fast = history.iloc[len(history.index)-1]['fast']
if fast < slow:
self.Liquidate(symbol)
'''-----------------------------'''
''' Piotroski Calculating Class '''
'''-----------------------------'''
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