Overall Statistics
Total Trades
4777
Average Win
0.06%
Average Loss
-0.05%
Compounding Annual Return
13.905%
Drawdown
2.300%
Expectancy
0.091
Net Profit
7.189%
Sharpe Ratio
2.404
Probabilistic Sharpe Ratio
81.322%
Loss Rate
54%
Win Rate
46%
Profit-Loss Ratio
1.35
Alpha
-0.008
Beta
0.419
Annual Standard Deviation
0.059
Annual Variance
0.004
Information Ratio
-2.719
Tracking Error
0.08
Treynor Ratio
0.34
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY R735QTJ8XC9X
/*
	This program was developed by Quantify and is property of Mario Sarli
    Usage and marketing of this program is permitted.
    
    Quantify Developer(s): Conor Flynn
    Date Created: 07/13/2021
    Client: Mario Sarli
    Client ID: 341994651
    
    If you find a bug or an inconsistantcy please contact your assigned developer.
    Contact: cflynn@quantify-co.com
    
    To request a new copy of your program please contact support:
    Contact: support@quantify-co.com
    
    Note: Client ID is required for client verification upon requesting a new copy
*/

namespace QuantConnect.Algorithm.CSharp
{
    public class Main : QCAlgorithm
    {
    	// BACKTESTING PARAMETERS
    	// =================================================================================================================
    	
    	// general settings:
    	
    	// set starting cash
    	private int starting_cash = 100000;
    	
    	// backtesting start date time:
    	// date setting variables
    	private int start_year = 2021;
    	private int start_month = 1;
    	private int start_day = 1;
    	
    	// backtesting end date time:
    	// determines whether there is a specified end date
    	// if false it will go to the current date (if 'true' it will go to the specified date)
    	private bool enable_end_date = false;
    	// date setting variables
    	private int end_year = 2021;
    	private int end_month = 5;
    	private int end_day = 15;
    	
    	
    	// universe settings:
    	
    	// data update resolution
    	// changes how often the data updates and algorithm looks for entry
    	// determines how often the function OnData runs
    	// list of resolutions:
    	// Resolution.Tick; Resolution.Second; Resolution.Minute; Resolution.Hour; Resolution.Daily
    	private readonly Resolution resolution = Resolution.Tick;
    	
    	// consolidated resolution count
    	// refers the number of datapoints for the resolution to consolidate
    	// example: if resolution = Resolution.Second and resolution_consolidation = 5
    	// the program will run on 5 second resolution
    	private readonly int resolution_consolidation = 20;
    	
    	// stock list
    	// list of stocks you want in the universe
    	// used in manual selection of universe
    	// format:
    	// new StockBase(TICKER, PORTFOLIO_ALLOC)
    	
    	// TICKER: ticker identifier
    	// PORTFOLIO_ALLOC: percentage of portfolio to allocate (note in decimal form: 0.50m -> 50%)
    	private readonly StockBase[] manual_universe = new StockBase[]{
    		new StockBase("SPY", 0.50m),
    		//new StockBase("AAPL", 0.50m)
    	};
    	
    	// drawdown limitation
    	// determines the max drawdown per ticker per day
    	// once reached the ticker will be liquidated and cannot be traded for the remainder of the day
    	// set as -1 to disable
    	// (note in decimal form: 0.03m -> 03%)
    	private static readonly decimal drawdown_universe = 0.03m;
    	
    	
    	// position settings:
    	
    	// percentage of order to be set as a market order
    	// in array form, so each element is a different position
    	// (note in decimal form: 0.40m -> 40%)
    	private readonly decimal[] mo_percent_position = new decimal[] {
    		0.40m
    	};
    	
    	// percentage of order to be set as limit orders
    	// in array form, so each element is a different position
    	// (note in decimal form: 0.20m -> 20%)
    	private readonly decimal[] lo_percent_position = new decimal[] {
    		0.60m
    	};
    	
    	
    	// indicator settings:
    	
    	// length of the SMA for the base line (line 2, init line 4, 5)
    	private readonly int length_base = 50;
    	
    	// base calculation type: (line 4, 5)
    	// 1: OHLC4
    	// 2: (H+L) / 2
    	private static readonly int calculation_base = 1;
    	
    	// length of the SMA for the space line: (line 2, init line 21)
    	private readonly int length_space = 50;
    	
    	// space calculation type: (line 21)
    	// 1: (H-L)
    	// 2: abs(C-O)
    	private static readonly int calculation_space = 1;
    	
    	// space factor: (line 24)
    	// determines the factor at which the calculated space is multipled by
    	private static readonly decimal factor_space = 1.0m;
    	
    	// number of spacers from the base line (default 10)
    	private readonly int count_space = 10;
    	
    	// =================================================================================================================
		
		// creates new universe variable setting
		private List<StockData> universe = new List<StockData>();
		
		// security changes variable
		private SecurityChanges securityChanges = SecurityChanges.None;
		
        public override void Initialize()
        {
        	// set start date
            SetStartDate(start_year, start_month, start_day);
            // set end date
            if(enable_end_date)
            	SetEndDate(end_year, end_month, end_day);
            // set starting cash
            SetCash(starting_cash);
            
            // add all equities into the universe
            foreach(StockBase sb in manual_universe) {
	            AddEquity(sb.ticker, resolution);
	            
	            // create StockData variable for security
            	StockData sd = new StockData();
            	sd.algorithm = this;
            	sd.ticker = sb.ticker;
            	sd.alloc = sb.alloc;
            	sd.sma_base = new SimpleMovingAverage(length_base);
            	sd.sma_space = new SimpleMovingAverage(length_space);
            	sd.pos_space = new decimal[count_space];
            	sd.neg_space = new decimal[count_space];
            	// add stockdata to universe
            	universe.Add(sd);
            	
            	// set fee model to 0 since no fees
            	Securities[sd.ticker].FeeModel = new ConstantFeeModel(0);
            	
            	// define consolidator
            	// tick resolution
            	if(resolution == Resolution.Tick) {
            		var consolidator = new TickConsolidator(TimeSpan.FromTicks(resolution_consolidation));
            		consolidator.DataConsolidated += OnDataConsolidated;
            		SubscriptionManager.AddConsolidator(sd.ticker, consolidator);
            	}
            	// second resolution
            	else if(resolution == Resolution.Second) {
            		var consolidator = new TradeBarConsolidator(TimeSpan.FromSeconds(resolution_consolidation));
            		consolidator.DataConsolidated += OnDataConsolidated;
            		SubscriptionManager.AddConsolidator(sd.ticker, consolidator);
            	}
            	// minute resolution
            	else if(resolution == Resolution.Minute) {
            		var consolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(resolution_consolidation));
            		consolidator.DataConsolidated += OnDataConsolidated;
            		SubscriptionManager.AddConsolidator(sd.ticker, consolidator);
            	}
            	// hour resolution
            	else if(resolution == Resolution.Hour) {
            		var consolidator = new TradeBarConsolidator(TimeSpan.FromHours(resolution_consolidation));
            		consolidator.DataConsolidated += OnDataConsolidated;
            		SubscriptionManager.AddConsolidator(sd.ticker, consolidator);	
            	}
            	// daily resolution
            	else if(resolution == Resolution.Daily) {
            		var consolidator = new TradeBarConsolidator(TimeSpan.FromDays(resolution_consolidation));
            		consolidator.DataConsolidated += OnDataConsolidated;
            		SubscriptionManager.AddConsolidator(sd.ticker, consolidator);
            	} else {
            		Error("INVALID RESOLUTION TYPE");
            	}
            }
        }
        
        // method for entering positions
        // direction: true = long, false = short
        public void EnterPosition(StockData sd, bool direction) {
        	
        	// if currently in position verify direction with current position
        	// if long and open long or short and open short, return
        	if(sd.direction == 1 && direction || sd.direction == -1 && !direction)
        		return;
        		
        	// if position is open, liquidate ticker
        	if(Portfolio[sd.ticker].Invested)
        		Liquidate(sd.ticker);
        		
        	// determine entry contracts based on position sizing
        	// enter market orders
        	for(int i = 0; i < mo_percent_position.Count(); i++) {
        		// determine contract count
        		int contracts = (int)(Portfolio.MarginRemaining * sd.alloc * mo_percent_position[i] / Securities[sd.ticker].Price);
        		
        		// invert if short
        		if(!direction)
        			contracts *= -1;
        		
        		// place market order
        		MarketOrder(sd.ticker, contracts);
        	}
        	
        	// enter limit orders
        	for(int i = 0; i < lo_percent_position.Count(); i++) {
        		// determine contract count
        		int contracts = (int)(Portfolio.MarginRemaining * sd.alloc * lo_percent_position[i] / Securities[sd.ticker].Price);
        		
        		// invert if short
        		if(!direction)
        			contracts *= -1;
        		
        		// place limit order
        		LimitOrder(sd.ticker, contracts, Securities[sd.ticker].Price);
        	}
        	
        	// update stock data direction
        	sd.direction = direction ? 1 : -1;
    		
    		// log entry
    		//if(direction)
    		//	Debug($"LONG:   {sd.prev_index} -> {sd.index} <{Time}>");
    		//else
    		//	Debug($"SHORT:  {sd.prev_index} -> {sd.index} <{Time}>");
        }

        // OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
        // Slice object keyed by symbol containing the stock data
        public override void OnData(Slice data) {
        	// loops through each stock in universe
        	foreach(StockData sd in universe) {
        		
        		// if data is not ready then skip
        		if(!sd.IsReady)
        			continue;	
        		
        		//Debug(sd.IsLong() + " " + sd.IsShort() + $" {sd.direction} {sd.prev_index} -> {sd.index} <{Time}>");
        		
        		// update the index position
        		sd.UpdateIndex(Securities[sd.ticker]);
        		
        		// if short check for long
        		if(sd.direction == -1 && sd.IsLong()) {
        			EnterPosition(sd, true);
        		}
        		
        		// if long check for short
        		else if(sd.direction == 1 && sd.IsShort()) {
        			EnterPosition(sd, false);
        		}
        		
        		// if not in position wait for either
        		else if(sd.direction == 0) {
        			if(sd.IsLong()) {
        				EnterPosition(sd, true);
        			} else if(sd.IsShort()) {
        				EnterPosition(sd, false);
        			}
        		}
        	}
        }
        
        // OnDataConsolidated event runs when the data is properly consolidated at the bar level
        // will execute based on user parameters
        public void OnDataConsolidated(object sender, TradeBar bar) {
        	
        	// loops through each stock in universe
        	foreach(StockData sd in universe) {
        		// update values
        		sd.update();
        		
        		// if data is not ready then skip
        		if(!sd.IsReady)
        			continue;	
        		
        		//Debug(sd.IsLong() + " " + sd.IsShort() + $" {sd.direction} {sd.prev_index} -> {sd.index} <{Time}>");
        		
        		// update the index position
        		//sd.UpdateIndex(Securities[sd.ticker]);
        		/*
        		// if short check for long
        		if(sd.direction == -1 && sd.IsLong()) {
        			EnterPosition(sd, true);
        		}
        		
        		// if long check for short
        		else if(sd.direction == 1 && sd.IsShort()) {
        			EnterPosition(sd, false);
        		}
        		
        		// if not in position wait for either
        		else if(sd.direction == 0) {
        			if(sd.IsLong()) {
        				EnterPosition(sd, true);
        			} else if(sd.IsShort()) {
        				EnterPosition(sd, false);
        			}
        		}*/
        	}
        }
		
		// contains default information of ticker
		public class StockBase {
			// ticker name
			public string ticker;
			// portfolio cash allocation
			public decimal alloc;
			
			// constructor
			public StockBase(string ticker, decimal alloc) {
				this.ticker = ticker;
				this.alloc = alloc;
			}
		}
		
		// default class containing all ticker information
		public class StockData {
			// QCAlgorithm variable
			public QCAlgorithm algorithm;
			// contains base information of ticker
			public StockBase sb;
			// stock ticker
			public string ticker = "";
			// position allocation
			public decimal alloc;
			// simple moving average baseline (line 7)
			public SimpleMovingAverage sma_base;
			// spacing period (line 21)
			public SimpleMovingAverage sma_space;
			// stores all line space calculations
			public decimal[] pos_space;
			public decimal[] neg_space;
			// stores the prior and current index
			public int index = -10000;
			public int prev_index = -10000;
			// variable determining long, short, or null (1, -1, 0):
			public int direction = 0;
			
			// determines if data is ready
			public bool IsReady => sma_base.IsReady && sma_space.IsReady && index != -10000 && prev_index != -10000;
			
			// updates spacing calculations
			public void update() {
				// define security object
				Security sym = algorithm.Securities[ticker];
				
				// Update SMAs
				UpdateSMA(sym);
				
				// Update Spacing
				UpdateSpacing(sym);
				
				// Update Index
				UpdateIndex(sym);
			}
			
			// updates the SMAs used for calculation
			private void UpdateSMA(Security sym) {
				// update the base sma
				decimal price;
				if(Main.calculation_base == 1) {
					// OHLC4
					price = (sym.Open + sym.High + sym.Low + sym.Close) / 4;
				} else if(Main.calculation_base == 2) {
					// (H+L) / 2
					price = (sym.High + sym.Low) / 2;
				} else {
					// invalid value
					algorithm.Error("INVALID <calculation_base> VALUE");
					price = -1.0m;
				}
				// push to object
				sma_base.Update(algorithm.Time, price);
				
				// update the space sma
				if(Main.calculation_space == 1) {
					// (H-L)
					price = (sym.High - sym.Low);
				} else if(Main.calculation_space == 2) {
					// abs(C-O)
					price = Math.Abs(sym.Close - sym.Open);
				} else {
					// invalid value
					algorithm.Error("INVALID <calculation_space> VALUE");
					price = -1.0m;
				}
				// push to object
				sma_space.Update(algorithm.Time, price);
			}
			
			// updates the indicies of the spacing arrays
			private void UpdateSpacing(Security sym) {
				// determine if above or below base
				if(sym.Price > sma_base) {
					
					// set 0 as base
					pos_space[0] = sma_space;
					
					// update spacing and determine where the price is at
					for(int i = 1; i < pos_space.Count(); i++)
						pos_space[i] = sma_base + (sma_space * i * Main.factor_space);
					
				} else if(sym.Price < sma_base) {
					// set 0 as base
					neg_space[0] = sma_space;
					
					// update spacing and determine where the price is at
					for(int i = 1; i < neg_space.Count(); i++)
						neg_space[i] = sma_base - (sma_space * i * Main.factor_space);
					
				} else {
					// price == sma_base
					prev_index = index;
					index = 0;
				}
			}
			
			public void UpdateIndex(Security sym) {
				// determine if above or below base
				if(sym.Price > sma_base) {
					
					// update spacing and determine where the price is at
					for(int i = 1; i < pos_space.Count(); i++) {
						
						// if between prior index and current then set accoring
						if(sym.Price > pos_space[i - 1] && sym.Price < pos_space[i]) {
							prev_index = index;
							index = i;
						}
					}
					
				} else if(sym.Price < sma_base) {
					
					// update spacing and determine where the price is at
					for(int i = 1; i < neg_space.Count(); i++) {
						
						// if between prior index and current then set accoring
						if(sym.Price < neg_space[i - 1] && sym.Price > neg_space[i]) {
							prev_index = index;
							index = i * -1;
						}
					}
					
				} else {
					// price == sma_base
					prev_index = index;
					index = 0;
				}
				
				// make sure data is ready
				//if(IsReady)
					//algorithm.Debug($"{prev_index} -> {index} <{algorithm.Time}>");
			}
			
			// determines if the algorithm crossed long
			public bool IsLong() {
				return IsReady && index > prev_index;
			}
			
			// determine if the algorithm crossed short
			public bool IsShort() {
				return IsReady && index < prev_index;
			}
			
		}
    }
}