Overall Statistics |
Total Trades 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Net Profit 0% Sharpe Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio -0.016 Tracking Error 0.101 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset |
using System; namespace GoldenCross { class Program { static void Main(string[] args) { Console.WriteLine("Hello World!"); } } }
/* * Copyright (C) 2022 by Lanikai Studios, Inc. - All Rights Reserved * * This code is not to be distributed to others, it is confidential information of Lanikai Studios. * * This code was built from open source Python code written by Aaron Eller - www.excelintrading.com */ using QuantConnect; using QuantConnect.Algorithm; using QuantConnect.Brokerages; using QuantConnect.Data; using QuantConnect.Data.Consolidators; using QuantConnect.Data.Fundamental; using QuantConnect.Data.UniverseSelection; using QuantConnect.Orders; using QuantConnect.Scheduling; using QuantConnect.Securities; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using static Lanikai.GlobalSettings; namespace Lanikai { /// <summary> /// The framework for the Lanikai algo. /// </summary> public class LanikaiQCAlgorithm : QCAlgorithm { /// <summary> /// The benchmark for comparison. Normally set to SPY. /// </summary> private Symbol bm; /// <summary> /// Used for benchmark plotting code. /// </summary> private decimal? bm_first_price; /// <summary> /// Used for benchmark plotting code. /// </summary> private int bm_plot_counter = 0; /// <summary> /// The data for each symbol we are following. /// </summary> private IDictionary<Symbol, SymbolData> symbol_data; /// <summary> /// true when we need to update the universe. /// </summary> private bool update_universe; /// <summary> /// The max number of positions allowed. /// </summary> private int maxPositions; /// <summary> /// The maximum percentage of the budget to spend on any one stock. /// </summary> private float maxPctPerPosition; /// <summary> /// Long exit when RSI crosses below this value /// </summary> public decimal RsiExit1Value { get; private set; } /// <summary> /// Long exit when RSI crosses below this value /// </summary> public decimal RsiExit2Value { get; private set; } // Initialize algorithm. public override void Initialize() { // Set backtest details SetBacktestDetails(); // Add instrument data to the algo AddInstrumentData(); // Schedule functions ScheduleFunctions(); // initialize each security with today's prices SetSecurityInitializer(Initialize); // Warm up the indicators prior to the start date // this.SetWarmUp() // This doesn't work with universe filters // Instead we'll use History() to warm up indicators when SymbolData class objects get created var strParam = GetParameter("MAX_POSITIONS"); maxPositions = strParam == null ? MAX_POSITIONS : Convert.ToInt32(strParam); maxPctPerPosition = 1.0f / maxPositions; var strRsiExit1Value = GetParameter("RSI_EXIT_1_VALUE"); RsiExit1Value = strRsiExit1Value == null ? RSI_EXIT_1_VALUE : Convert.ToInt32(strRsiExit1Value); var strRsiExit2Value = GetParameter("RSI_EXIT_2_VALUE_ADD"); RsiExit2Value = strRsiExit2Value == null ? RSI_EXIT_2_VALUE : RsiExit1Value - Convert.ToInt32(strRsiExit2Value); if (PRINT_SETTINGS) { Log($"maxPositions = {maxPositions}, maxPctPerPosition = {maxPctPerPosition}"); Log($"rsiExit1Value = {RsiExit1Value}."); Log($"rsiExit2Value = {RsiExit2Value}."); Log($"Stop loss: {SL_PCT * 100}%"); } } private void Initialize (Security security) { security.SetMarketPrice(GetLastKnownPrice(security)); } /// <summary> /// End of algorithm run event handler. This method is called at the end of a backtest or /// live trading operation. Intended for closing out logs. /// </summary> public override void OnEndOfAlgorithm() { // Check if we want to plot the benchmark if (PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART) PlotBenchmarkOnEquityCurve(true); if (PRINT_VERBOSE || PRINT_ORDERS) Log($"End of backtest at {Time}"); if (PRINT_ORDERS) { Log("Portfolio:"); var positions = (from symbol in Portfolio.Keys where Portfolio[symbol].Invested select symbol).ToList(); foreach (var sym in positions) Log($"{sym} holding {Portfolio[sym].Quantity} shares at {Portfolio[sym].Price:C}"); } } /// <summary> /// Set the backtest details. /// </summary> public void SetBacktestDetails() { SetStartDate(START_DATE.Year, START_DATE.Month, START_DATE.Day); SetEndDate(END_DATE.Year, END_DATE.Month, END_DATE.Day); SetCash(CASH); SetTimeZone(TIMEZONE.Id); // Setup trading framework // Transaction and submit/execution rules will use IB models // brokerages: https://github.com/QuantConnect/Lean/blob/master/Common/Brokerages/BrokerageName.cs // account types: AccountType.Margin, AccountType.Cash SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Cash); // Configure all universe securities // This sets the data normalization mode // You can also set custom fee, slippage, fill, and buying power models // see if SubscriptionManager.SubscriptionDataConfigService has SetDataNormalizationMode() if the root class is cast to Equity this.SetSecurityInitializer(new MySecurityInitializer()); // Adjust the cash buffer from the default 2.5% to custom setting Settings.FreePortfolioValuePercentage = (decimal)FREE_PORTFOLIO_VALUE_PCT; } // Add instrument data to the algo. public void AddInstrumentData() { // Set data resolution based on input Resolution resolution; switch (DATA_RESOLUTION) { case "SECOND": resolution = Resolution.Second; break; case "MINUTE": resolution = Resolution.Minute; break; case "HOUR": resolution = Resolution.Hour; break; default: throw new ApplicationException($"Must set resolution to SECOND, MINUTE, or HOUR. Is set to {DATA_RESOLUTION}."); } // Define the desired universe this.AddUniverse(this.CoarseSelectionFunction, this.FineSelectionFunction); // Set universe data properties desired UniverseSettings.Resolution = resolution; UniverseSettings.ExtendedMarketHours = false; UniverseSettings.DataNormalizationMode = DataNormalizationMode.Adjusted; UniverseSettings.MinimumTimeInUniverse = new TimeSpan(MIN_TIME_IN_UNIVERSE, 0, 0, 0); // Add data for the benchmark and set benchmark // Always use minute data for the benchmark bm = AddEquity(BENCHMARK, Resolution.Minute).Symbol; SetBenchmark(BENCHMARK); // Create a dictionary to hold SymbolData class objects symbol_data = new Dictionary<Symbol, SymbolData>(); // Create a variable to tell the algo when to update the universe update_universe = true; } /// <summary> /// Scheduling the functions required by the algo. /// </summary> public void ScheduleFunctions() { // For live trading, universe selection occurs approximately 04:00-07:00am EST on Tue-Sat. // For backtesting, universe selection occurs at 00:00am EST (midnight) // We need to update the self.update_universe variable // before both of these scenarios are triggered // Desired order of events: // 1. Update the self.update_universe variable True // end of week/month, 5 min after market close // 2. Coarse/Fine universe filters run and update universe // run everyday at either 00:00 or 04:00 EST // Update self.update_universe variable True when desired IDateRule date_rules; switch (UNIVERSE_FREQUENCY) { case "daily": date_rules = DateRules.EveryDay(this.bm); break; case "weekly": // Want to schedule at end of the week, so actual update on start // of the next week date_rules = DateRules.WeekEnd(this.bm); break; case "monthly": // Want to schedule at end of the month, so actual update on // first day of the next month date_rules = DateRules.MonthEnd(this.bm); break; default: throw new ApplicationException($"UNIVERSE_FREQUENCY needs to be set to daily, weekly, or monthly. Is set to {UNIVERSE_FREQUENCY}."); } // Timing is after the market closes Schedule.On(date_rules, TimeRules.BeforeMarketClose(this.bm, -5), this.UpdateUniverse); // Calling -5 minutes "BeforeMarketClose" schedules the function 5 // minutes "after market close" // Calling -5 minutes "AfterMarketOpen" schedules the function 5 // minutes "before market open" // Now the coarse/fine universe filters will run automatically either at // 00:00(midnight) EST for backtesting or // 04:00am EST for live trading // Check for new signals SIGNAL_CHECK_MINUTES before the market open Schedule.On(DateRules.EveryDay(this.bm), TimeRules.AfterMarketOpen(this.bm, -SIGNAL_CHECK_MINUTES), this.CheckForSignals); // Check if end of day exit is desired if (EOD_EXIT) { Utils.Trap(); // Schedule function to liquidate the portfolio EOD_EXIT_MINUTES // before the market close Schedule.On(DateRules.EveryDay(this.bm), TimeRules.BeforeMarketClose(this.bm, EOD_EXIT_MINUTES), this.LiquidatePortfolio); } // Check if we want to plot the benchmark on the equity curve if (PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART) // Schedule benchmark end of day event 5 minutes after the close Schedule.On(DateRules.EveryDay(this.bm), TimeRules.BeforeMarketClose(this.bm, -5), this.BenchmarkOnEndOfDay); else Utils.Trap(); } /// <summary> /// Event called when re-balancing is desired. /// </summary> public void UpdateUniverse() { // Update variable to trigger the universe to be updated update_universe = true; } // // Perform coarse filters on universe. // Called once per day. // Returns all stocks meeting the desired criteria. // // Attributes available: // .AdjustedPrice // .DollarVolume // .HasFundamentalData // .Price -> always the raw price! // .Volume // /// <summary> /// Perform coarse filters on universe. Called once per day. /// /// Attributes available: /// .AdjustedPrice /// .DollarVolume /// .HasFundamentalData /// .Price -> always the raw price! /// .Volume /// </summary> /// <param name="coarse">Summary information for each stock.</param> /// <returns>All stocks meeting the desired criteria.</returns> public IEnumerable<Symbol> CoarseSelectionFunction(IEnumerable<CoarseFundamental> coarse) { // # Testing - catch specific symbol // for x in coarse: // # if str(x.Symbol).split(" ")[0] in ['AAPL']: // if x.Symbol.ID.Symbol in ['AAPL']: // # Stop and debug below // print(x) // Check if the universe doesn't need to be updated if (!update_universe) // Return unchanged universe return Universe.Unchanged; // Otherwise update the universe based on the desired filters // Filter all securities with appropriate price and volume var filteredCoarse = (from x in coarse where x.Price >= MIN_PRICE && x.Price <= MAX_PRICE && x.Volume >= MIN_DAILY_VOLUME && x.DollarVolume >= MIN_DAILY_DOLLAR_VOLUME select x).ToList(); // Check if fundamental data is required if (REQUIRE_FUNDAMENTAL_DATA) filteredCoarse = (from x in filteredCoarse where x.HasFundamentalData select x).ToList(); // Return the symbol objects List<Symbol> universe = (from x in filteredCoarse select x.Symbol).ToList(); // Print universe details when desired if (PRINT_COARSE) Log($"Coarse filter returned {universe.Count} stocks."); return universe; } /// <summary> /// Perform fine filters on universe. Called once per day. /// /// Attributes available: /// .AssetClassification /// .CompanyProfile /// .CompanyReference /// .EarningRatios /// .EarningReports /// .FinancialStatements /// .MarketCap /// .OperationRatios /// .Price -> always the raw price! /// .ValuationRatios /// </summary> /// <param name="fine">Summary information for each stock.</param> /// <returns>All stocks meeting the desired criteria.</returns> public IEnumerable<Symbol> FineSelectionFunction(IEnumerable<FineFundamental> fine) { // # Testing - catch specific symbol // for x in coarse: // # if str(x.Symbol).split(" ")[0] in ['AAPL']: // if x.Symbol.ID.Symbol in ['AAPL']: // # Stop and debug below // print(x) // Check if the universe doesn't need to be updated if (!update_universe) // Return unchanged universe return Universe.Unchanged; // Otherwise update the universe based on the desired filters // Filter by allowed exchange and market cap var symbols = (from x in fine where ALLOWED_EXCHANGE.Contains(x.SecurityReference.ExchangeId) && x.MarketCap >= MIN_MARKET_CAP && x.MarketCap <= MAX_MARKET_CAP select x).ToList(); // Filter stocks based on primary share class if (PRIMARY_SHARES) symbols = (from x in symbols where x.SecurityReference.IsPrimaryShare select x).ToList(); // Filter stocks based on disallowed sectors if (SECTORS_NOT_ALLOWED.Count > 0) { Utils.Trap(); symbols = (from x in symbols where !SECTORS_NOT_ALLOWED.Contains(x.AssetClassification.MorningstarSectorCode) select x).ToList(); } // Filter stocks based on disallowed industry groups if (GROUPS_NOT_ALLOWED.Length > 0) { Utils.Trap(); symbols = (from x in symbols where !GROUPS_NOT_ALLOWED.Contains(x.AssetClassification.MorningstarIndustryGroupCode) select x).ToList(); } // Filter stocks based on disallowed industries if (INDUSTRIES_NOT_ALLOWED.Length > 0) { Utils.Trap(); symbols = (from x in symbols where !INDUSTRIES_NOT_ALLOWED.Contains(x.AssetClassification.MorningstarIndustryCode) select x).ToList(); } // Return the symbol objects we want in our universe. IList<Symbol> universe = (from x in symbols select x.Symbol).ToList(); if (PRINT_FINE) Log($"Fine filter returned {universe.Count} stocks."); this.update_universe = false; return universe; } // Event handler for changes to our universe. /// <summary> /// Event fired each time the we add/remove securities from the data feed. /// </summary> /// <param name="changes">Security additions/removals for this time step.</param> public override void OnSecuritiesChanged(SecurityChanges changes) { // Loop through securities added to the universe foreach (var security in changes.AddedSecurities) { // Skip if BENCHMARK - we cannot trade this! if (security.Symbol.ID.Symbol == BENCHMARK) continue; // Create a new symbol_data object for the security symbol_data.Add(security.Symbol, new SymbolData(this, security.Symbol)); } // Loop through securities removed from the universe foreach (var security in changes.RemovedSecurities) { // Liquidate removed securities if (security.Invested) { if (PRINT_VERBOSE) Log($"{security.Symbol.ID.Symbol} removed from the universe, so closing open position."); Liquidate(security.Symbol); } // Remove from symbol_data dictionary if (symbol_data.ContainsKey(security.Symbol)) { Utils.Trap(); // Remove desired bar consolidator for this security TradeBarConsolidator consolidator = symbol_data[security.Symbol].Consolidator; SubscriptionManager.RemoveConsolidator(security.Symbol, consolidator); // Remove symbol from symbol data if (PRINT_VERBOSE) Log($"{security.Symbol.ID.Symbol} removed from symbol_data."); symbol_data.Remove(security.Symbol); } } } /// <summary> /// Liquidate the entire portfolio. Also cancel any pending orders. /// </summary> public void LiquidatePortfolio() { Utils.Trap(); if (Portfolio.Invested && PRINT_ORDERS) Log("Time for end of day exit. Liquidating the portfolio."); Liquidate(); } /// <summary> /// Event called when signal checks are desired. /// </summary> public void CheckForSignals() { if (PRINT_VERBOSE) Log($"CheckForSignals() Time={Time}"); // Check for exit signals CheckForExits(); // Check for entry signals CheckForEntries(); } /// <summary> /// Check for exit signals. /// </summary> public void CheckForExits() { // Get list of current positions var positions = (from symbol in Portfolio.Keys where Portfolio[symbol].Invested select symbol).ToList(); // Loop through positions foreach (var sym in positions) // Check for long position if (Portfolio[sym].Quantity > 0) // long - Check for long exit signal symbol_data[sym].LongExitSignalChecks(); } // Check for entry signals. public void CheckForEntries() { // Get list of current positions List<Symbol> positions = (from symbol in Portfolio.Keys where Portfolio[symbol].Invested select symbol).ToList(); // Get number of new entries allowed var newEntryNum = maxPositions - positions.Count; // Return if no new positions are allowed if (newEntryNum == 0) return; // Create a list of long symbol var longSymbols = new List<SymbolData>(); // Loop through the SymbolData class objects foreach (SymbolData symbolData in symbol_data.Values) { // Skip if already invested if (Securities[symbolData.Symbol].Invested) continue; // Check if indicators are ready - can be false for new stocks & sparsely traded ones if (symbolData.IndicatorsReady) // Check for long entry signal if (symbolData.LongEntrySignal) longSymbols.Add(symbolData); } // Check if the number of new entries exceeds limit if (longSymbols.Count > newEntryNum) { if (PRINT_ORDERS) Log($"{positions.Count} existing + {longSymbols.Count} additional orders > {maxPositions} maximum positions"); // Sort the entry_tuples list of tuples by largest pct_change // pct_change is the second element of the tuple // reverse=True for descending order (highest to lowest) longSymbols = longSymbols.OrderByDescending(sd => sd.FastSlowPctDifference).ToList(); // Only keep the top newEntryNum longSymbols = longSymbols.Take(newEntryNum).ToList(); } // Print entry signal summary when desired if (PRINT_ENTRIES && longSymbols.Count > 0) { string symbols = string.Join(", ", longSymbols.Select(sd => sd.Symbol.Value)); Log($"{longSymbols.Count} LONG entry signal(s): {symbols}"); } // Place entry orders foreach (SymbolData symbolData in longSymbols) SetHoldings(symbolData.Symbol, maxPctPerPosition, tag: "initial buy"); } /// <summary> /// Order fill event handler. On an order fill update the resulting information is passed to this method. /// </summary> /// <param name="orderEvent">Order event details containing details of the evemts.</param> public override void OnOrderEvent(OrderEvent orderEvent) { // Skip if not filled if (orderEvent.Status != OrderStatus.Filled) return; // Get the order's symbol if (PRINT_VERBOSE) Log($"{orderEvent.Symbol.Value} OnOrderEvent({orderEvent})"); // this can come in after we removed a security if (!symbol_data.ContainsKey(orderEvent.Symbol)) { Utils.Trap(); if (PRINT_VERBOSE) Log($"{orderEvent.Symbol.Value} not in self.symbol_data"); return; } // Call on_order_event for the symbol's SymbolData class this.symbol_data[orderEvent.Symbol].OnOrderEvent(orderEvent); } /// <summary> /// Event handler for end of trading day for the benchmark. /// </summary> public void BenchmarkOnEndOfDay() { PlotBenchmarkOnEquityCurve(); } /// <summary> /// Plot the benchmark buy & hold value on the strategy equity chart. /// </summary> /// <param name="force_plot"></param> public void PlotBenchmarkOnEquityCurve(bool force_plot = false) { // Initially set percent change to zero float pct_change = 0; // Get today's daily prices // history algo on QC github shows different formats that can be used: // https://github.com/QuantConnect/Lean/blob/master/Algorithm.Python/HistoryAlgorithm.py IEnumerable<Slice> history = this.History(new List<Symbol> { bm }, new TimeSpan(1, 0, 0, 0), Resolution.Daily); var listHistory = history.ToList(); decimal? price; if (listHistory.Count > 0) { TradeBar tradebar = listHistory[0].Bars[bm]; // We have not created the first price variable yet // Get today's open and save as the first price if (bm_first_price == null) { bm_first_price = tradebar.Open; Log($"Benchmark first price = {bm_first_price}"); } // Get today's closing price price = tradebar.Close; // Calculate the percent change since the first price pct_change = (float) ((price - bm_first_price) / bm_first_price); } else price = null; // Calculate today's ending value if we have the % change from the start if (pct_change != 0) { var bm_value = Math.Round(CASH * (decimal)(1 + pct_change), 2); // Plot every PLOT_EVERY_DAYS days bm_plot_counter += 1; if ((this.bm_plot_counter >= PLOT_EVERY_DAYS) || force_plot) { // Plot the benchmark's value to the Strategy Equity chart // Plot function requires passing the chart name, series name, // then the value to plot this.Plot("Strategy Equity", "Benchmark", bm_value); // Plot the account leverage var account_leverage = Portfolio.TotalHoldingsValue / Portfolio.TotalPortfolioValue; Plot("Leverage", "Leverge", account_leverage); // Reset counter to 0 bm_plot_counter = 0; // Log benchmark's ending price for reference if (force_plot) { Log($"Benchmark's first price = {bm_first_price}"); Log($"Benchmark's final price = {price}"); Log($"Benchmark buy & hold value = {bm_value}"); } } } } /// <summary> /// Define models to be used for securities as they are added to the algorithm's universe. /// </summary> private class MySecurityInitializer : ISecurityInitializer { public void Initialize(Security security) { // Define the data normalization mode security.SetDataNormalizationMode(DataNormalizationMode.Adjusted); // Define the fee model to use for the security // security.SetFeeModel() // Define the slippage model to use for the security // security.SetSlippageModel() // Define the fill model to use for the security // security.SetFillModel() // Define the buying power model to use for the security } } } }
/* * Copyright (C) 2022 by Lanikai Studios, Inc. - All Rights Reserved * * This code is not to be distributed to others, it is confidential information of Lanikai Studios. */ using System; using System.Diagnostics; using QuantConnect.Data.Fundamental; namespace Lanikai { /// <summary> /// Basic utilities for the project. /// /// Created by: David Thielen /// Version: 1.0 /// </summary> public static class Utils { /// <summary> /// Break into the debugger if running in DEBUG mode. /// </summary> public static void Trap() { //#if DEBUG // Debugger.Break(); Console.Out.WriteLine($"Trap() Stack = {Stack}"); //#endif } /// <summary> /// Break into the debugger if running in DEBUG mode and the parameter is true.. /// </summary> /// <param name="breakIfTrue">Break into the debugger if true, do nothing if false.</param> public static void Trap(bool breakIfTrue) { //#if DEBUG if (breakIfTrue) { // Debugger.Break(); Console.Out.WriteLine($"Trap(true) Stack = {Stack}"); } //#endif } private static string Stack { get { string stack = Environment.StackTrace; int start = stack.IndexOf("Trap"); if (start == -1) return stack.Trim(); start = stack.IndexOf('\n', start) + 1; int end = stack.IndexOf("QuantConnect"); if (end == -1) return stack.Substring(start).Trim().Replace('\n', ' '); end = stack.LastIndexOf('\n', end); if (end == -1) return stack.Substring(start).Trim().Replace('\n', ' '); return stack.Substring(start, end - start).Trim().Replace('\n', ' '); } } } }
/* * Copyright (C) 2022 by Lanikai Studios, Inc. - All Rights Reserved * * This code is not to be distributed to others, it is confidential information of Lanikai Studios. * * This code was built from open source Python code written by Aaron Eller - www.excelintrading.com */ using System; using System.Collections.Generic; using System.Linq; namespace Lanikai { /// <summary> /// Moving Average Cross Universe Strategy /// Version 1.1.0 /// /// Revision Notes: /// 1.0.0 (01/17/2021) - Initial.Started from "Universe Strategy_v103.py". Also /// copied SymbolData logic from /// "Moving Average Crossover_v107.py". /// 1.1.0 (01/26/2021) - converted to C# /// /// References: /// -QC(Lean) Class List https://lean-api-docs.netlify.app/annotated.html /// -OrderTicket properties https://lean-api-docs.netlify.app/classQuantConnect_1_1Orders_1_1OrderTicket.html /// -QC Universe https://www.quantconnect.com/docs/algorithm-reference/universes /// -QC Universe Settings https://www.quantconnect.com/docs/algorithm-reference/universes#Universes-Universe-Settings /// -QC Universe Fundamentals https://www.quantconnect.com/docs/data-library/fundamentals /// -Speeding up QC Universe https://www.quantconnect.com/forum/discussion/7875/speeding-up-universe-selection/p1 /// </summary> public static class GlobalSettings { //////////////////////////////////////////////////// // BACKTEST INPUTS /// <summary>Everything runs on New York time (not UTC). Therefore the market open/close is constant</summary> public static readonly TimeZoneInfo TIMEZONE; /// <summary>Backtest and Optimize start on this day (exclusive - because QC needs to run to overnight to pupulate equity pricing).</summary> private static readonly DateTime StartDate = new DateTime(2020, 1, 1); public static readonly DateTimeOffset START_DATE; /// <summary>Backtest and Optimize end on this day (inclusive).</summary> private static readonly DateTime EndDate = new DateTime(2020, 2, 1); public static readonly DateTimeOffset END_DATE; /// <summary>Starting portfolio value</summary> public static decimal CASH = 500000; //////////////////////////////////////////////////// // DATA INPUTS /// <summary> /// Define the data resolution to be fed to the algorithm /// Must be "SECOND", "MINUTE", or "HOUR" /// </summary> public static string DATA_RESOLUTION = "HOUR"; /// <summary> /// How often to update the universe? /// Options: 'daily', 'weekly', or 'monthly' /// </summary> public static string UNIVERSE_FREQUENCY = "monthly"; //////////////////////////////////////////////////// // COARSE UNIVERSE SELECTION INPUTS /// <summary>if True, stocks only / no etfs e.g.</summary> public static bool REQUIRE_FUNDAMENTAL_DATA = true; /// <summary>Selected stocks must be this price per share, or more. Set to 0 to disable.</summary> public static decimal MIN_PRICE = 10.0m; /// <summary> Selected stocks must be this price per share, or less. Set to 1e6 to disable.</summary> public static decimal MAX_PRICE = 1000000.0m; /// <summary>Daily volume of trades in this stock must be this amount, or more. Set to 0 to disable.</summary> public static int MIN_DAILY_VOLUME = 0; /// <summary>Daily total price of trades in this stock must be this amount, or more. Set to 1e6 to disable.</summary> public static decimal MIN_DAILY_DOLLAR_VOLUME = 10000000.0m; //////////////////////////////////////////////////// // FINE UNIVERSE SELECTION INPUTS /// <summary>Minimum market cap. e6=million/e9=billion/e12=trillion</summary> public static decimal MIN_MARKET_CAP = 10E9m; /// <summary>Maximum market cap. e6=million/e9=billion/e12=trillion</summary> public static decimal MAX_MARKET_CAP = 10E12m; // Turn on/off specific exchanges allowed /// <summary>Archipelago Electronic Communications Network.</summary> public static bool ARCX = false; /// <summary>American Stock Exchange</summary> public static bool ASE = false; /// <summary>Better Alternative Trading System</summary> public static bool BATS = false; /// <summary>Nasdaq Stock Exchange</summary> public static bool NAS = true; /// <summary>New York Stock Exchange</summary> public static bool NYS = true; /// <summary>Only allow a stock's primary shares</summary> public static bool PRIMARY_SHARES = true; // Turn on/off specific sectors allowed public static bool BASIC_MATERIALS = true; public static bool CONSUMER_CYCLICAL = true; public static bool FINANCIAL_SERVICES = true; public static bool REAL_ESTATE = true; public static bool CONSUMER_DEFENSIVE = true; public static bool HEALTHCARE = true; public static bool UTILITIES = true; public static bool COMMUNICATION_SERVICES = true; public static bool ENERGY = true; public static bool INDUSTRIALS = true; public static bool TECHNOLOGY = true; /// <summary> /// Set Morningstar Industry Groups not allowed /// Use Industry Group Code from: /// https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Asset-Classification /// example: MorningstarIndustryGroupCode.Banks (10320) /// </summary> public static int[] GROUPS_NOT_ALLOWED = new int [0]; /// <summary> /// Set Morningstar Industries not allowed /// Use Industry Codes from: /// https://www.quantconnect.com/docs/data-library/fundamentals#Fundamentals-Asset-Classification /// example: MorningstarIndustryCode.Gambling (10218038) /// </summary> public static int[] INDUSTRIES_NOT_ALLOWED = new int[0]; /// <summary> /// Set the minimum number of days to leave a stock in a universe. /// This helps with making the universe output more stable. /// </summary> public static int MIN_TIME_IN_UNIVERSE = 65; /// <summary> /// Set the minimum number of days with historical data /// </summary> public static int MIN_TRADING_DAYS = 200; //////////////////////////////////////////////////// // ENTRY SIGNAL INPUTS /// <summary> /// The fast EMA period (number of trading days). /// </summary> public static int EMA_FAST_PERIOD = 50; /// <summary> /// The slow EMA period (number of trading days). /// </summary> public static int EMA_SLOW_PERIOD = 200; /// <summary> /// How many minutes prior to the open to check for new signals? /// Ideally this is called AFTER the universe filters run! /// </summary> public static int SIGNAL_CHECK_MINUTES = 30; //////////////////////////////////////////////////// // POSITION SIZING INPUTS /// <summary> /// Set the percentage of the portfolio to free up to avoid buying power issues. /// decimal percent, e.g. 0.025=2.5% (default). /// </summary> public static float FREE_PORTFOLIO_VALUE_PCT = 0.025f; /// <summary> /// Set the max number of positions allowed /// </summary> public static int MAX_POSITIONS = 4; /// <summary> /// Calculate max % of portfolio per position /// </summary> public static float MAX_PCT_PER_POSITION = 1.0f / MAX_POSITIONS; //////////////////////////////////////////////////// // EXIT SIGNAL INPUTS - stop loss /// <summary> /// Turn on/off stop loss /// </summary> public static bool STOP_LOSS = true; /// <summary> /// Set stop loss percentage as a decimal percent, e.g. 0.02 == 2.0% /// </summary> public static float SL_PCT = 0.05f; /// <summary> /// Turn on/off trailing stop. Starts and trails based on SL_PCT /// </summary> public static bool TRAILING_STOP = true; /// <summary> /// Turn on/off end of day exit. /// </summary> public static bool EOD_EXIT = false; /// <summary> /// When end of exit exit is desired, how many minutes prior to the market close should positions be liquidated? /// </summary> public static int EOD_EXIT_MINUTES = 15; //////////////////////////////////////////////////// // EXIT SIGNAL INPUTS - Death Cross /// <summary> /// Turn on/off exiting on fast EMA crossing under the slow EMA /// </summary> public static bool EMA_CROSSUNDER_EXIT = true; //////////////////////////////////////////////////// // EXIT SIGNAL INPUTS - RSI /// <summary> /// Set the RSI period (trading days). /// </summary> public static int RSI_PERIOD = 14; /// <summary> /// Turn on/off RSI exit /// </summary> public static bool RSI_EXIT = true; /// <summary> /// Long exit when RSI crosses below this value /// </summary> public static decimal RSI_EXIT_1_VALUE = 62m; /// <summary> /// decimal percent of initial position, 0.50=50.0% /// </summary> public static float RSI_EXIT_1_PCT = 0.5f; /// <summary> /// Long exit when RSI crosses below this value /// </summary> public static decimal RSI_EXIT_2_VALUE = 52m; //////////////////////////////////////////////////// // EXIT SIGNAL INPUTS - days held /// <summary> /// Turn on/off days held exit /// </summary> public static bool DAYS_HELD_EXIT = false; /// <summary> /// How many days held until selling exit #1 /// </summary> public static int DAYS_HELD_EXIT_1_VALUE = 2; /// <summary> /// What percentage of the initial order to sell on this exit. decimal percent, e.g. 0.25=25.0% /// </summary> public static float DAYS_HELD_EXIT_1_PCT = 0.25f; /// <summary> /// How many days held until selling exit #2 /// </summary> public static int DAYS_HELD_EXIT_2_VALUE = 4; /// <summary> /// What percentage of the initial order to sell on this exit. decimal percent, e.g. 0.25=25.0% /// </summary> public static float DAYS_HELD_EXIT_2_PCT = 0.25f; /// <summary> /// How many days held until selling exit #3 /// </summary> public static int DAYS_HELD_EXIT_3_VALUE = 6; /// <summary> /// What percentage of the initial order to sell on this exit. decimal percent, e.g. 0.25=25.0% /// </summary> public static float DAYS_HELD_EXIT_3_PCT = 0.25f; /// <summary> /// How many days held until selling exit #4 /// </summary> public static int DAYS_HELD_EXIT_4_VALUE = 8; //////////////////////////////////////////////////// // EXIT SIGNAL INPUTS - percent increase /// <summary> /// Turn on/off profit target /// </summary> public static bool PROFIT_TARGET = false; /// <summary> /// Set profit target percentage as a decimal percent, e.g. 0.05=5.0% /// </summary> public static float PT1_PCT = 0.05f; /// <summary> /// Set profit target order percentage as a decimal percent, e.g. 0.50=50.0% /// </summary> public static float PT1_ORDER_PCT = 0.5f; /// <summary> /// Set profit target percentage as a decimal percent, e.g. 0.07=7.0% /// </summary> public static float PT2_PCT = 0.07f; /// <summary> /// Set profit target order percentage as a decimal percent, e.g. 0.25=25.0% /// </summary> public static float PT2_ORDER_PCT = 0.25f; /// <summary> /// Set profit target percentage as a decimal percent, e.g. 0.09=9.0% /// </summary> public static float PT3_PCT = 0.09f; //////////////////////////////////////////////////// // ROLLING WINDOWS, BENCHMARK DETAILS /// <summary> /// How many trading days to keep in the rolling windows. Set to how far back want to look at historical data. /// </summary> public static int INDICATOR_WINDOW_LENGTH = 10; /// <summary> /// Turn on/off using the custom benchmark plot on the strategy equity chart /// </summary> public static bool PLOT_BENCHMARK_ON_STRATEGY_EQUITY_CHART = true; /// <summary> /// Define benchmark equity. Currently set to not be able to trade the benchmark! /// Also used for scheduling functions, <b>so make sure it has same trading hours as instruments traded.</b> /// </summary> public static string BENCHMARK = "SPY"; //////////////////////////////////////////////////// // LOGGING DETAILS (minimize logging due to QC limits!) /// <summary> /// print summary of coarse universe selection /// </summary> public static bool PRINT_COARSE = false; /// <summary> /// print summary of fine universe selection /// </summary> public static bool PRINT_FINE = false; /// <summary> /// print summary of daily entry signals /// </summary> public static bool PRINT_ENTRIES = true; /// <summary> /// print exit signals triggered /// </summary> public static bool PRINT_EXITS = false; /// <summary> /// print stats for new orders /// </summary> public static bool PRINT_ORDER_STATS = true; /// <summary> /// print new orders /// </summary> public static bool PRINT_ORDERS = false; /// <summary> /// print changes in stop loss, etc. orders /// </summary> public static bool PRINT_UPDATES = false; /// <summary> /// print settings for the run /// </summary> public static bool PRINT_SETTINGS = false; /// <summary> /// print equity data initialization /// </summary> public static bool PRINT_EQUITY_INIT = false; /// <summary> /// print verbose info /// </summary> public static bool PRINT_VERBOSE = false; /// <summary> /// print verbose info for only these stocks. /// </summary> public static string[] VERBOSE_SYMBOL = {"BKR"}; //////////////////////////////////////////////////// // END OF ALL USER INPUTS // VALIDATE USER INPUTS - DO NOT CHANGE BELOW!!! //////////////////////////////////////////////////// /// <summary> /// List of the allowed exchanges /// </summary> public static List<string> ALLOWED_EXCHANGE; /// <summary> /// List of the sectors NOT allowed /// </summary> public static List<int> SECTORS_NOT_ALLOWED; private static readonly string[] resolutions = {"SECOND", "MINUTE", "HOUR"}; private static string[] frequencies = { "daily", "weekly", "monthly" }; /// <summary> /// The number of market warmup days required. /// </summary> public static int MARKET_WARMUP_DAYS; /// <summary> /// The number of calendar days required to match MARKET_WARMUP_DAYS. This number will be >= to MARKET_WARMUP_DAYS /// as it uses rough additions to account for holidays. /// </summary> public static int CALENDAR_WARMUP_DAYS; /// <summary> /// Calculate the frequency of days that we can create a new plot. /// </summary> public static int PLOT_EVERY_DAYS; /// <summary> /// Initialize global lists, validate settings /// </summary> static GlobalSettings() { // first try the Linux name TIMEZONE = TimeZoneInfo.FindSystemTimeZoneById("America/New_York"); // if that fails - the Windows name if (TIMEZONE == null) TIMEZONE = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); START_DATE = new DateTimeOffset(StartDate, TIMEZONE.GetUtcOffset(StartDate)); END_DATE = new DateTimeOffset(EndDate, TIMEZONE.GetUtcOffset(EndDate)); ALLOWED_EXCHANGE = new List<string>(); if (ARCX) ALLOWED_EXCHANGE.Add("ARCX"); if (ASE) ALLOWED_EXCHANGE.Add("ASE"); if (BATS) ALLOWED_EXCHANGE.Add("BATS"); if (NAS) ALLOWED_EXCHANGE.Add("NAS"); if (NYS) ALLOWED_EXCHANGE.Add("NYS"); SECTORS_NOT_ALLOWED = new List<int>(); if (! BASIC_MATERIALS) SECTORS_NOT_ALLOWED.Add(101); if (! CONSUMER_CYCLICAL) SECTORS_NOT_ALLOWED.Add(102); if (! FINANCIAL_SERVICES) SECTORS_NOT_ALLOWED.Add(103); if (! REAL_ESTATE) SECTORS_NOT_ALLOWED.Add(104); if (! CONSUMER_DEFENSIVE) SECTORS_NOT_ALLOWED.Add(205); if (! HEALTHCARE) SECTORS_NOT_ALLOWED.Add(206); if (! UTILITIES) SECTORS_NOT_ALLOWED.Add(207); if (! COMMUNICATION_SERVICES) SECTORS_NOT_ALLOWED.Add(308); if (! ENERGY) SECTORS_NOT_ALLOWED.Add(309); if (! INDUSTRIALS) SECTORS_NOT_ALLOWED.Add(310); if (! TECHNOLOGY) SECTORS_NOT_ALLOWED.Add(311); DATA_RESOLUTION = DATA_RESOLUTION.ToUpper(); if (! resolutions.Contains(DATA_RESOLUTION)) throw new ApplicationException($"DATA_RESOLUTION = {DATA_RESOLUTION} is an invalid value."); UNIVERSE_FREQUENCY = UNIVERSE_FREQUENCY.ToLower(); if (!frequencies.Contains(UNIVERSE_FREQUENCY)) throw new ApplicationException($"UNIVERSE_FREQUENCY = {UNIVERSE_FREQUENCY} is an invalid value."); // For the benchmark plotting int PLOT_LIMIT = 4000; int BT_DAYS = (END_DATE - START_DATE).Days; BT_DAYS = (int)Math.Ceiling(((float)BT_DAYS * 252f) / 365f); PLOT_EVERY_DAYS = Math.Max(1, (int) Math.Ceiling((float)BT_DAYS / (float)PLOT_LIMIT)); // Calculate the number of days in the backtest // Calculate the period to warm up the data int BARS_PER_DAY = 1; // Get the minimum number of bars required to fill all indicators int MIN_BARS = Math.Max(Math.Max(EMA_FAST_PERIOD, EMA_SLOW_PERIOD), RSI_PERIOD) + INDICATOR_WINDOW_LENGTH; MARKET_WARMUP_DAYS = (int) Math.Ceiling((float)MIN_BARS / (float)BARS_PER_DAY); // Assume 252 market days per calendar year (or 365 calendar days), add a 10% and +2 buffer for holidays CALENDAR_WARMUP_DAYS = (int) ((MARKET_WARMUP_DAYS * 1.1f) * 365f / 252f + 2f); } } }
/* * Copyright (C) 2022 by Lanikai Studios, Inc. - All Rights Reserved * * This code is not to be distributed to others, it is confidential information of Lanikai Studios. * * This code was built from open source Python code written by Aaron Eller - www.excelintrading.com */ using System; using System.Collections.Generic; using System.Reflection; using System.Text; using MathNet.Numerics; using QuantConnect; using QuantConnect.Data; using QuantConnect.Data.Consolidators; using QuantConnect.Data.Market; using QuantConnect.Indicators; using QuantConnect.Orders; using QuantConnect.Securities; using static Lanikai.GlobalSettings; namespace Lanikai { /// <summary> /// Class to store data for a specific symbol. /// </summary> public class SymbolData { /// <summary>The parent algorithm that owns this object.</summary> private readonly LanikaiQCAlgorithm algo; /// <summary> /// The symbol this object operates on. /// The underlying text symbol is symbol.ID.Symbol /// </summary> public Symbol Symbol { get; } /// <summary>The desired daily bar consolidator for the symbol.</summary> public TradeBarConsolidator Consolidator { get; private set; } /// <summary>The fast EMA indicator. Used to find when we hit the Golden Cross.</summary> private ExponentialMovingAverage ema_fast; /// <summary>The slow EMA indicator. Used to find when we hit the Golden Cross.</summary> private ExponentialMovingAverage ema_slow; /// <summary>The RSI indicator. Used optionally to determine when to sell.</summary> private RelativeStrengthIndex rsi; // bugbug - correct naming of vars. /// <summary>A boolean for each day true if the fast EMA isn > the slow EMA, false otherwise. /// Used to determine when to buy.</summary> private RollingWindow<bool> fast_ema_gt_slow_ema; /// <summary>Create a rolling window of the last closing prices. /// Optionally used to take profits based on the curve of recent closes.</summary> private RollingWindow<decimal> window_closes; /// <summary>Create a rolling window of the recent fast EMA values. /// Optionally used to take profits based on the curve of recent EMA values.</summary> public RollingWindow<decimal> window_ema_fast; /// <summary>Create a rolling window of the recent slow EMA values. /// Optionally used to take profits based on the curve of recent EMA values.</summary> public RollingWindow<decimal> window_ema_slow; /// <summary> /// Create a rolling window of the recent RSI values. /// Optionally used to take profits based on the curve of recent RSI values. /// RSI has a value of 0 ... 100. Moving up is bullish, moving down bearish. /// </summary> private RollingWindow<double> window_rsi; /// <summary>Create a rolling window of the recent trade bars (1 per day). /// Optionally used to take profits based on the curve of recent trade bar values.</summary> private RollingWindow<TradeBar> window_bar; /// <summary>All of the indicators and rolling windows. /// This is weird as there is no common base class, but both do have an IsReady property.</summary> private List<object> indicators; /// <summary>The exchange hours for this security.</summary> private SecurityExchangeHours exchangeHours; /// <summary>The time the market opens for this stock when the market is open for the full day.</summary> private DateTime mktOpenTime; /// <summary>The time the market closes for this stock when the market is open for the full day.</summary> private DateTime mktCloseTime; private decimal? cost_basis; ///<summary> The low price today. Used for adjusting the stop loss.</summary> private decimal? todaysLow; private OrderTicket sl_order; private decimal? sl_price; private OrderTicket pt_order1; private OrderTicket pt_order2; private OrderTicket pt_order3; /// <summary>When selling part (profit taking) this is set to that percentage to only do that once. Set to 0 when first purchasing.</summary> private float percentSold; private int days_held; public SymbolData(LanikaiQCAlgorithm algo, Symbol symbol) { // Save the references this.algo = algo; this.Symbol = symbol; // Get the symbol's exchange market info GetExchangeInfo(); // Initialize strategy specific variables InitializeStrategyVariables(); // Add the bars and indicators required AddBarsIndicators(); } /// <summary> /// Get the security's exchange info. /// </summary> public void GetExchangeInfo() { exchangeHours = algo.Securities[Symbol].Exchange.Hours; // using a date we know the market was open the standard hours // every market day should be these times OR LESS. DateTime jan4 = new DateTime(2021, 1, 4); DateTimeOffset date = new DateTimeOffset(jan4, TIMEZONE.GetUtcOffset(jan4)); // Save the full day market open and close times mktOpenTime = exchangeHours.GetNextMarketOpen(date.DateTime, false); mktCloseTime = exchangeHours.GetNextMarketClose(date.DateTime, false); } /// <summary> /// Initialize the strategy variables. /// </summary> /// <returns></returns> public void InitializeStrategyVariables() { // Initialize order variables ResetOrderVariables(); } /// <summary> /// Reset order variables for the strategy. /// </summary> public void ResetOrderVariables() { cost_basis = null; todaysLow = null; sl_order = null; sl_price = null; pt_order1 = null; pt_order2 = null; pt_order3 = null; days_held = 0; } /// <summary> /// Set up the consolidators, indicators, & rolling windows. /// </summary> public void AddBarsIndicators() { // Create the desired daily bar consolidator for the symbol Consolidator = new TradeBarConsolidator(DailyUSEquityCalendar); // Create an event handler to be called on each new consolidated bar Consolidator.DataConsolidated += OnDataConsolidated; // Link the consolidator with our symbol and add it to the algo manager algo.SubscriptionManager.AddConsolidator(Symbol, Consolidator); // Create indicators to be based on the desired consolidated bars ema_fast = new ExponentialMovingAverage(EMA_FAST_PERIOD); ema_slow = new ExponentialMovingAverage(EMA_SLOW_PERIOD); rsi = new RelativeStrengthIndex(RSI_PERIOD); // Create rolling windows of whether the fast EMA is > or < slow EMA // format: RollingWindow[object type](length) fast_ema_gt_slow_ema = new RollingWindow<bool>(2); // Create a rolling window of the last closing prices // used to make sure there is enough data to start trading window_closes = new RollingWindow<decimal>(MIN_TRADING_DAYS); // Create rolling windows for desired data window_ema_fast = new RollingWindow<decimal>(INDICATOR_WINDOW_LENGTH); window_ema_slow = new RollingWindow<decimal>(INDICATOR_WINDOW_LENGTH); window_rsi = new RollingWindow<double>(INDICATOR_WINDOW_LENGTH); // we need EMA_SLOW_PERIOD because we rebuild the indicators each day. window_bar = new RollingWindow<TradeBar>(EMA_SLOW_PERIOD); // Keep a list of all indicators & rolling windows - for indicators.IsReady property indicators = new List<object> { ema_fast, ema_slow, rsi, fast_ema_gt_slow_ema, window_closes, window_ema_fast, window_ema_slow, window_rsi, window_bar }; // Warm up the indicators with historical data WarmupIndicators(); } private CalendarInfo DailyUSEquityCalendar(DateTime dateTime) { // Set up daily consolidator calendar info for the US equity market. // This should return a start datetime object that is timezone unaware // with a valid date/time for the desired securities' exchange's time zone. // Create a datetime.datetime object to represent the market open DateTime start = new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, mktOpenTime.Hour, mktOpenTime.Minute, 0, 0, dateTime.Kind); // Get today's end time from the SecurityExchangeHours Class object // exchange_class = self.algo.Securities[self.symbol].Exchange // exchange_hrs_class = self.algo.Securities[self.symbol].Exchange.Hours // which is saved as self.exchange_hours DateTime end = exchangeHours.GetNextMarketClose(start, false); // Return the start datetime and the consolidation period return new CalendarInfo(start, end-start); } /// <summary> /// Warm up indicators using historical data. /// </summary> public void WarmupIndicators() { // Get historical data IEnumerable<Slice> history = algo.History(new List<Symbol> {Symbol}, MARKET_WARMUP_DAYS, Resolution.Daily); int num = 0; foreach (Slice slice in history) { // bar must exist TradeBar bar = slice.Bars[Symbol]; if (bar == null) continue; UpdateIndicators(bar); num++; } if (PRINT_EQUITY_INIT && ((! IndicatorsReady) || VERBOSE_SYMBOL.Contains(Symbol.Value))) algo.Log($"{Symbol.Value} indicators " + (IndicatorsReady ? "" : "not ") + $"ready {this}, history.Count = {num}"); } /// <summary> /// Event handler that fires when a new piece of data is produced. /// </summary> /// <param name="sender"></param> /// <param name="bar">The newly consolidated data.</param> private void OnDataConsolidated(object sender, TradeBar bar) { // Manually update all of the indicators UpdateIndicators(bar); } /// <summary> /// true if all of the indicators used are ready (warmed up). /// Once true, should never revert to false. /// </summary> public bool IndicatorsReady { get { // Return False if any indicator is not ready foreach (var indicator in indicators) { // can be an indicator or a rolling window - so use reflection PropertyInfo prop = indicator.GetType().GetProperty("IsReady"); if ((prop != null) && !((bool) prop.GetValue(indicator))) return false; } // Otherwise all indicators are ready, so return True return true; } } /// <summary> /// true if there is an EMA cross-over today. /// </summary> public bool EmaCrossOver { get { // Need latest value true (fast > slow ema) // and previous one false (fast <= slow ema) return fast_ema_gt_slow_ema[0] && !fast_ema_gt_slow_ema[1]; } } /// <summary> /// true if there is an EMA cross-under today. /// </summary> public bool EmaCrossUnder { get { // Need latest value false (fast < slow ema) // and previous one true (fast >= slow ema) return (!fast_ema_gt_slow_ema[0]) && fast_ema_gt_slow_ema[1]; } } /// <summary> /// The percent difference between the fast and slow EMAs. /// </summary> public decimal FastSlowPctDifference { get { return (ema_fast.Current.Value - ema_slow.Current.Value) / ema_slow.Current.Value; } } /// <summary> /// The current quantity held in the portfolio. /// </summary> public decimal CurrentQuantity { get { return algo.Portfolio[Symbol].Quantity; } } /// <summary> /// true if there is a valid long entry signal. /// </summary> public bool LongEntrySignal { get { if (EmaCrossOver && (! RSILiquidate)) { if (PRINT_ENTRIES) algo.Log($"{Symbol.Value} LONG ENTRY SIGNAL: EMA CROSSOVER; EMA fast: {RollingWindowToString(window_ema_fast, FastEMASlope)}; EMA slow: {RollingWindowToString(window_ema_slow, SlowEMASlope)}; RSI: {RSIRollingWindowToString(window_rsi, RSISlope)}"); return true; } return false; } } /// <summary> /// Check if there are any valid long exit signals. /// </summary> public void LongExitSignalChecks() { // make a switch // Trigger on an EMA cross-under if (EMA_CROSSUNDER_EXIT && EmaCrossUnder) { if (PRINT_EXITS) algo.Log($"{Symbol.Value} LONG EXIT SIGNAL: EMA CROSSUNDER"); algo.Log($"***** fast_ema_gt_slow_ema[0] = {fast_ema_gt_slow_ema[0]}; EndTime={window_bar[0].EndTime}"); algo.Log($"***** fast_ema_gt_slow_ema[1] = {fast_ema_gt_slow_ema[1]}; EndTime={window_bar[1].EndTime}"); algo.Log($"***** RSI={RSIRollingWindowToString(window_rsi, RSISlope)}"); algo.Log($"***** EMA fast={ToString(ema_fast)}"); algo.Log($"***** EMA slow={ToString(ema_slow)}"); algo.Liquidate(Symbol); return; } // Check for RSI exits if (RSI_EXIT) { // if the RSI is falling, then we exit if fallen below the exit values if (RSISlope <= 0) { if (PRINT_VERBOSE) algo.Log($"{Symbol.Value} RSI is falling {RSIRollingWindowToString(window_rsi, RSISlope)}"); // Check for RSI below the RSI_EXIT_1_VALUE if (rsi.Current.Value < algo.RsiExit1Value && percentSold < RSI_EXIT_1_PCT) { percentSold = RSI_EXIT_1_PCT; if (PRINT_EXITS) algo.Log($"{Symbol.Value} LONG EXIT SIGNAL: RSI ({rsi.Current.Value.ToString("F2")}) < {algo.RsiExit1Value}. Now closing {RSI_EXIT_1_PCT * 100.0}% of the position."); var exitQty = (int) (-CurrentQuantity * (decimal) RSI_EXIT_1_PCT); algo.MarketOrder(Symbol, exitQty, tag: $"sell {RSI_EXIT_1_PCT * 100}%, dropped below RSI_EXIT_1_VALUE"); } // Check for RSI below the RSI_EXIT_2_VALUE if (rsi.Current.Value < algo.RsiExit2Value && percentSold < 1) { percentSold = 1; if (PRINT_EXITS) algo.Log($"{Symbol.Value} LONG EXIT SIGNAL: RSI ({rsi.Current.Value.ToString("F2")}) < {algo.RsiExit2Value}. Now closing 100% of the position."); algo.Liquidate(Symbol, tag: $"liquidate, dropped below RSI_EXIT_2_VALUE"); } } else if (PRINT_VERBOSE) algo.Log($"{Symbol.Value} RSI is climbing {RSIRollingWindowToString(window_rsi, RSISlope)}"); } // Check for days held exits if (DAYS_HELD_EXIT) { Utils.Trap(); if (days_held >= DAYS_HELD_EXIT_1_VALUE && percentSold < DAYS_HELD_EXIT_1_PCT) { Utils.Trap(); percentSold = DAYS_HELD_EXIT_1_PCT; if (PRINT_EXITS) algo.Log( $"{Symbol.Value} LONG EXIT SIGNAL: Days held ({days_held}) = {DAYS_HELD_EXIT_1_VALUE}. Now closing {DAYS_HELD_EXIT_1_PCT * 100.0}% of the position."); var exitQty = (int) (-CurrentQuantity * (decimal) DAYS_HELD_EXIT_1_PCT); algo.MarketOrder(Symbol, exitQty); } if (days_held >= DAYS_HELD_EXIT_2_VALUE && percentSold < DAYS_HELD_EXIT_2_PCT) { Utils.Trap(); percentSold = DAYS_HELD_EXIT_2_PCT; if (PRINT_EXITS) algo.Log( $"{Symbol.Value} LONG EXIT SIGNAL: Days held ({days_held}) = {DAYS_HELD_EXIT_2_VALUE}. Now closing {DAYS_HELD_EXIT_2_PCT * 100.0}% of the position."); var exitQty = (int) (-CurrentQuantity * (decimal) DAYS_HELD_EXIT_2_PCT); algo.MarketOrder(Symbol, exitQty); } if (days_held == DAYS_HELD_EXIT_3_VALUE && percentSold < 1) { Utils.Trap(); percentSold = 1; if (PRINT_EXITS) algo.Log( $"{Symbol.Value} LONG EXIT SIGNAL: Days held ({days_held}) = {DAYS_HELD_EXIT_3_VALUE}. Now closing 100% of the position."); algo.Liquidate(Symbol); } } } /// <summary> /// Manually update all of the symbol's indicators. /// </summary> /// <param name="bar">The trade bar for this symbol for the period just ended.</param> public void UpdateIndicators(TradeBar bar) { // need this first - to recalc the indicators on the latest data window_bar.Add(bar); // Update the EMAs and RSI // need to recalc if the indicators are not discarding the oldest value when Update() is called // no need if warming up // if window_bar is not fully populated (new stock) then can't do this. if ((! algo.IsWarmingUp) && window_bar.IsReady) { ema_fast.Reset(); for (int index=EMA_FAST_PERIOD-1; index>=0; index--) { TradeBar _bar = window_bar[index]; ema_fast.Update(_bar.EndTime, _bar.Close); } ema_slow.Reset(); for (int index=EMA_SLOW_PERIOD-1; index>=0; index--) { TradeBar _bar = window_bar[index]; ema_slow.Update(_bar.EndTime, _bar.Close); } rsi.Reset(); for (int index=RSI_PERIOD-1; index>=0; index--) { TradeBar _bar = window_bar[index]; rsi.Update(_bar.EndTime, _bar.Close); } } else algo.Log($"***** {Symbol.Value} can't calculate indicators @ {bar.EndTime}. IsWarmingUp={algo.IsWarmingUp}, IsReady={window_bar.IsReady}"); // Update rolling windows window_ema_fast.Add(ema_fast.Current.Value); window_ema_slow.Add(ema_slow.Current.Value); fast_ema_gt_slow_ema.Add(ema_fast.Current.Value > ema_slow.Current.Value); window_rsi.Add((float)rsi.Current.Value); window_closes.Add(bar.Close); // Update the trades best price if trailing stop is used if (TRAILING_STOP) UpdateTradeBestPrice(bar); // Increment days held counter if there is a position if (CurrentQuantity != 0) { days_held += 1; // track the best price algo.Log($"***** {Symbol.Value} update highest @ {bar.EndTime}"); highestHigh = highestHigh == null ? bar.High : Math.Max(bar.High, highestHigh.Value); highestOpen = highestOpen == null ? bar.Open : Math.Max(bar.Open, highestOpen.Value); } } /// <summary> /// Update the trade's best price if there is an open position. /// </summary> /// <param name="bar">The trade bar for this symbol for the period just ended.</param> public void UpdateTradeBestPrice(TradeBar bar) { // Check if there is an open position if (CurrentQuantity <= 0) return; // Update the trade best price when appropriate if (todaysLow == null) { Utils.Trap(); // Should be set, so raise error to debug // throw new ApplicationException("what's going on here - bugbug"); algo.Log("***** what's going on here?"); return; } // we only move the stop loss up. if (bar.Low > todaysLow) { todaysLow = bar.Low; // Get the current stop loss price decimal? slPrice = StopPrice; // Check for increase in stop loss price if ((slPrice != null) && slPrice > this.sl_price) { // Update the stop loss order's price UpdateOrderStopPrice(sl_order, slPrice.Value); sl_price = slPrice; } } } /// <summary> /// The current stop loss price. This is SL_PCT below the lowest price from today's trades. /// The caller of this property needs to determine if this is a higher value than the present /// stop loss and only set to this if it's an increase (or decrease for shorting). /// </summary> public decimal? StopPrice { get { if (todaysLow == null) return null; // long if (CurrentQuantity > 0) return Math.Round(todaysLow.Value * (decimal) (1 - SL_PCT), 2); // short if (CurrentQuantity < 0) return Math.Round(todaysLow.Value * (decimal) (1 + SL_PCT), 2); Utils.Trap(); return null; } } /// <summary> /// Cancel any open exit orders. /// </summary> public void CancelExitOrders() { // Cancel open profit target order #1, if one if (pt_order1 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Cancelling {Symbol.Value} open profit target #1 order {pt_order1}."); try { pt_order1.Cancel(); pt_order1 = null; } catch { algo.Log($"Error trying to cancel {Symbol.Value} profit target #1 order {pt_order1}."); } } // Cancel open profit target order #2, if one if (pt_order2 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Cancelling {Symbol.Value} open profit target #2 order {pt_order2}."); try { pt_order2.Cancel(); pt_order2 = null; } catch { algo.Log($"Error trying to cancel {Symbol.Value} profit target #2 order {pt_order2}."); } } // Cancel open profit target order #3, if one if (pt_order3 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Cancelling {Symbol.Value} open profit target #3 order {pt_order3}."); try { pt_order3.Cancel(); pt_order3 = null; } catch { algo.Log($"Error trying to cancel {Symbol.Value} profit target #3 order {pt_order3}."); } // Cancel open stop order, if one if (sl_order != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Cancelling {Symbol.Value} open stop order {sl_order}."); try { sl_order.Cancel(); sl_order = null; } catch { algo.Log($"Error trying to cancel {Symbol.Value} stop loss order {sl_order}."); } } } // Reset order variables ResetOrderVariables(); } /// <summary> /// Get the profit target exit quantities for orders #1, #2, #3. /// </summary> /// <param name="initial">true if first time calculating the order quantity. false if updating the quantity.</param> /// <returns>The profit taking order quantities for profit taking level 1, level 2, & level 3.</returns> public Tuple<int, int, int> GetPtExitQuantities(bool initial = false) { // need as float to multiply times the percentages. float exitQty = (float) -CurrentQuantity; // Initialize each to 0 int pt1ExitQty = 0; int pt2ExitQty = 0; int pt3ExitQty = 0; // Get PT1, PT2, PT3 exit quantities if ((initial && PROFIT_TARGET) || (pt_order1 != null)) pt1ExitQty = Convert.ToInt32(exitQty * PT1_ORDER_PCT); if ((initial && PROFIT_TARGET) || (pt_order2 != null)) pt2ExitQty = Convert.ToInt32(exitQty * PT2_ORDER_PCT); if ((initial && PROFIT_TARGET) || (pt_order3 != null)) pt3ExitQty = (int) (exitQty - (pt1ExitQty + pt2ExitQty)); // Return the quantities return Tuple.Create(pt1ExitQty, pt2ExitQty, pt3ExitQty); } /// <summary> /// Update any open exit orders. /// </summary> public void UpdateExitOrders() { // Get the desired profit target exit order quantities var exitQuantities = GetPtExitQuantities(); // Update open profit target order #1, if one if (pt_order1 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Updating {Symbol.Value} open profit target #1 order qty to {exitQuantities.Item1}."); UpdateOrderQty(pt_order1, exitQuantities.Item1, $"updating profit taking quantity to {exitQuantities.Item1}"); } // Update open profit target order #2, if one if (pt_order2 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Updating {Symbol.Value} open profit target #2 order qty to {exitQuantities.Item2}."); UpdateOrderQty(pt_order2, exitQuantities.Item2, $"updating profit taking quantity to {exitQuantities.Item2}"); } // Update open profit target order #3, if one if (pt_order3 != null) { Utils.Trap(); if (PRINT_ORDERS) algo.Log($"Updating {Symbol.Value} open profit target #3 order qty to {exitQuantities.Item3}."); UpdateOrderQty(pt_order3, exitQuantities.Item3, $"updating profit taking quantity to {exitQuantities.Item3}"); } // Update open stop loss order, if one if (sl_order != null) { if (PRINT_ORDERS) algo.Log($"Updating {Symbol.Value} open stop loss order qty to {-CurrentQuantity}."); UpdateOrderQty(sl_order, -CurrentQuantity, $"updating stop loss quantity to {-CurrentQuantity}"); } } /// <summary> /// Update the desired order ticket's qty. /// </summary> /// <param name="ticket">The ticket to update.</param> /// <param name="qty">The quantity to change it to.</param> public void UpdateOrderQty(OrderTicket ticket, decimal qty, string tag) { if (PRINT_UPDATES) algo.Log($"{Symbol.Value} {tag}"); ticket.UpdateQuantity(qty, tag: tag); } /// <summary> /// Update the desired order ticket's stop price. /// </summary> /// <param name="ticket">The ticket to update.</param> /// <param name="price">The new price for the ticket.</param> public void UpdateOrderStopPrice(OrderTicket ticket, decimal price) { if (PRINT_UPDATES) algo.Log($"{Symbol.Value} updating stop price to {price}"); ticket.UpdateStopPrice(price, tag: $"updating stop price to {price}"); } /// <summary> /// Update the desired order ticket's limit price. /// </summary> /// <param name="ticket">The ticket to update.</param> /// <param name="price">The new price for the ticket.</param> public void UpdateOrderLimitPrice(OrderTicket ticket, decimal price) { Utils.Trap(); if (PRINT_UPDATES) algo.Log($"{Symbol.Value} updating limit price to {price}"); ticket.UpdateLimitPrice(price, tag: $"updating limit price to {price}"); } /// <summary> /// Place the desired stop loss order. This assumes that StopPrice will not return a null. /// </summary> public void PlaceStopLossOrder() { // Save and place the stop loss order sl_price = StopPrice; sl_order = algo.StopMarketOrder(Symbol, (int) -CurrentQuantity, sl_price.Value, tag: $"initial stop price of {sl_price.Value}"); if (PRINT_ORDERS) algo.Log($"{Symbol.Value} stop order placed at {sl_price.Value}"); } /// <summary> /// Place the desired profit target orders. /// </summary> public void PlaceProfitTakingOrders() { // Get the desired profit target exit order quantities var exitQuantities = GetPtExitQuantities(initial: true); if (PROFIT_TARGET && cost_basis != null && CurrentQuantity != 0) { Utils.Trap(); // Check for placing a profit target order #1 // Calculate the profit target price decimal ptPrice = 0; // long if (CurrentQuantity > 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 + PT1_PCT), 2); // short else if (CurrentQuantity < 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 - PT1_PCT), 2); // Place and save the stop loss order pt_order1 = algo.LimitOrder(Symbol, exitQuantities.Item1, ptPrice); if (PRINT_ORDERS) algo.Log($"{Symbol.Value} profit target #1 order: {exitQuantities.Item1} at {ptPrice}"); // Check for placing a profit target order #2 // Calculate the profit target price ptPrice = 0; // long if (CurrentQuantity > 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 + PT2_PCT), 2); // short else if (CurrentQuantity < 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 - PT2_PCT), 2); // Place and save the stop loss order pt_order2 = algo.LimitOrder(Symbol, exitQuantities.Item2, ptPrice); if (PRINT_ORDERS) algo.Log($"{Symbol.Value} profit target #2 order: {exitQuantities.Item2} at {ptPrice}"); // Check for placing a profit target order #3 // Calculate the profit target price ptPrice = 0; // long if (CurrentQuantity > 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 + PT3_PCT), 2); // short else if (CurrentQuantity < 0) ptPrice = Math.Round(cost_basis.Value * (decimal) (1 - PT3_PCT), 2); // Place and save the stop loss order pt_order3 = algo.LimitOrder(Symbol, exitQuantities.Item3, ptPrice); if (PRINT_ORDERS) algo.Log($"{Symbol.Value} profit target #3 order: {exitQuantities.Item3} at {ptPrice}"); } } // when order placed private double fastSlope; private double slowSlope; private double rsiSlope; private decimal purchasePrice; private decimal? highestOpen; private decimal? highestHigh; /// <summary> /// Order fill event handler. On an order fill update the resulting information is passed to this method. /// </summary> /// <param name="orderEvent">Order event details containing details of the evemts</param> public void OnOrderEvent(OrderEvent orderEvent) { // Get the order details var order = algo.Transactions.GetOrderById(orderEvent.OrderId); var order_qty = Convert.ToInt32(order.Quantity); var avg_fill = orderEvent.FillPrice; // Get current qty of symbol var qty = CurrentQuantity; // Check for entry order - Entry order filled if (order_qty == qty) { // reset stats for new purchase fastSlope = FastEMASlope; slowSlope = SlowEMASlope; rsiSlope = RSISlope; purchasePrice = avg_fill; highestOpen = highestHigh = null; // new purchase so reset percentSold = 0; if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} entry order filled: {order_qty} at {avg_fill}"); if (PRINT_ORDER_STATS) algo.Log(ToString()); // Save the cost basis and trade best price cost_basis = avg_fill; // use fill until next call where we can get Low. todaysLow = avg_fill; // Place a stop loss order when desired if (STOP_LOSS || TRAILING_STOP) PlaceStopLossOrder(); else Utils.Trap(); // Place profit target orders when desired PlaceProfitTakingOrders(); // Done with event, so return return; } if (PRINT_ORDER_STATS) { algo.Log($"{Symbol.Value} " + (avg_fill > purchasePrice ? "profit" : "LOSS") + $" of {avg_fill - purchasePrice}, price={avg_fill}. Held: HighestOpen={highestOpen}, HighestHigh={highestHigh}. Purchase: price={purchasePrice}, fastEMAslope={fastSlope}, slowEMAslope={slowSlope}, RSIslope={rsiSlope}"); algo.Log(ToString()); } // Check for stop order if (sl_order != null) { // Check for matching order ids if (orderEvent.OrderId == sl_order.OrderId) { // Stop order filled if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} stop order filled: {order_qty} at {avg_fill}"); // Cancel open exit orders CancelExitOrders(); // Done with event, so return return; } } // Check for profit target order #1 if (pt_order1 != null) { Utils.Trap(); // Check for matching order ids - Profit target order filled if (orderEvent.OrderId == pt_order1.OrderId) { Utils.Trap(); if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} profit target order #1 filled: {order_qty} at {avg_fill}"); // Check if the position is still open if (qty != 0) { Utils.Trap(); // Update open exit orders UpdateExitOrders(); } else { Utils.Trap(); // Cancel open exit orders CancelExitOrders(); } // Set order to None pt_order1 = null; // Done with event, so return return; } Utils.Trap(); } // Check for profit target order #2 if (pt_order2 != null) { Utils.Trap(); // Check for matching order ids - Profit target order filled if (orderEvent.OrderId == pt_order2.OrderId) { Utils.Trap(); if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log("${Symbol.Value} profit target order #2 filled: {order_qty} at {avg_fill}"); // Check if the position is still open if (qty != 0) { Utils.Trap(); // Update open exit orders UpdateExitOrders(); } else { Utils.Trap(); // Cancel open exit orders CancelExitOrders(); } // Set order to None pt_order2 = null; // Done with event, so return return; } } // Check for profit target order #3 if (pt_order3 != null) { Utils.Trap(); // Check for matching order ids - Profit target order filled if (orderEvent.OrderId == pt_order3.OrderId) { Utils.Trap(); if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} profit target order #3 filled: {order_qty} at {avg_fill}"); // Check if the position is still open if (qty != 0) { Utils.Trap(); // Update open exit orders UpdateExitOrders(); } else { Utils.Trap(); // Cancel open exit orders CancelExitOrders(); } // Set order to None pt_order3 = null; // Done with event, so return return; } } // Check for full exit order - Exit order filled if (qty == 0) { // liquidated so reset percentSold = 0; if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} full exit order filled: {order_qty} at {avg_fill}"); // Cancel open exit orders CancelExitOrders(); // Done with event, so return return; } // Check for pyramid entry order (qty and order_qty have the same signs) if (qty * order_qty > 0) { Utils.Trap(); // This strategy doesn't have pyramid entries, so raise error // throw new ApplicationException("pyramid entry order"); algo.Log($"***** pyramid entry order for {Symbol.Value}, qty={qty}, order_qty={order_qty}"); algo.Liquidate(this.Symbol); return; } // Otherwise a partial exit order - Partial exit order filled if (PRINT_ORDERS || PRINT_ORDER_STATS) algo.Log($"{Symbol.Value} partial exit order filled: {order_qty} at {avg_fill}"); // Update open exit orders UpdateExitOrders(); } /// <summary> /// Returns the slope of the fast EMA over the last 5 days. /// </summary> public double FastEMASlope { get { double [] xData = {1, 2, 3, 4, 5}; double [] yData = Array.ConvertAll(window_ema_fast.Take(5).Reverse().ToArray(), x => (double)x); Tuple<double, double> line = Fit.Line(xData, yData); return line.Item2; } } /// <summary> /// Returns the slope of the slow EMA over the last 5 days. /// </summary> public double SlowEMASlope { get { double [] xData = {1, 2, 3, 4, 5}; double [] yData = Array.ConvertAll(window_ema_slow.Take(5).Reverse().ToArray(), x => (double)x); Tuple<double, double> line = Fit.Line(xData, yData); return line.Item2; } } /// <summary> /// Returns the slope of the RSI over the last 5 days. /// </summary> public double RSISlope { get { double [] xData = {1, 2, 3, 4, 5}; double [] yData = window_rsi.Take(5).Reverse().ToArray(); Tuple<double, double> line = Fit.Line(xData, yData); return line.Item2; } } ///<summary> /// Returns true if the RSI values are set to liquidate (falling and below RSI_EXIT_2). ///</summary> public bool RSILiquidate { get { return RSISlope <= 0 && rsi.Current.Value < algo.RsiExit2Value; } } public override string ToString() { return $"Symbol:{Symbol.Value}; EMAFast:{ToString(ema_fast)}; EMASlow:{ToString(ema_slow)}; RSI:{ToString(rsi)}; FastGTSlow={ToString(fast_ema_gt_slow_ema)}; " + $" winCloses:{ToString(window_closes)}; winEmaFast:{ToString(window_ema_fast)}; winEmaSlow:{ToString(window_ema_slow)}; winRsi:{ToString(window_rsi)}; winBar:{ToString(window_bar)}"; } private string ToString(Indicator indicator) { return $"[IsReady:{indicator.IsReady}; Value:{indicator}"; } private string ToString<T>(RollingWindow<T> window) { return $"[IsReady:{window.IsReady}; Count:{window.Count}/{window.Size}" + (window.Count == 0 ? "" : $"[0]: {window[0]}") + "]"; } private string ToString(RollingWindow<decimal> window) { return $"[IsReady:{window.IsReady}; Count:{window.Count}/{window.Size}" + (window.Count == 0 ? "" : $"[0]: {window[0].ToString("C")}") + "]"; } private string ToString(RollingWindow<double> window) { return $"[IsReady:{window.IsReady}; Count:{window.Count}/{window.Size}" + (window.Count == 0 ? "" : $"[0]: {window[0].ToString("F2")}") + "]"; } public static string RollingWindowToString(RollingWindow<decimal> window, double? slope) { StringBuilder buf = new StringBuilder("["); for (int index=window.Size-1; index>=0; index--) buf.Append($"{window[index]:C}, "); buf.Remove(buf.Length - 2, 2); if (slope != null) buf.Append($" = {slope:F4}"); buf.Append("]"); return buf.ToString(); } private static string RSIRollingWindowToString(RollingWindow<double> window, double? slope) { StringBuilder buf = new StringBuilder("["); for (int index=window.Size-1; index>=0; index--) buf.Append($"{window[index]:F2}, "); buf.Remove(buf.Length - 2, 2); if (slope != null) buf.Append($" = {slope:F4}"); buf.Append("]"); return buf.ToString(); } } }