Overall Statistics |
Total Trades 17 Average Win 0.18% Average Loss -0.38% Compounding Annual Return -25.239% Drawdown 3.900% Expectancy -0.452 Net Profit -2.440% Sharpe Ratio -2.648 Probabilistic Sharpe Ratio 11.048% Loss Rate 62% Win Rate 38% Profit-Loss Ratio 0.46 Alpha -0.211 Beta 0.424 Annual Standard Deviation 0.08 Annual Variance 0.006 Information Ratio -2.357 Tracking Error 0.09 Treynor Ratio -0.496 Total Fees $17.00 Estimated Strategy Capacity $5600000.00 Lowest Capacity Asset AMZN R735QTJ8XC9X |
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // Volume Adjusted Relative Srength Trader // ---------------------------------------------------- // Ikezi Kamanu // Tristan Johnson // // Reference: // -------------- // https://www.reddit.com/r/RealDayTrading/comments/omw9rn/screenersscannerswatchlists/ // // Entry: // ------- // Select top X stocks with highest relative strength to SPY) // // Stocks with high volume // // Exit: // ------- // Exit when Trend power goes negative // // Todo and/or to consider // ------------------------- // 1. Make sure our consolidated bars start at the right time. // 2. Limit to SPY 500 stocks // 3. Never hold overnight. get out at EoD // 3. Add relative Spy Strength *on multiple time frames* as additional screening criteria, // 4. Consider market regime filters. eg: only short if spy is bearish. // 5. For exit, try checking for changes in trend direction (eg: going from positive to negative) // 7. Try a different "Power" (exponent) value for the trend power indicator // 8. Ensure price is above ichi moku cloud // // Consider: // ------------------- // 1. Daily Selection: If the Trend over past Ten hour-bars > 0 , add to universe // 2. Position Entry: If the Trend over past Thirty minute-bars < 0, buy // 3. Position Exit: When trend strength goes from positive to negative, or stop loss of X% // // Code checks: // 1. Check / refactor the use of tradeBar.Period in the history call, make sure it is always correcct // //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// using QuantConnect.Data.UniverseSelection; using System; using System.Collections.Generic; using System.Linq; namespace QuantConnect.Algorithm.CSharp { public class VARSTrader: QCAlgorithm { public bool forceAMZN; public int timeframeInMins; public int lookbackInMins; public int lookbackInBars; public int timeframeBInMins; public int lookbackBInMins; public int lookbackBInBars; public int screenTimeInMins; public int entryTimeInMins; public int exitCheckInterval; public Dictionary<Symbol, SymbolData> symDataDict; public Resolution resolution; public object SPY; public int rankingMetric; public int screeningMetric; public decimal pctPerHolding; public decimal pctTakeProfit; public decimal pctStopLoss; public decimal atrMultiplier; public int maxHoldings; public bool useTrendPowerExit; public bool useTrailStopExit; public Symbol AMZN; public IEnumerable<FineFundamental> myFineUniverse; // ==================================== public void InitInternalParams() { // Debug this.forceAMZN = true; // Set Primary Timeframe and lookback // ------------------------------ this.timeframeInMins = 60; this.lookbackInMins = 600; this.lookbackInBars = Convert.ToInt32(this.lookbackInMins / this.timeframeInMins); // Set Secondary Timeframe and lookback // --------------------------------- this.timeframeBInMins = 5; this.lookbackBInMins = 1950; this.lookbackBInBars = Convert.ToInt32(this.lookbackBInMins / this.timeframeBInMins); // screening timing this.screenTimeInMins = 60; // entry timing this.entryTimeInMins = 120; // exit timing this.exitCheckInterval = 120; // this.timeFrames = new Dictionary<object, object> {}; // this.timeFrames["5M"] = 5; // this.timeFrames["30M"] = 30; // this.timeFrames["1H"] = 60; // this.timeFrames["1D"] = 195; } // ===================================== public override void Initialize() { this.InitInternalParams(); this.InitExternalParams(); this.EnableAutomaticIndicatorWarmUp = true; this.SetStartDate(2020, 1, 1); this.SetEndDate(2020, 2, 1); this.SetCash(100000); // self.SetStartDate(2021, 4, 22) // self.SetEndDate(2021, 4, 30) this.resolution = Resolution.Minute; this.SPY = this.AddEquity("SPY", this.resolution).Symbol; // this.myFineUniverse = new IEnumerable<FineFundamental>(); this.UniverseSettings.Resolution = Resolution.Minute; // self.SPYSMA = self.SMA(self.SPY, 200, Resolution.Daily) // self.SPYMomPct = MomentumPercent('SPYMomPct', period = 10) // self.SetSecurityInitializer(lambda x: x.SetMarketPrice(self.GetLastKnownPrice(x))) // Try filtering SP500 // https://www.quantconnect.com/forum/discussion/7406/filtering-the-qc500-universe/p1 // ---------------------------------------------------------------------------------- // self.AddUniverse(self.CoarseSelectionFunction) this.AddUniverse(this.CoarseSelectionFunction, this.FineSelectionFunction); this.symDataDict = new Dictionary<Symbol, SymbolData> { }; this.ScheduleRoutines(); } // ===================================== public void InitExternalParams() { // --------------------------- // Populate External Params // --------------------------- this.rankingMetric = Convert.ToInt32(this.GetParameter("rankingMetric")); this.screeningMetric = Convert.ToInt32(this.GetParameter("screeningMetric")); this.pctPerHolding = Convert.ToDecimal(this.GetParameter("pctPerHolding")) / 100; this.pctTakeProfit = Convert.ToDecimal(this.GetParameter("pctTakeProfit")) / 100; this.pctStopLoss = Convert.ToDecimal(this.GetParameter("pctStopLoss")) / 100; this.atrMultiplier = Convert.ToDecimal(this.GetParameter("atrMultiplier")); this.maxHoldings = Convert.ToInt32(this.GetParameter("maxHoldings")); this.useTrendPowerExit = Convert.ToInt32(this.GetParameter("useTrendPowerExit")) == 1; this.useTrailStopExit = Convert.ToInt32(this.GetParameter("useTrailStopExit")) == 1; } // ===================================== public void ScheduleRoutines() { // # Schedule Screening routine run every day, X mins after market open // # ------------------------------------------------------------------ this.Schedule.On(this.DateRules.EveryDay(), this.TimeRules.AfterMarketOpen("SPY", this.screenTimeInMins - 1), this.ScreenUniverseStocks); // # Schedule Entry routine to run every day, X mins after market open // # ------------------------------------------------------------------ this.Schedule.On(this.DateRules.EveryDay(), this.TimeRules.AfterMarketOpen("SPY", this.entryTimeInMins - 1), this.ProcessEntrySignals); // Schedule Position Manager routine to run every X mins // ------------------------------------------------------------------ this.Schedule.On(this.DateRules.EveryDay(), this.TimeRules.Every(TimeSpan.FromMinutes(this.exitCheckInterval)), this.ManageOpenPositions); // Schedule Exit routine to run every X mins // ------------------------------------------------------------------ this.Schedule.On(this.DateRules.EveryDay(), this.TimeRules.Every(TimeSpan.FromMinutes(this.exitCheckInterval)), this.ProcessExits); } // ===================== public void ManageOpenPositions() { if (this.Securities["SPY"].Exchange.DateTimeIsOpen(this.Time)) { foreach (KeyValuePair<Symbol, SymbolData> symData in this.symDataDict) { Symbol symbol = symData.Key; if (this.symDataDict[symbol].IndicatorsAreSeededAndRegistered) { this.symDataDict[symbol].ManageOpenPosition(); } } } } // ===================== public void ProcessExits() { if (this.Securities["SPY"].Exchange.DateTimeIsOpen(this.Time)) { this.ManageOpenPositions(); this.ProcessExitSignals(); } } // ===================================== public void OnData(Slice dataSlice) { // Call the OnData methods for each symbol we are tracking foreach (KeyValuePair<Symbol, SymbolData> symData in this.symDataDict) { Symbol symbol = symData.Key; if (dataSlice.ContainsKey(symbol) && dataSlice[symbol] != null) { this.symDataDict[symbol].OnData(dataSlice[symbol]); } } } // Check for entries // ==================================================== public void ProcessEntrySignals() { // If the market isn't open, don't process any signals // ---------------------------------------------------- if (!this.Securities["SPY"].Exchange.DateTimeIsOpen(this.Time)) { return; } //# self.Debug(f"{self.Time} :::: PROCESS ENTRY SIGNALS ") var dataSlice = this.CurrentSlice; IEnumerable<SymbolData> topTrendersToBuy = new List<SymbolData>(); List<SymbolData> topTrendersSymData = new List<SymbolData>(); // IEnumerable<SymbolData> var numOfCurrentHoldings = (from x in this.Portfolio where x.Value.Invested select x.Key).ToList().Count; // IF we have less than our max holdings, add more, up to max. // ----------------------------------------------------------- if (numOfCurrentHoldings < this.maxHoldings) { var numOfHoldingsToAdd = this.maxHoldings - numOfCurrentHoldings; // Simplified code. Expanding for debugging sake // ------------------------------------------------- // trendersSymData = [self.symDataDict[symb] for symb in self.symDataDict \ // if (dataSlice.ContainsKey(symb) and self.symDataDict[symb].EntrySignalFired())] // Expanded Code // ------------------------------ var trendersSymData = new List<SymbolData>(); foreach (KeyValuePair<Symbol, SymbolData> symData in this.symDataDict) { Symbol symbol = symData.Key; if (dataSlice.ContainsKey(symbol)) { if (this.symDataDict[symbol].EntrySignalFired()) { trendersSymData.Add(this.symDataDict[symbol]); } } } topTrendersSymData = trendersSymData.OrderByDescending(symbolData => symbolData.RankingMetric).ToList(); topTrendersToBuy = topTrendersSymData.Take(numOfHoldingsToAdd); foreach (SymbolData symbolData in topTrendersToBuy) { var orderMsg = "Metric = {round(symbolData.RankingMetric * 100,3)}"; this.SetHoldings(symbolData.symbol, this.pctPerHolding, false, orderMsg); //# self.Debug(f"{self.Time} \t\t Bought {symbolData.symbol}") // else: //# self.Debug(f"{self.Time} \t\t Already @ Max Holdings") // remove stocks from universe that we didnt enter, or we are not invested in // Todo: consider doing this only at end of day, in case we want to try enterint // later in the day. // --------------------------------------------------------------------------- } } foreach (var kvp in this.Securities) { var symbol = kvp.Key; if (!this.Portfolio[symbol].Invested && !(from SymbolData x in topTrendersToBuy select x.symbol).ToList().Contains(symbol)) { this.RemoveSecurity(symbol); // if symbol != self.SPY: //# self.Debug(f"{self.Time} \t\t Removed Security - {symbol}") } } return; } // ============================ public void ProcessExitSignals() { foreach (var symbol in this.CurrentSlice.Keys) { if (this.symDataDict.ContainsKey(symbol)) { var symbolData = this.symDataDict[symbol]; if (this.Portfolio[symbol].Invested) { if (symbolData.ExitSignalFired()) { Plot("AMZN", "EXIT", symbolData.MomPct.Current.Value); this.Liquidate(symbol, symbolData.LastOrderMsg); this.RemoveSecurity(symbol); } } } } } // ===================================== private IEnumerable<Symbol> CoarseSelectionFunction(IEnumerable<CoarseFundamental> universe) { IEnumerable<CoarseFundamental> coarseuniverse = (from x in universe where x.HasFundamentalData && x.Price > 50 select x); coarseuniverse = coarseuniverse.OrderByDescending(x => x.DollarVolume).Take(200); return (from x in coarseuniverse select x.Symbol); } // ========================================================================== // Fine selection // --------------- // Select stocks with positive EV/EBITDA ratio and high market cap (> 2Billion) // Market cap is calculated using the BasicAverageShares in earnings reports. // Market cap = Shares Outstanding * Price = Shares Outstanding * (Earnings Per Share * PE ratio) // Price per share was extracted via a single history request. // // Then we ranked the universe by EV/EBITDA and select the top 100 // ========================================================================== private IEnumerable<Symbol> FineSelectionFunction(IEnumerable<FineFundamental> universe) { // Filter companies with certain security types, partnership types, etc // ---------------------------------------------------------------------- this.myFineUniverse = (from x in universe where (x.SecurityReference.IsPrimaryShare) && (x.SecurityReference.SecurityType == "ST00000001") && (x.SecurityReference.IsDepositaryReceipt) && (x.CompanyReference.IsLimitedPartnership) select x).ToList(); // Filter companies with positive EV-EBITDA ratio, and high market cap // ---------------------------------------------------------------------- try { this.myFineUniverse = (from x in this.myFineUniverse where (x.ValuationRatios.EVToEBITDA > 0.0M) && (x.EarningReports.BasicAverageShares.ThreeMonths > 0.0M) && (x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths * x.ValuationRatios.PERatio) > 2000000000.0M) select x); } catch { this.myFineUniverse = (from x in this.myFineUniverse where x.ValuationRatios.EVToEBITDA > 0.0M && x.EarningReports.BasicAverageShares.ThreeMonths > 0.0M select x).ToList(); } return new List<Symbol>(); } // Add screened stocks that meet our criteria // this should be backed / confirmed by volume // If the stocks met screening crtieria // we can subscribe to their data feed using addEquity // ===================================================== public void ScreenUniverseStocks() { var screenedStocks = this.GetScreenedStocks(); foreach (var symbol in screenedStocks) { if (!this.Securities.ContainsKey(symbol) || !this.ActiveSecurities.ContainsKey(symbol)) { this.AddEquity(symbol, this.resolution); } } } // ===================================== public IEnumerable<Symbol> GetScreenedStocks() { List<Symbol> selected = new List<Symbol>(); // ------------- // Debug: Force universe to be just AMZN, for Performance comparison // -------------------------------------------------------------- if (this.forceAMZN) { this.AMZN = QuantConnect.Symbol.Create("AMZN", SecurityType.Equity, Market.USA); FineFundamental tmpObject = new FineFundamental(); tmpObject.Symbol = this.AMZN; this.myFineUniverse = new List<FineFundamental> { tmpObject }; } foreach (var element in this.myFineUniverse) { Symbol symbol = element.Symbol; // Store data for this symbol, seed it with some history // ----------------------------------------------------- if (!this.symDataDict.ContainsKey(symbol)) { this.AddNewSymbolDataToDict(symbol); } SymbolData symbolData = this.symDataDict[symbol]; this.SeedSymbolData(symbolData); // If indicators are ready and criteria is met, it makes the cut. // -------------------------------------------------------------- if (symbolData.IndicatorsAreSeeded && this.symDataDict[symbol].ScreeningCriteriaMet()) { selected.Add(symbol); } } return selected; } // =================================== public void AddNewSymbolDataToDict(Symbol symbol) { SymbolData tmpSymbolData = new SymbolData(symbol, this); this.symDataDict[symbol] = tmpSymbolData; } // =================================== public void SeedSymbolData(SymbolData symbolData) { // Add an extra bar by increasing lookback + bar length var history = this.History(symbolData.symbol, this.lookbackInMins + this.timeframeInMins, this.resolution); symbolData.SeedIndicators(history); } // =================================== public override void OnSecuritiesChanged(SecurityChanges changes) { Symbol symbol; foreach (Security security in changes.AddedSecurities) { symbol = security.Symbol; if (symbol == this.SPY) { continue; } this.symDataDict[symbol].OnSecurityAdded(security); } // Investigate if this could possiby be triggered prematurely // ie: The security may not be in universe, but we are watching for signal // ---------------------------------------------------------------------- foreach (Security security in changes.RemovedSecurities) { symbol = security.Symbol; if (this.symDataDict.ContainsKey(symbol)) { this.symDataDict[symbol].OnSecurityRemoved(); this.symDataDict.Remove(symbol); } } } } }
using QuantConnect.Indicators; using QuantConnect.Algorithm.CSharp; namespace QuantConnect { // using AverageTrueRange = QuantConnect.Indicators.AverageTrueRange; // using AugenPriceSpike = QuantConnect.Indicators.AugenPriceSpike; // using IndicatorDataPoint = QuantConnect.Indicators.IndicatorDataPoint; public class SymbolData { public VARSTrader algo; public Symbol symbol; public decimal lastPrice; public object stopLossOrder; public object takeProfitOrder; public bool IndicatorsAreSeeded; public bool IndicatorsAreRegistered; public string LastOrderMsg; public decimal TrailingStopLoss; public decimal LastClose; public TradeBarConsolidator seedDataConsolidator; public MomentumPercent MomPct; public SimpleMovingAverage SMA; public AverageTrueRange ATR; public AugenPriceSpike APS; public SymbolData(Symbol theSymbol, VARSTrader algo) { // Algo / Symbol / Price / Order reference // ---------------------------------------- this.algo = algo; this.symbol = theSymbol; this.lastPrice = 0; this.stopLossOrder = null; this.takeProfitOrder = null; this.IndicatorsAreSeeded = false; this.IndicatorsAreRegistered = false; this.LastOrderMsg = "No Order Msg"; this.TrailingStopLoss = 0; this.LastClose = 0; // Initialize our indicators // ---------------------------------------- // self.TrendPower = TrendPower('MomPct', period = self.algo.lookbackInBars, power = 1.5) this.MomPct = new MomentumPercent(this.algo.lookbackInBars); this.SMA = new SimpleMovingAverage(this.algo.lookbackInBars); this.ATR = new AverageTrueRange(period: 5); this.APS = new AugenPriceSpike(period: 20); // if we are NOT using our custom consolidator, initialize the QC consolidator this.AddQCSeedDataConsolidator(); } // Set up consolidators for historical seed bars // https://www.quantconnect.com/forum/discussion/10414/open-to-feedback-and-or-questions-on-this-simple-options-algo/p1/comment-29630 // =================================== public void AddQCSeedDataConsolidator() { this.seedDataConsolidator = new TradeBarConsolidator(TimeSpan.FromMinutes(this.algo.timeframeInMins)); this.seedDataConsolidator.DataConsolidated += this.SeedDataConsolidatorHandler; } // # ======================================== public void SeedDataConsolidatorHandler(object sender, TradeBar tradeBar) { this.UpdateIndicatorsWithBars(tradeBar); } // =================================== public void SeedIndicators(IEnumerable<TradeBar> history_raw) { List<TradeBar> history = new List<TradeBar>(); foreach (TradeBar bar in history_raw) history.Add(bar); if (history.Count == 0) { this.IndicatorsAreSeeded = false; return; } else { foreach (var tradeBar in history) { this.seedDataConsolidator.Update(tradeBar); } this.IndicatorsAreSeeded = true; } /* // Loop over the history data and seed the indicator // ------------------------------------------------------------- if (history.empty || !history.columns.Contains("close")) { this.IndicatorsAreSeeded = false; return; } // Loop through bars and update indicators // # ---------------------------------------------------- foreach (var _tup_1 in history.iterrows()) { var index = _tup_1.Item1; var bar = _tup_1.Item2; var tradeBar = new TradeBar(); tradeBar.Close = bar.close; tradeBar.Open = bar.open; tradeBar.High = bar.high; tradeBar.Low = bar.low; tradeBar.Volume = bar.volume; // if using bars that have NOT been Consolidated // pass the minute trade bar to the QC consolidator // which will form the appropriate bars accordingly // -------------------------------------------------------- tradeBar.Period = TimeSpan.FromMinutes(1); tradeBar.Time = index[1]; this.seedDataConsolidator.Update(tradeBar); } this.IndicatorsAreSeeded = true; */ } // ======================================== // Once the security has been added, this means it successfully // used it's seed data to verify an entry signa. // Now we need to remove the seed data consolidators, and subscribe to data // todo: investigate why Consolidators arent orking as expected // ============================================ public void OnSecurityAdded(object security) { // self.RemoveSeedDataConsolidator() this.RegisterIndicators(); } // ========================================================= // Once the security has been removed, UnSubscribe from data // --------------- // Todo: Investigate if this could possiby be triggered prematurely // eg: if a stock is removed from the universe but we're still // interested in it, will the securitryRemoved handler get called? // ======================================================== public void OnSecurityRemoved() { // self.RemoveSeedDataConsolidator() return; } // ======================================================== public void RegisterIndicators() { this.algo.RegisterIndicator(this.symbol, this.ATR, TimeSpan.FromMinutes(this.algo.timeframeInMins)); this.algo.RegisterIndicator(this.symbol, this.APS, TimeSpan.FromMinutes(this.algo.timeframeInMins)); this.algo.RegisterIndicator(this.symbol, this.SMA, TimeSpan.FromMinutes(this.algo.timeframeInMins)); this.algo.RegisterIndicator(this.symbol, this.MomPct, TimeSpan.FromMinutes(this.algo.timeframeInMins)); this.IndicatorsAreRegistered = true; algo.Debug("RUNS"); } // ===================================== public void UpdateIndicatorsWithBars(TradeBar tradeBar) { if (tradeBar != null && this.MomPct != null && this.ATR != null) { // self.MomPct.Update(tradeBar) this.ATR.Update(tradeBar); this.MomPct.Update(new IndicatorDataPoint(tradeBar.EndTime, tradeBar.Close)); this.SMA.Update(new IndicatorDataPoint(tradeBar.EndTime, tradeBar.Close)); this.APS.Update(new IndicatorDataPoint(tradeBar.EndTime, tradeBar.Close)); } } public bool IndicatorsAreSeededAndRegistered { get { return this.IndicatorsAreSeeded && this.IndicatorsAreRegistered; } } public object RankingMetric { get { if (this.IndicatorsAreSeeded) { if (this.algo.rankingMetric == 1) { return this.MomPct.Current.Value; } if (this.algo.rankingMetric == 2) { return this.APS.Current.Value; } return null; } else { return null; } } } // ===================================== // ===================================== // ===================================== // ========================================== // Screening crtieria should include volume // ========================================== public bool ScreeningCriteriaMet() { if (this.algo.forceAMZN) { return true; } if (this.IndicatorsAreSeeded) { if (this.algo.screeningMetric == 1) { return this.MomPct.Current.Value > 0; } if (this.algo.screeningMetric == 2) { if (this.MomPct.Current.Value > 0 && this.APS.Current.Value > 0) { return true; } else { return false; } } } else { return false; } return false; } // ===================================== public bool EntrySignalFired() { if (this.IndicatorsAreSeeded) { return this.MomPct.Current.Value > 0; // return (self.SMA.Current.Value < self.LastClose) // ===================================== } return false; } public bool ExitSignalFired() { decimal pctProfit = 0.0M; pctProfit = this.algo.Portfolio[this.symbol].UnrealizedProfitPercent; var profitLabel = Decimal.Round(pctProfit * 100, 4); // Exit: % StopLoss Exit // ---------------------- if (this.algo.pctStopLoss > 0 && pctProfit <= -this.algo.pctStopLoss) { this.LastOrderMsg = $"Pct Loss Exit @ {profitLabel}% Profit"; return true; } // Exit: % Take Profit Exit // ---------------------- if (this.algo.pctTakeProfit > 0 && pctProfit >= this.algo.pctTakeProfit) { this.LastOrderMsg = $"Pct Profit Exit @ {profitLabel}% Profit"; return true; } // Exit: Trailing Stop-Loss Exit (Breakeven or above) // ---------------------------------------------- if (this.algo.useTrailStopExit && pctProfit > 0 && this.LastClose < this.TrailingStopLoss) { this.LastOrderMsg = $"Trailing SL @ {profitLabel}% Profit ({LastClose} < {TrailingStopLoss})"; return true; } // Exit: Negative Trend Power // ---------------------------------------------- // tmp SMA check // if self.algo.useTrendPowerExit and (self.SMA.IsReady) and (self.SMA.Current.Value > self.LastClose): if (this.algo.useTrendPowerExit && this.MomPct.IsReady && this.MomPct.Current.Value < 0) { this.LastOrderMsg = $"Trend Power Exit @ {profitLabel}% Profit power={MomPct.Current.Value}"; return true; } return false; } // ===================================== public void OnData(TradeBar DataSliceBar) { this.LastClose = DataSliceBar.Close; } // ===================================== public void ManageOpenPosition() { if (this.algo.useTrailStopExit) { this.UpdateStopLoss(); } } // Todo: Add logic to activate stop loss after a certain value // (maybe after breakeven, when UnrealizedProfitPercent >= 0) // Try Different values for multiplier // ===================================== public void UpdateStopLoss() { var newStopLoss = this.LastClose - this.ATR.Current.Value * this.algo.atrMultiplier; this.TrailingStopLoss = Math.Max(this.TrailingStopLoss, newStopLoss); } } }