| Overall Statistics |
|
Total Trades 237 Average Win 1.22% Average Loss -1.58% Compounding Annual Return 88.652% Drawdown 8.900% Expectancy 0.306 Net Profit 75.060% Sharpe Ratio 3.289 Probabilistic Sharpe Ratio 98.162% Loss Rate 26% Win Rate 74% Profit-Loss Ratio 0.77 Alpha 0.574 Beta -0.017 Annual Standard Deviation 0.175 Annual Variance 0.031 Information Ratio 2.495 Tracking Error 0.273 Treynor Ratio -34.539 Total Fees $512.04 Estimated Strategy Capacity $2000000.00 Lowest Capacity Asset SNTG XPY9HNNNXVFP |
#region imports
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
public class GapUpShort : QCAlgorithm
{
/** REFERENCE LINKS **/
// https://www.quantconnect.com/forum/discussion/4010/pre-market-scanning/p1
// https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/trade-fills/key-concepts
// https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/trade-fills/supported-models
// https://www.quantconnect.com/docs/v2/writing-algorithms/reality-modeling/slippage/key-concepts
/** CONSTANTS **/
private const string BENCHMARK = "SPY";
public const string BUY_TAG = "BUY";
public const string SELL_TAG = "SELL";
private const decimal MINIMUM_GAP_UP_PERCENTAGE = 29;
private const decimal MAXIMUM_MARKET_CAP = 1_000_000_000;
private const decimal MINIMUM_PREMARKET_VOLUME = 500_000;
private const decimal UNIVERSE_ENTRY_CAP = MAXIMUM_MARKET_CAP * 2;
private const decimal MINIMUM_OPEN_PRICE = 2;
private const decimal PERCENT_EXPOSURE_PER_POSITION = 6;
private const decimal STOP_LOSS_PERCENT = 50;
const bool ENABLE_TESTING = true;
/** DATA TRACKERS **/
/// Indicator Data Tracking
List<Symbol> _symbolTracker = new List<Symbol>();
Dictionary<Symbol, SymbolData> _symbolDict = new Dictionary<Symbol, SymbolData>();
Dictionary<Symbol, TradeStruct> _tradeInfo = new Dictionary<Symbol, TradeStruct>();
/** PRIVATE VARS **/
string[] _TestShortsListNov2022 =
{"FRZA", "VRAX", "PKBO", "SNTG", "GCT", "MACK", "SNAL", "QH", "PXMD"}; /// Test Period | Nov 1 2022 - Nov 20 2022
///string[] _TestShortsList = {"AFAQ", "PDSB", "IMMX", "CELZ", "LIXT"};
string[] _TestShortsList = {"IMMX", "CELZ", "IMMX", "CELZ", "LIXT", "MYNZ", "LIXT", "MYNZ", "EAR", "EAR", "HGSH", "ABSI", "HGSH", "ABSI", "SBEV", "SBEV", "BSFC", "BSFC", "BTBD", "BTBD", "IINN", "IINN", "OLB", "OLB", "ESM", "DHBC", "ESM", "DHBC", "NRGV", "NRGV", "TEN", "TEN", "INDO", "INDO", "INDO", "INDO", "NINE", "HUSA", "HUSA", "NINE", "USWS", "USEG", "MARPS", "HUSA", "APMI", "USWS", "USEG", "MARPS", "HUSA", "APMI", "SBFM", "DRCT", "SBFM", "AGRI", "INDO", "AGRI", "INDO", "HYMC", "HYMC", "HYMC", "HYMC", "AKAN", "AKAN", "PIK", "PIK", "NRSN", "NRSN", "CELZ", "CELZ", "IGMS", "IGMS", "DRCT", "ADGI", "DRCT", "ADGI", "CLVS", "LGVN", "LGVN", "CLVS", "ACER", "ACER", "SBFM", "SBFM", "MOBQ", "MOBQ", "MNTS", "MNTS", "IVDA", "VERU", "VERU", "IVDA", "LIXT", "LIXT", "CASA", "CASA", "STSS", "STSS", "CYN", "CYN", "VLON", "VLON", "NKTX", "NKTX", "VIVK", "VIVK", "IDAI", "IDAI", "SNOA", "SNOA", "RVSN", "RVSN", "SIDU", "SIDU", "SOPA", "SOPA", "VERU", "VERU", "DTST", "DTST", "NURO", "NURO", "IMMX", "IMMX", "STON", "STON", "SPRC", "SPRC", "TXMD", "JAN", "TXMD", "JAN", "AMLX", "AMLX", "YMTX", "YMTX", "YMTX", "YMTX", "JAN", "JAN", "AUVI", "AUVI", "COGT", "COGT", "DAWN", "DAWN", "EVOK", "SIDU", "SIDU", "EVOK", "ADN", "SIDU", "ADN", "SIDU", "BKSY", "SVFC", "BKSY", "SVFC", "FSTX", "FSTX", "FSTX", "FSTX", "TBLT", "TBLT", "AXSM", "NRSN", "AXSM", "NRSN", "HMCOU", "HMCOU", "RPID", "NRSN", "RPID", "NRSN", "RBCN", "RBCN", "RFP", "IINN", "RFP", "IINN", "TBLT", "PLRX", "TBLT", "PLRX", "HSTO", "GOEV", "INDO", "HSTO", "GOEV", "HSTO", "GOEV", "INDO", "HSTO", "GOEV", "USEA", "USEA", "ADN", "ADN", "DRCT", "DRCT", "SDIG", "SDIG", "HUSN", "HUSN", "BWV", "BWV", "TBLT", "BTTX", "TBLT", "BTTX", "PRPB", "AYLA", "PRPB", "AYLA", "PSTX", "PSTX", "NNVC", "SOPA", "NVIV", "NVIV", "NNVC", "SOPA", "MOBQ", "IONM", "MOBQ", "IONM", "BBBY", "BNSO", "BBBY", "BNSO", "CLWT", "CLWT", "DRCT", "ARHS", "DRCT", "ARHS", "VRDN", "VRDN", "BWV", "EQHA", "HIL", "BWV", "EQHA", "HIL", "VLCN", "VLCN", "AERI", "AERI", "OLB", "OLB", "BRPM", "BRPM", "ECOM", "ECOM", "SIDU", "SIDU", "PIXY", "PIXY", "INM", "INM", "ETNB", "AKRO", "ETNB", "AKRO", "BIAF", "BIAF", "PRPL", "PRPL", "VRAX", "VRAX", "LVTX", "LVTX", "ATXI", "ATXI", "ABOS", "BIAF", "ABOS", "BIAF", "MOTS", "MOTS", "LITM", "FXLV", "AIMD", "LITM", "FXLV", "AIMD", "ATXI", "ATXI", "CLAQ", "CLAQ", "BEAT", "MOTS", "BEAT", "MOTS", "LUCY", "LUCY", "IMUX", "SOBR", "IMUX", "SOBR", "DICE", "DICE", "HPCO", "HPCO", "LASE", "LASE", "BMRA", "BMRA", "AKUS", "AVEO", "AKUS", "AVEO", "USER", "AGFS", "GATE", "USER", "AGFS", "GATE", "NUVL", "NUVL", "TWNT", "EFSH", "TWNT", "EFSH", "FRZA", "FRZA", "VRAX", "VRAX", "IGNY", "SNTG", "IGNY", "SNTG", "GCT", "OYST", "GCT", "OYST", "MACK", "MACK", "AFAQ", "AFAQ", "SNAL", "SNAL", "PDSB", "PDSB", "PXMD", "PXMD", };
/// NYS = NYSE
/// NAS = NASDAQ
/// ASE = AMEX
/// See: https://www.quantconnect.com/forum/discussion/12234/exchange-id-mapping/p1
/// See: https://www.quantconnect.com/forum/discussion/11121/how-to-backtest-and-live-trade-on-chinese-stocks/p1
string[] _ValidExchanges = {"NYS", "NAS", "ASE"};
private DateTime _startDate = new DateTime(2022, 1, 1);
private DateTime _endDate = new DateTime(2022, 11, 20);
private int _accountStartSize = 20_000;
/// Initialize and Prepare Algo
/// Warm-up Data and Indicators
public override void Initialize()
{
/// Date Setup
SetStartDate(_startDate);
SetEndDate(_endDate);
/// Account Setup
SetCash(_accountStartSize);
/// Benchmark Setup
var lBenchmarkSecurity = AddEquity(BENCHMARK, Resolution.Daily);
SetBenchmark(BENCHMARK);
/// Universe Setup
UniverseSettings.Resolution = Resolution.Minute;
UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted;
UniverseSettings.ExtendedMarketHours = true;
EnableAutomaticIndicatorWarmUp = true;
/// SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage);
AddUniverseSelection(new FineFundamentalUniverseSelectionModel(SelectCoarse, SelectFine));
var lMarketOpenHour = 9;
var lMarketOpenMinute = 30;
Schedule.On(Schedule.DateRules.EveryDay(),
Schedule.TimeRules.At(new TimeSpan(lMarketOpenHour, lMarketOpenMinute, 0)), OnEachMarketOpen);
Schedule.On(Schedule.DateRules.EveryDay(),
Schedule.TimeRules.BeforeMarketClose(BENCHMARK, 0), OnEachMarketClose);
/// Warmup One Week Worth Of Data
SetWarmUp(new TimeSpan(2, 0, 0, 0));
}
public void OnData(TradeBars aTradeBars)
{
/// QuantConnect Seemingly Has a Bug where using StopMarketOrders with
/// a ticker that has the same Open High Low Close, causes erroneous fills
/// so instead I use a direct Liquidation once the high exceeds the stop
foreach(var lKeyValuePair in Portfolio)
{
Symbol lSymbol = lKeyValuePair.Key;
if (aTradeBars.ContainsKey(lSymbol))
{
var lTradeBar = aTradeBars[lSymbol];
if(lKeyValuePair.Value.Invested)
{
if(_tradeInfo.ContainsKey(lSymbol))
{
if(lTradeBar.High > _tradeInfo[lSymbol].StopLoss)
{
Liquidate(lSymbol, tag: BUY_TAG);
Log("Closing Order On: " + lSymbol);
_tradeInfo.Remove(lSymbol);
}
}
}
}
}
}
public void OnEachMarketClose()
{
/// Cover All Positions At Open
/// Using This Logic In Place of Liquidation to Avoid Cancellation Issues
foreach(var position in _tradeInfo)
{
if(ActiveSecurities[position.Key].Invested)
{
MarketOnOpenOrder(position.Key, position.Value.ShareQuantity, tag: BUY_TAG);
Log("Closing Order On: " + position.Key);
}
}
_tradeInfo.Clear();
}
/// Called On Each Open To Execute Gap Up Short Criteria
public void OnEachMarketOpen()
{
/// Ignore Warmup Periods
if(IsWarmingUp) return;
/// Ignore Mondays
if(Time.Day == (int)DayOfWeek.Monday) return;
decimal lGapPercentMultiplier = (100 + MINIMUM_GAP_UP_PERCENTAGE) / 100;
/// Get All Potential Setups
List<Symbol> lPotentialShortSetups = (from pair in ActiveSecurities
where _symbolDict.ContainsKey(pair.Key)
where pair.Value.Fundamentals != null
where pair.Value.Fundamentals.MarketCap <= MAXIMUM_MARKET_CAP
where pair.Value.Close > MINIMUM_OPEN_PRICE
select pair.Key).ToList();
/// Final Selection Process = Check Premarket Volume, We
/// do this last as we have to do a full history request and that is more taxing performance wise
var lPreMarketStartHour = 4;
DateTime lStartTime = new DateTime(Time.Year, Time.Month, Time.Day, lPreMarketStartHour, 0, 0);
List<Symbol> lShortSetups = new List<Symbol>();
foreach(var lSymbol in lPotentialShortSetups)
{
/// Get Historical Data For Checking Prior Highs + Gaps
bool lProperPricing = false;
var lPriorDayBarHistory = History<TradeBar>(lSymbol, new TimeSpan(1,0,0,0), Resolution.Daily);
if(!lPriorDayBarHistory.IsNullOrEmpty())
{
TradeBar lPriorDayBar = lPriorDayBarHistory.First();
if(ActiveSecurities[lSymbol].Close > lPriorDayBar.High &&
ActiveSecurities[lSymbol].Close > lPriorDayBar.Close * lGapPercentMultiplier)
{
lProperPricing = true;
}
}
if(!lProperPricing)
{
continue;
}
/// Get All Premarket Data For Volume Purposes
var lTradeBars = History<TradeBar>(lSymbol, lStartTime, Time);
decimal lTotalVolume = 0;
foreach(var lTradebar in lTradeBars)
{
lTotalVolume += lTradebar.Volume;
}
if(lTotalVolume > MINIMUM_PREMARKET_VOLUME)
{
lShortSetups.Add(lSymbol);
}
}
decimal lCurrentPortfolioValue = Portfolio.TotalPortfolioValue;
foreach(var lSymbolToShort in lShortSetups)
{
/// No Duplicate Trades
if(!ActiveSecurities[lSymbolToShort].Invested)
{
/// Calculate Stop
decimal lStopLoss = ActiveSecurities[lSymbolToShort].Close * ConvertPercentToPositiveMultiplier(STOP_LOSS_PERCENT);
/// Calculate Exposure
var lNumberOfSharesToShort = GetNumberOfSharesToShort(lCurrentPortfolioValue, lSymbolToShort);
/// Track Stop & Number Of Shares to Short
_tradeInfo[lSymbolToShort] = new TradeStruct(lStopLoss, lNumberOfSharesToShort);
/// Place Order Immediately At Open
MarketOrder(lSymbolToShort, -lNumberOfSharesToShort, false, SELL_TAG);
Log("Opening Trade on - " + lSymbolToShort);
if(!_symbolTracker.Contains(lSymbolToShort))
{
_symbolTracker.Add(lSymbolToShort);
}
}
}
}
/// Called At Exit of Algorithm
public override void OnEndOfAlgorithm()
{
string lSymbolsString = "string[] _TestShortsList = {";
foreach(var symbol in _symbolTracker)
{
lSymbolsString += "\"" + symbol.Value + "\", ";
}
lSymbolsString += " };";
Log(lSymbolsString);
base.OnEndOfAlgorithm();
}
/// Called Based On Changes To Current Stock Universe
public override void OnSecuritiesChanged(SecurityChanges changes)
{
// If we have no changes, do nothing
if (changes == SecurityChanges.None) return;
foreach (var security in changes.AddedSecurities)
{
/// Set Leverage to 1 For Added Securities
security.SetLeverage(1);
/// Override QC Default Fill Model
/*security.SetFillModel(new ImmediateFillModel());
security.SetSlippageModel(new ConstantSlippageModel(0));
security.SetFeeModel(new ConstantFeeModel(0));*/
_symbolDict[security.Symbol] = new SymbolData(this, security.Symbol);
}
// If you have a dynamic universe, track removed securities
foreach (var security in changes.RemovedSecurities)
{
if (_symbolDict.TryGetValue(security.Symbol, out var lSymbolData))
{
lSymbolData.Dispose();
_symbolDict.Remove(security.Symbol);
}
}
}
/// Coarse Universe Selector
IEnumerable<Symbol> SelectCoarse(IEnumerable<CoarseFundamental> coarse)
{
var lStocks = (from security in coarse
where security.Volume > 0 && security.Price > 1
where DoesTickerExist(security.Symbol.Value)
select security.Symbol).ToList();
return lStocks;
}
/// Fine Universe Selector
IEnumerable<Symbol> SelectFine(IEnumerable<FineFundamental> fine)
{
var filteredFine =
(from security in fine
where _ValidExchanges.Contains(security.CompanyReference.PrimaryExchangeID)
where security.MarketCap < MAXIMUM_MARKET_CAP
orderby security.MarketCap descending
select security.Symbol).ToList();
return filteredFine;
}
/// If Testing Is Enabled. We Only Run The Code With Our Test List
public bool DoesTickerExist(string ticker)
{
if(!ENABLE_TESTING)
{
return true;
}
bool lTickerExists = false;
foreach(var symbol in _TestShortsList)
{
if(ticker == symbol)
{
lTickerExists = true;
break;
}
}
return lTickerExists;
}
public decimal ConvertPercentToNegativeMultiplier(decimal aPercentage)
{
return aPercentage / 100;
}
public decimal ConvertPercentToPositiveMultiplier(decimal aPercentage)
{
return 1 + (aPercentage / 100);
}
public int GetNumberOfSharesToShort(decimal aCurrentPortfolioValueAtOpen, Symbol aSymbol)
{
decimal lTotalDollarExposure = (aCurrentPortfolioValueAtOpen) * ConvertPercentToNegativeMultiplier(PERCENT_EXPOSURE_PER_POSITION);
int lNumberOfSharesToShort = (int) Math.Round(lTotalDollarExposure / ActiveSecurities[aSymbol].Close);
return lNumberOfSharesToShort;
}
}
public class TradeStruct
{
private decimal _stopLoss;
private decimal _shareQuantity;
public TradeStruct(decimal aStopLoss, decimal aShareQuantity)
{
_stopLoss = aStopLoss;
_shareQuantity = aShareQuantity;
}
public decimal StopLoss
{
get
{
return _stopLoss;
}
}
public decimal ShareQuantity
{
get
{
return _shareQuantity;
}
}
}
/// Handles Tracking Indicators and Warming Up the Data For Indicators as well as
/// attaching to the Algorithm
public class SymbolData
{
private QCAlgorithm _algorithm;
private Symbol _symbol;
private TradeBarConsolidator _consolidator;
/// Daily Indicators
public Maximum High;
public Maximum Close;
public SymbolData(QCAlgorithm algorithm, Symbol symbol)
{
_algorithm = algorithm;
_symbol = symbol;
// Create Indicators
High = new Maximum(1);
Close = new Maximum(1);
// Create a consolidator to update the indicator
_consolidator = new TradeBarConsolidator(TimeSpan.FromDays(1));
_consolidator.DataConsolidated += DailyDataUpdate;
// Register the consolidator to update the indicator
algorithm.SubscriptionManager.AddConsolidator(symbol, _consolidator);
// Warm Up Indicators
algorithm.WarmUpIndicator(symbol, High);
algorithm.WarmUpIndicator(symbol, Close);
}
/// Updates Indicators that run on specifically at a daily resolution
/// Updates Once a Day
private void DailyDataUpdate(object sender, TradeBar consolidatedBar)
{
High.Update(consolidatedBar.EndTime, consolidatedBar.High);
Close.Update(consolidatedBar.EndTime, consolidatedBar.Close);
}
// If you have a dynamic universe, remove consolidators for the securities removed from the universe
public void Dispose()
{
_algorithm.SubscriptionManager.RemoveConsolidator(_symbol, _consolidator);
}
}
}