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;
        }

    }
}