| Overall Statistics |
|
Total Trades 118 Average Win 1.26% Average Loss -1.74% Compounding Annual Return 9.292% Drawdown 38.100% Expectancy 0.023 Net Profit 6.222% Sharpe Ratio 0.416 Probabilistic Sharpe Ratio 29.078% Loss Rate 41% Win Rate 59% Profit-Loss Ratio 0.73 Alpha 0.19 Beta -0.151 Annual Standard Deviation 0.398 Annual Variance 0.158 Information Ratio 0.001 Tracking Error 0.571 Treynor Ratio -1.093 Total Fees $390.01 |
using System.Collections.Concurrent;
namespace QuantConnect.Algorithm.CSharp
{
/*
Investment Thesis: Quality and momentum factors are well-known phenomena
in the finance industry and have had decent individual results over the last 20 years.
The quality factor refers to the tendency of high-quality stocks with
typically more stable earnings, stronger balance sheets and higher margins
to outperform low-quality stocks, over a long time horizon.
Momentum factor refers to the empirically observed tendency for rising asset
prices to keep rising, and falling prices to keep falling.
This algo attemps to combine the quality and momentum factors together to achieve
alpha. To measure Quality, it uses Piotroski F-Score,
and to measure momentum it uses % change. This is a long-only algo with an index filter. During an
uptrend it will load a portfolio with the 10 best stocks based on the quality
and momentum criteria, and during a downtrend it will gradually scale-out
of equity positions and invest in cash and bonds to minimize draw-down.
Rotation and rebalancing is done once a month (first trading day) to minimize commission fees.
Logic overview:
At the start of each month:
Sell positions which no longer fit our criteria
index filter = 1:
Rebalance exisiting positions to maintain equal % of portfolio invested in 10 stocks
Acquire new positions meeting criteria
index filter = 0:
Invest remaining cash in bonds
criteria = stocks that have a Piotroski score >= 6, have momentum > 3, US stock exchange only, exclude financial services or utilities
Key Ideas:
- gradually scales out of stocks when the index is no longer in an uptrend by disallowing new long positions and letting exisiting long positions to close based on absolute momentum
- fully-load in stocks as soon as the index becomes uptrending to catch explosive rallies after a market crash
- rebalance exisiting positions to maintain equal % of portfolio in each stock. This is NOT ideal because volatile stock will dominate the portfolio. A better approach may be to rebalance based on ATR.
- gradually scale-in bonds each month the previous month's leftover cash ... this can be improved
Insipred by:
Stocks on the Move - Andreas Clenow
Dual Momentum - Gary Antonacci
Little Book that still beats the market - Joel Greenblatt
Revisions:
V1.0 Ivan Krotnev 03/02/2020 -- initial revision
*/
public class QualityMomentum : QCAlgorithm
{
//------parameters---------//
public int FastPeriod = 90;
public int SlowPeriod = 200;
public int NumberOfSymbolsFine = 10;
public decimal TargetPercent = 0.095m; //Invest slightly less than 10% of our cash into 10 stocks leaving a small cash buffer for slippage. Fully loaded portfolio thus = 95% stocks 5% cash.
private Symbol _spy = QuantConnect.Symbol.Create("SPY", SecurityType.Equity, Market.USA); //SPY ETF starting date < 2000
private Symbol _agg = QuantConnect.Symbol.Create("AGG", SecurityType.Equity, Market.USA); //BOND ETF starting date 2003
//-----state variables-------//
private SecurityChanges _changes = SecurityChanges.None;
public SimpleMovingAverage Slow; //holds a simple moving average of the SPY for a trend filter
private int _lastMonth = -1;
int indexFilterSMA = 0; //0 - index filter is in a downtrend, 1 - index filter is in an uptrend
public override void Initialize()
{
SetStartDate(2020, 1, 1); //Set Start Date
//SetEndDate(2013, 1, 1); //Set End Date
SetCash(100000); //Set Strategy Cash
//SetBrokerageModel(Brokerages.BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin);
// Set zero transaction fees
//SetSecurityInitializer(security => security.FeeModel = new ConstantFeeModel(0));
UniverseSettings.Resolution = Resolution.Daily;
AddEquity(_spy,Resolution.Daily); //spy etf
AddEquity(_agg, Resolution.Daily); //bond etf
Slow = SMA(_spy, SlowPeriod, Resolution.Daily);
SetWarmup(SlowPeriod);
SetBenchmark(_spy);
// this add universe method accepts two parameters:
// - coarse selection function: accepts an IEnumerable<CoarseFundamental> and returns an IEnumerable<Symbol>
// - fine selection function: accepts an IEnumerable<FineFundamental> and returns an IEnumerable<Symbol>
AddUniverse(CoarseSelectionFunction, FineSelectionFunction);
}
public IEnumerable<Symbol> CoarseSelectionFunction(IEnumerable<CoarseFundamental> coarse)
{
// check if it's the beginning of a new month
if (Time.Month == _lastMonth)
{
return Universe.Unchanged;
}
//Debug(Time + " Resetting _lastMonth to " + Time.Month);
_lastMonth = Time.Month;
// select only symbols with fundamental data aka. Stocks only, not ETFs
var sortedByDollarVolume = coarse
.Where(x => x.HasFundamentalData)
.Where(x => x.AdjustedPrice > 0m);
//.OrderByDescending(x => x.DollarVolume);
Debug(Time +" Number of Securities in coarse selection table:" + sortedByDollarVolume.Count().ToString());
// we need to return only the symbol objects
return sortedByDollarVolume.Select(x => x.Symbol);
}
public IEnumerable<Symbol> FineSelectionFunction(IEnumerable<FineFundamental> fine)
{
ConcurrentDictionary<Symbol, IndicatorBase<IndicatorDataPoint>> averages = new ConcurrentDictionary<Symbol, IndicatorBase<IndicatorDataPoint>>();
var piatroskiscore = (from x in fine
where x.SecurityReference.SecurityType == "ST00000001" && x.SecurityReference.IsPrimaryShare
where !x.SecurityReference.IsDepositaryReceipt
where x.AssetClassification.MorningstarIndustryGroupCode != MorningstarSectorCode.FinancialServices
where x.AssetClassification.MorningstarIndustryGroupCode != MorningstarSectorCode.Utilities
where (Time - x.SecurityReference.IPODate).TotalDays > 180 //we need this to
// where (x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) > 300000000) //&& x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) < 2000000000)
where x.MarketCap >= 100e6m
//where x.MarketCap <= 100e6m
let fs = 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, // fix need last year
x.OperationRatios.CurrentRatio.ThreeMonths, x.OperationRatios.CurrentRatio.OneYear, // fix need last year
x.OperationRatios.AssetsTurnover.ThreeMonths, x.OperationRatios.AssetsTurnover.OneYear // fix need last year
)
where (fs >= 6)
let avg = averages.GetOrAdd(x.Symbol, sym => WarmUpIndicator(x.Symbol, new MomentumPercent(FastPeriod), Resolution.Daily))
// Update returns true when the indicators are ready, so don't accept until they are
where avg.Update(x.EndTime, x.Price)
// only pick symbols who have positive slopes [Absolute Momentum]
where avg > 3m
// prefer symbols with a larger slope
orderby (avg) descending
select x.Symbol);
Debug(Time +" Number of Securities is Fine selection table:" + piatroskiscore.Count().ToString());
// we need to return only the symbol objects
return piatroskiscore.Take(NumberOfSymbolsFine);
}
//Data Event Handler: New data arrives here. "TradeBars" type is a dictionary of strings so you can access it by symbol.
public void OnData(TradeBars data) //executes once a day
{
// wait for our indicators to ready
if (!Slow.IsReady) return;
//update indicators and metrics at end of each day
if (data.ContainsKey(_spy)) {
indexFilterSMA = data[_spy.Value].Price > Slow.Current.Value ? 1 : 0;
}
// plot indicators, the series name will be the name of the indicator
decimal freeCashLeverage = (Portfolio.TotalPortfolioValue - Portfolio.TotalAbsoluteHoldingsCost) / Portfolio.TotalPortfolioValue;
decimal aggLeverage = Portfolio[_agg.Value].AbsoluteHoldingsCost / Portfolio.TotalPortfolioValue;
Plot("Benchmark", Slow);
Plot("IndexFilter","IndexFilterSMA", indexFilterSMA);
Plot("Exposures", "Cash", freeCashLeverage);
Plot("Exposures", "AGG", aggLeverage);
Plot("Memory", "memusage", (float)GC.GetTotalMemory(false));
// if we have no changes, do nothing
if (_changes == SecurityChanges.None) return;
// liquidate removed securities
Debug(Time + " ---Liquidating removed securities...");
foreach (var security in _changes.RemovedSecurities)
{
if (security.Invested)
{
Liquidate(security.Symbol);
Debug(Time + " Liquidated Stock: " + security.Symbol.Value);
}
}
//rebalance existing positions
Debug(Time + " ---Rebalancing existing positions...");
bool markedForDeletion = false;
foreach (var security in ActiveSecurities.Values)
{
if (security.Symbol == _spy || security.Symbol == _agg) continue;
if (security.Invested) //rebalance existing stock
{
//check to make sure it is not marked for deletion
markedForDeletion = false;
foreach (var s in _changes.RemovedSecurities)
{
if (s.Symbol.Value == security.Symbol.Value)
{
markedForDeletion = true;
break;
}
}
if (!markedForDeletion) {
SetHoldings(security.Symbol, TargetPercent);
Debug(Time + " Rebalanced Stock: " + security.Symbol.Value);
}
}
}
Debug(Time + " ---Looking to purchase more securities...");
//index filter
if (indexFilterSMA > 0) { //SPY is above its moving average
Debug(Time + " ---SPY is trending UP. Liquidating bonds and purchasing new securities...");
//Liquidate bond allocations, if invested
Liquidate(_agg);
// we want 10% allocation in each security in our universe
foreach (var security in ActiveSecurities.Values)
{
if (security.Symbol == _spy || security.Symbol == _agg) continue;
if (!security.Invested) { //purchase new stock
SetHoldings(security.Symbol, TargetPercent);
Debug("Purchased Stock: " + security.Symbol.Value);
}
}
} else { //SPY is below its moving average
Debug(Time + " ---SPY is trending DOWN. Skipping buying new securities and moving extra cash to bonds...");
//invest remaining cash in bonds
SetHoldings(_agg, aggLeverage+freeCashLeverage);
}
_changes = SecurityChanges.None;
}
// this event fires whenever we have changes to our universe
public override void OnSecuritiesChanged(SecurityChanges changes)
{
_changes = changes;
//Log($"OnSecuritiesChanged({UtcTime:o}):: {changes}");
if (changes.AddedSecurities.Count > 0)
{
Debug(Time + " Securities added: " + string.Join(",", changes.AddedSecurities.Select(x => x.Symbol.Value)));
}
if (changes.RemovedSecurities.Count > 0)
{
Debug(Time + " Securities removed: " + string.Join(",", changes.RemovedSecurities.Select(x => x.Symbol.Value)));
}
}
// calculate the f-score
private int FScore(
decimal netincome,
decimal operating_cashflow,
decimal roa_current, decimal roa_past,
decimal issued_current, decimal issued_past,
decimal grossm_current, decimal grossm_past,
decimal longterm_current, decimal longterm_past,
decimal curratio_current, decimal curratio_past,
decimal assetturn_current, decimal assetturn_past
)
{
int fscore = 0;
fscore += (roa_current > 0m ? 1 : 0 ); // return on assets is positive
fscore += ( operating_cashflow > 0m ? 1 : 0 ); // operating cashflow in current year is positive
fscore += ( roa_current >= roa_past ? 1 : 0 ); // roa has increased since last Time
fscore += ( operating_cashflow > roa_current ? 1 : 0 ); // cashflow from current operations are greater than roa
fscore += ( longterm_current <= longterm_past ? 1 : 0 ); // a decrease in the long term debt ratio
fscore += ( curratio_current >= curratio_past ? 1 : 0 ); // an increase in the current ratio
fscore += ( issued_current <= issued_past ? 1 : 0 ); // no new shares have been issued
fscore += ( grossm_current >= grossm_past ? 1 : 0 ); // an increase in gross margin
fscore += ( assetturn_current >= assetturn_past ? 1 : 0 ); // a higher asset turnover ratio
// Debug("<<<<<<");
//Debug("f-score :" +fscore);
// Debug("roa current : " + roa_current);
// Debug("operating cashflow : " + operating_cashflow);
// Debug("roa current : " + roa_current + " past : " + roa_past);
// Debug("operating cashflow : " + operating_cashflow );
// Debug("longterm_current : " + longterm_current + " past : " + longterm_past);
// Debug("curratio_current : " + curratio_current + " past : " + curratio_past);
// Debug("issued current : " + issued_current + " : past " + issued_past);
// Debug("grossm current : " + grossm_current + " : past " + grossm_past);
// Debug("assetturn_current : " + assetturn_current + " : " + assetturn_past);
return fscore;
}
}
}