I am new to programming and I have been working on my own classes to manage and backtest trades. I wanted to be able to manage trades and signals per symbol so I created a TradableProduct class, this contains a Trade class for managing individual trades based on the TradableProduct.OrderEventsDict Dictionary<int, Trade> where the int is simply the Entry order ID.
There is also a CustomOrderEvent class that instantiates into Dictionary<string, CustomOrderEvent> to Cue up OrderEvents and Process them by individual TradableProduct object, this Process is locked per symbol/TradableProduct until it is finished,
there are a few other things like TradableProduct.TrendAndStrength() that allows me to sort my products by some variables like trend when I need to.
My problem is I don't seem to be able to trigger limit or stop orders.
I was trying to Que up order events to submit and manage stops but even if I try to submit limit orders when I submit Entry order(which work), my limits and stops never seem to submit. at first I thought it was a problem with my OrderEventCompletion() method but it doesn't seem to matter where I put the order submission method. (I did have a method for just using market orders but I want to be able to chose to have brackets in market).
Any help would be appreciated, sorry its a bit of a mess atm, not sure what I will need and what I can delete later, I haven't decided where to hide those classes and I had trouble trying to create a seperate file. I will link the main.cs file later but I have an upload issue at the moment. cheers.
trade.StopLossOrder = StopMarketOrder(trade.Symbol, quantity, trade.StopLossPrice, tag:"Stop Loss");
trade.TargetOrder = LimitOrder(trade.Symbol, quantity, trade.TargetPrice, tag:"Target");
#region imports
using System;
using System.Collections.Concurrent;
using System.Threading;
using MathNet.Numerics;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Drawing;
using QuantConnect;
using QuantConnect.Algorithm.Framework;
using QuantConnect.Algorithm.Framework.Selection;
using QuantConnect.Algorithm.Framework.Alphas;
using QuantConnect.Algorithm.Framework.Portfolio;
using QuantConnect.Algorithm.Framework.Execution;
using QuantConnect.Algorithm.Framework.Risk;
using QuantConnect.Parameters;
using QuantConnect.Benchmarks;
using QuantConnect.Brokerages;
using QuantConnect.Util;
using QuantConnect.Interfaces;
using QuantConnect.Algorithm;
using QuantConnect.Indicators;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Custom;
using QuantConnect.DataSource;
using QuantConnect.Data.Fundamental;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Orders.Fees;
using QuantConnect.Orders.Fills;
using QuantConnect.Orders.Slippage;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Securities.Future;
using QuantConnect.Securities.Option;
using QuantConnect.Securities.Forex;
using QuantConnect.Securities.Crypto;
using QuantConnect.Securities.Interfaces;
using QuantConnect.Storage;
using QuantConnect.Data.Custom.AlphaStreams;
using QCAlgorithmFramework = QuantConnect.Algorithm.QCAlgorithm;
using QCAlgorithmFrameworkBridge = QuantConnect.Algorithm.QCAlgorithm;
#endregion
namespace QuantConnect.Algorithm.CSharp
{
public enum TrendDirection {Bullish, Bearish, Range, Error}
public enum Result {Win, Loss, BreakEven, InProgress, Exit, Error};
public enum TradeSide {Long, Short};
public class TrendTrading : QCAlgorithm
{
private ConcurrentDictionary<int, ManualResetEventSlim> waitHandles = new ConcurrentDictionary<int, ManualResetEventSlim>();
public readonly Dictionary<Symbol, object> symbolLocks = new Dictionary<Symbol, object>();
public int EventCounter = int.MinValue;
public bool AdoptPositionsOn = true;
public int Period = 10000;
public decimal StopPercent = 0.5m;
public decimal TargetPercent = 1m;
public bool TrailingStopEnabled = true;
private List<TradableProduct> TradableProductsList;
private Dictionary<Symbol, TradableProduct> TradableProductsDict;
public decimal TradeSize = 100000;
public decimal openingBalanceWeekly;
public decimal openingBalanceDaily;
public bool WeeklyStopHit = false;
public bool DailyStopHit = false;
public int MinimumTrendStrengthPercent = 60;
public int AnalysisLookBackPeriod = 5; // For Trend Strength
public bool TradingHours;
public bool AnalysisTime;
public bool analysisPerformedMorning = false;
public bool analysisPerformedAfternoon = false;
public decimal MaxCashValue = 1000000;
public bool weekendExitFlag;
public override void Initialize()
{
SetStartDate(2008, 1, 2); //Set Start Date
SetEndDate(2023, 6, 1);
SetCash(100000); //Set Strategy Cash;
SetBrokerageModel(BrokerageName.OandaBrokerage, AccountType.Margin);
SetTimeZone("UTC");
SetBenchmark("AUDUSD");
List<string> ProductKeysList = new List<string>
{
"AUDUSD",// "EURAUD", "EURCAD", "EURCHF","AUDJPY","EURAUD", "EURCAD", "EURCHF","USDJPY","USDCAD", "USDCHF"
// "AUDCAD", "AUDCHF", "AUDHKD", "AUDJPY", "AUDNZD", "AUDSGD",
// "AUDUSD", "CADCHF", "CADHKD", "CADJPY", "CADSGD", "CHFHKD",
// "CHFJPY", "CHFZAR", "EURAUD", "EURCAD", "EURCHF", "EURCZK",
// "EURDKK", "EURGBP", "EURHKD", "EURHUF", "EURJPY", "EURNOK",
// "EURNZD", "EURPLN", "EURSEK", "EURSGD", "EURTRY", "EURUSD",
// "EURZAR", "GBPAUD", "GBPCAD", "GBPCHF", "GBPHKD", "GBPJPY",
// "GBPNZD", "GBPPLN", "GBPSGD", "GBPUSD", "GBPZAR", "HKDJPY",
// "NZDCAD", "NZDCHF", "NZDHKD", "NZDJPY", "NZDSGD", "NZDUSD",
// "SGDCHF", "SGDHKD", "SGDJPY", "TRYJPY", "USDCAD", "USDCHF",
// "USDCNH", "USDCZK", "USDDKK", "USDHKD", "USDHUF", "USDINR",
// "USDJPY", "USDMXN", "USDNOK", "USDPLN", "USDSAR", "USDSEK",
// "USDSGD", "USDTHB", "USDTRY", "USDZAR", "ZARJPY"
};
Log("Products List Created");
TradableProductsList = new(ProductKeysList.Count);
TradableProductsDict = new Dictionary<Symbol, TradableProduct>(TradableProductsList.Count());
foreach (var ticker in ProductKeysList)
{
var fx = AddForex(ticker, Resolution.Second, Market.Oanda, true, 500);
Console.WriteLine($"Creating {ticker} Tradable Product");
TradableProduct tp = new TradableProduct(this, fx ,fx.Symbol, Period, TrailingStopEnabled, TradeSize, MinimumTrendStrengthPercent);
tp.ResolutionHistory = History<QuoteBar>(tp.Symbol, Period, Resolution.Second);
tp.ActivateWindows(Period);
tp.Subscribe();
//Log($"Subscribed to {tp.Symbol}");
AddTradableProduct(tp);
//Log($"Added {ticker} to Tradable Products List");
Console.WriteLine($"Added {ticker} to Tradable Products List");
}
Log("Finished Adding Tradable Products");
SetWarmUp(TimeSpan.FromDays(4));
Log("Finished Initializing");
}
// we use the add and remove methods so that dictionary and list are always in sync, dictonary is used for fast lookup
public void AddTradableProduct(TradableProduct tp)
{
TradableProductsList.Add(tp);
TradableProductsDict[tp.Symbol] = tp;
}
public void RemoveTradableProduct(TradableProduct tp)
{
TradableProductsList.Remove(tp);
TradableProductsDict.Remove(tp.Symbol);
}
// this will be used for selection criteria, any method or indicator can be used
public void OrderTradableProductsByStrength(int LookBackPeriod, int MinimumTrendStrengthPercent)
{
foreach(TradableProduct tp in TradableProductsList)
{
if (tp.Subscribed == false){
tp.Subscribe();
}
tp.Analyse(LookBackPeriod, MinimumTrendStrengthPercent);
}
TradableProductsList = TradableProductsList.OrderByDescending(pt => pt.Strength).Take(TradableProductsList.Count()).ToList(); //try order by desscending;
Log("Finished Ordering Tradable Products by Strength");
}
public override void OnData(Slice data)
{
foreach(var bar in data.QuoteBars)
{
if (TradableProductsDict.ContainsKey(bar.Key))
{
TradableProductsDict[bar.Key].ResolutionWindow.Add(bar.Value);
}
}
if (Time.DayOfWeek == DayOfWeek.Monday || Time.DayOfWeek == DayOfWeek.Tuesday || Time.DayOfWeek == DayOfWeek.Wednesday || Time.DayOfWeek == DayOfWeek.Thursday)
{
weekendExitFlag = false;
}
if (IsWarmingUp) return;
//Check if Weekly Stop Hit
if (Portfolio.TotalPortfolioValue < openingBalanceWeekly * 0.95m)
{
Log($"Weekly Stop Hit, Liquidated Portfolio");
Console.WriteLine($"Weekly Stop Hit, Liquidated Portfolio");
WeeklyStopHit = true;
}
//Check if Weekly Stop Hit
if (Portfolio.TotalPortfolioValue < openingBalanceDaily * 0.98m)
{
Log($"Daily Stop Hit, Liquidated Portfolio");
Console.WriteLine($"Daily Stop Hit, Liquidated Portfolio");
DailyStopHit = true;
}
if(WeeklyStopHit || weekendExitFlag || DailyStopHit)
{
foreach (var tp in TradableProductsDict.Values)
{
tp.Liquidate();
}
}
//Reset Weekly stop for new trading week
if (data.Time.DayOfWeek == DayOfWeek.Sunday && Time.Hour == 17 && (Time.Minute >1 && Time.Minute < 5))
{
WeeklyStopHit = false;
openingBalanceWeekly = Portfolio.Cash;
Log($"Weekly Stop Reset, Opening Balance = {openingBalanceWeekly}");
Console.WriteLine($"Weekly Stop Reset, Opening Balance = {openingBalanceWeekly}");
}
//Reset Daily stop for new trading day
if (data.Time.Hour == 24)
{
DailyStopHit = false;
openingBalanceWeekly = Portfolio.Cash;
Log($"Weekly Stop Reset, Opening Balance = {openingBalanceWeekly}");
Console.WriteLine($"Weekly Stop Reset, Opening Balance = {openingBalanceWeekly}");
}
PreMarketAnalysis();
TradingHours = (Time.Hour >= 07 && Time.Hour < 09) || (Time.Hour >= 13 && Time.Hour < 15);
//Control for our resolution triggers
if (TradableProductsDict.Count() == 0 || TradableProductsDict == null)
Console.WriteLine("No Tradable Products");
//Update Our Tradable Product Objects
foreach(var bar in data.QuoteBars)
{
var tp = TradableProductsDict[bar.Key];
if (bar.Key == tp.Symbol)
{
ProcessQuedOrderEvents(tp.Symbol);
tp.NewTradeAllowed = Portfolio.TotalHoldingsValue < MaxCashValue && !WeeklyStopHit && !DailyStopHit; // -- Not needed as eeach Tradable product has its own max trades allwance
}
}
// Weekend Exit
if (!weekendExitFlag && Time.DayOfWeek == DayOfWeek.Friday && Time.Hour == 19 && Time.Minute >= 55)
{
Console.WriteLine("------Weekend Exit------");
Log("------Weekend Exit------");
weekendExitFlag = true;
}
}
public void PreMarketAnalysis()
{
// Check if it's time for morning analysis
if (Time.Hour == 7 && Time.Minute == 01 && !analysisPerformedMorning)
{
OrderTradableProductsByStrength(AnalysisLookBackPeriod, MinimumTrendStrengthPercent);
Console.WriteLine($"{Time.DayOfWeek} Morning Analysis Time, Tradable Products Ordered by Strength");
analysisPerformedMorning = true; // Set flag to avoid duplicate analysis
}
// Check if it's time for afternoon analysis
if (Time.Hour == 13 && Time.Minute == 01 && !analysisPerformedAfternoon)
{
OrderTradableProductsByStrength(AnalysisLookBackPeriod, MinimumTrendStrengthPercent);
Console.WriteLine($"{Time.DayOfWeek} Afternoon Analysis Time, Tradable Products Ordered by Strength");
analysisPerformedAfternoon = true; // Set flag to avoid duplicate analysis
}
// Reset flags at the end of each period
if (Time.Hour != 7 && analysisPerformedMorning)
analysisPerformedMorning = false;
if (Time.Hour != 13 && analysisPerformedAfternoon)
analysisPerformedAfternoon = false;
}
public void LiquidateUnwatchedProducts(TradableProduct tradableProduct)
{
if (!tradableProduct.Watching && Portfolio[tradableProduct.Symbol].Invested)
{
tradableProduct.Liquidate();
}
Log("Liquidate Unwatched Products Complete");
Console.WriteLine("Liquidate Unwatched Products Complete");
}
public class CustomOrderEvent
{
public string EventIdentifier { get; set; }
public int OrderId { get; set; }
public Symbol Symbol { get; set; }
public string Tag { get; set; }
public OrderStatus Status { get; set; }
public bool Processed { get; set; }
public DateTime TimeSubmitted { get; set; }
public decimal FillPrice { get; set; }
public string FillPriceCurrency { get; set; }
public decimal FillQuantity { get; set; }
public decimal AbsoluteFillQuantity => Math.Abs(FillQuantity);
public decimal? StopPrice { get; set; }
public decimal? TriggerPrice { get; set; }
public decimal? LimitPrice { get; set; }
public decimal Quantity { get; set; }
public OrderTicket Ticket { get; set; }
public CustomOrderEvent(QCAlgorithm algo, OrderEvent orderEvent, int EventCounter)
{
EventIdentifier = Convert.ToString(orderEvent.Symbol + orderEvent.OrderId);
OrderId = EventCounter;
Symbol = orderEvent.Symbol;
var order = algo.Transactions.GetOrderById(orderEvent.OrderId);
Tag = order.Tag;
Status = orderEvent.Status;
Processed = false;
TimeSubmitted = orderEvent.UtcTime;
// Additional attributes
FillPrice = orderEvent.FillPrice;
FillPriceCurrency = orderEvent.FillPriceCurrency;
FillQuantity = orderEvent.FillQuantity;
StopPrice = orderEvent.StopPrice;
TriggerPrice = orderEvent.TriggerPrice;
LimitPrice = orderEvent.LimitPrice;
Quantity = orderEvent.Quantity;
}
}
private object GetSymbolLock(Symbol symbol)
{
lock (symbolLocks)
{
if (!symbolLocks.ContainsKey(symbol))
{
symbolLocks[symbol] = new object();
}
return symbolLocks[symbol];
}
}
public override void OnOrderEvent(OrderEvent orderEvent)
{
lock (GetSymbolLock(orderEvent.Symbol))
{
EventCounter++;
var customOrderEvent = new CustomOrderEvent(this, orderEvent, EventCounter);
if (!TradableProductsDict[orderEvent.Symbol].OrderEventsDict.ContainsKey(customOrderEvent.EventIdentifier))
{
TradableProductsDict[orderEvent.Symbol].OrderEventsDict.Add(customOrderEvent.EventIdentifier, customOrderEvent);
}
if (TradableProductsDict[orderEvent.Symbol].OrderEventsDict.ContainsKey(customOrderEvent.EventIdentifier))
{
ProcessQuedOrderEvents(orderEvent.Symbol);
}
}
}
public void ProcessQuedOrderEvents(Symbol symbol)
{
lock (symbolLocks)
{
var tp = TradableProductsDict[symbol];
tp.OrderEventsDict = tp.OrderEventsDict.OrderBy(x => x.Value.TimeSubmitted).ToDictionary(x => x.Key, x => x.Value);
foreach (var customOrderEvent in tp.OrderEventsDict.Values)
{
OrderEventQue(customOrderEvent);
if (customOrderEvent.Processed)
{
tp.OrderEventsDict.Remove(customOrderEvent.EventIdentifier);
}
}
}
}
//Here we wait for the Trade to be added to the Trades Dictionary before we continue on to analyse the OrderEvent
public void OrderEventQue(CustomOrderEvent customOrderEvent)
{
lock (symbolLocks)
{
TradableProduct tp = TradableProductsDict[customOrderEvent.Symbol];
//var order = Transactions.GetOrderById(customOrderEvent.OrderId);
int eventOrderID = customOrderEvent.OrderId;
if (customOrderEvent.Tag == null || customOrderEvent.Status == OrderStatus.Invalid || customOrderEvent.Status != OrderStatus.None)
return;
if (customOrderEvent.Tag == "Entry")
{
// Check if the item is already present in the dictionary or if the dictionary is null
while (!tp.Trades.ContainsKey(customOrderEvent.OrderId) || tp.Trades[customOrderEvent.OrderId] == null)
{
// Create a ManualResetEventSlim if it doesn't exist
ManualResetEventSlim resetEvent1 = waitHandles.GetOrAdd(eventOrderID, new ManualResetEventSlim(false));
// Wait until the item is added
resetEvent1.Wait();
}
if (tp.Trades.ContainsKey(customOrderEvent.OrderId) && waitHandles.TryRemove(customOrderEvent.OrderId, out var resetEvent))
{
resetEvent.Set();
resetEvent.Dispose();
}
}
if (customOrderEvent.Status != OrderStatus.Filled && customOrderEvent.Status != OrderStatus.Canceled && customOrderEvent.Status != OrderStatus.PartiallyFilled) // removed partial fills
{
return;
}
switch (customOrderEvent.Tag)
{
case "Entry":
Trade trade = tp.Trades[eventOrderID];
OrderEventCompletion(trade, customOrderEvent);
break;
case "Stop Loss":
foreach(var t in tp.Trades.Values)
{
if (t.StopLossOrder.OrderId == customOrderEvent.OrderId)
{
OrderEventCompletion(t, customOrderEvent);
}
}
break;
case "Target":
foreach(var t in tp.Trades.Values)
{
if (t.TargetOrder.OrderId == customOrderEvent.OrderId)
{
OrderEventCompletion(t, customOrderEvent);
}
}
break;
default:
throw new Exception("Order Tag Not Recognised");
}
}
}
public void OrderEventCompletion(Trade trade, CustomOrderEvent customOrderEvent)
{
lock (symbolLocks)
{
int eventOrderID = customOrderEvent.OrderId;
if (customOrderEvent.Status == OrderStatus.PartiallyFilled)
{
if (eventOrderID == trade.EntryOrder.OrderId)
{
if (trade.Side == TradeSide.Long)
{
trade.HoldingQuantity += customOrderEvent.FillQuantity;
Console.WriteLine($"Trade {trade.EntryOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
else if (trade.Side == TradeSide.Short)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
Console.WriteLine($"Trade {trade.EntryOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
decimal quantity = trade.HoldingQuantity * -1;
trade.StopLossOrder = StopMarketOrder(trade.Symbol, quantity, trade.StopLossPrice, tag:"Stop Loss");
trade.TargetOrder = LimitOrder(trade.Symbol, quantity, trade.TargetPrice, tag:"Target");
Console.WriteLine($"Stop Loss Order {trade.StopLossOrder.OrderId} and Target Order {trade.TargetOrder.OrderId} Created");
}
else if (eventOrderID == trade.StopLossOrder.OrderId)
{
if (trade.TargetOrder == null || trade.TargetOrder.Status != OrderStatus.Submitted || trade.TargetOrder.Status != OrderStatus.Filled)
{
trade.TargetOrder = LimitOrder(trade.Symbol, trade.HoldingQuantity * -1, trade.TargetPrice, tag:"Target");
Console.WriteLine($"Target Order {trade.TargetOrder.OrderId} Created");
}
if (trade.Side == TradeSide.Long)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
trade.TargetOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Trade {trade.StopLossOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
else if (trade.Side == TradeSide.Short)
{
trade.HoldingQuantity += customOrderEvent.FillQuantity;
trade.TargetOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Trade {trade.StopLossOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
}
else if (eventOrderID == trade.TargetOrder.OrderId)
{
if (trade.StopLossOrder == null || trade.StopLossOrder.Status != OrderStatus.Submitted || trade.StopLossOrder.Status != OrderStatus.Filled)
{
decimal quantity = trade.HoldingQuantity * -1;
trade.StopLossOrder = StopMarketOrder(trade.Symbol, quantity, trade.StopLossPrice, tag:"Stop Loss");
Console.WriteLine($"Stop Loss Order {trade.StopLossOrder.OrderId} Created");
}
if (trade.Side == TradeSide.Long)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
trade.StopLossOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Trade {trade.TargetOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
else if (trade.Side == TradeSide.Short)
{
trade.HoldingQuantity += customOrderEvent.FillQuantity;
trade.StopLossOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Trade {trade.TargetOrder.OrderId} Partially Filled, Holding Quantity = {trade.HoldingQuantity}");
}
}
}
else if (customOrderEvent.Status == OrderStatus.Filled)
{
if (eventOrderID == trade.EntryOrder.OrderId)
{
trade.HoldingQuantity += customOrderEvent.FillQuantity;
if (trade.StopLossOrder != null)
{
trade.StopLossOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Stop Loss Order {trade.StopLossOrder.OrderId} Updated");
}
else if (trade.StopLossOrder == null)
{
decimal quantity = trade.HoldingQuantity * -1;
trade.StopLossOrder = StopMarketOrder(trade.Symbol, quantity, trade.StopLossPrice, tag:"Stop Loss");
Console.WriteLine($"Stop Loss Order {trade.StopLossOrder.OrderId} Created");
}
if (trade.TargetOrder != null)
{
trade.TargetOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
Console.WriteLine($"Target Order {trade.TargetOrder.OrderId} Updated");
}
else if (trade.TargetOrder == null)
{
decimal quantity = trade.HoldingQuantity * -1;
trade.TargetOrder = LimitOrder(trade.Symbol, quantity, trade.TargetPrice, tag:"Target");
Console.WriteLine($"Target Order {trade.TargetOrder.OrderId} Created");
}
}
else if (eventOrderID == trade.StopLossOrder.OrderId)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
if (trade.HoldingQuantity > 0)
{
trade.TargetOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
}
if (trade.HoldingQuantity == 0 && trade.EntryOrder.Status == OrderStatus.Filled)
{
trade.Result = Result.Exit;
if (trade.TargetOrder != null || trade.TargetOrder.Status != OrderStatus.Filled)
{
trade.TargetOrder.Cancel();
}
TradableProductsDict[customOrderEvent.Symbol].ManageResult(eventOrderID, trade);
}
}
else if (eventOrderID == trade.TargetOrder.OrderId)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
if (trade.HoldingQuantity > 0)
{
trade.StopLossOrder.Update(new UpdateOrderFields
{
Quantity = trade.HoldingQuantity
});
}
if (trade.HoldingQuantity == 0 && trade.EntryOrder.Status == OrderStatus.Filled)
{
trade.Result = Result.Exit;
if (trade.StopLossOrder != null || trade.StopLossOrder.Status != OrderStatus.Filled)
{
trade.StopLossOrder.Cancel();
}
TradableProductsDict[customOrderEvent.Symbol].ManageResult(eventOrderID, trade);
}
}
else if (eventOrderID == trade.ExitOrder.OrderId)
{
trade.HoldingQuantity -= customOrderEvent.FillQuantity;
trade.Result = Result.Exit;
if (trade.EntryOrder.Status != OrderStatus.Filled)
{
trade.EntryOrder.Cancel();
}
if (trade.StopLossOrder != null)
{
trade.StopLossOrder.Cancel();
}
if (trade.TargetOrder != null)
{
trade.TargetOrder.Cancel();
}
TradableProductsDict[customOrderEvent.Symbol].ManageResult(eventOrderID, trade);
}
}
else if (customOrderEvent.Status == OrderStatus.Canceled)
{
if (eventOrderID == trade.EntryOrder.OrderId)
{
trade.Result = Result.Exit;
if (trade.StopLossOrder != null && trade.HoldingQuantity == 0)
{
trade.StopLossOrder.Cancel();
}
if (trade.TargetOrder != null && trade.HoldingQuantity == 0)
{
trade.TargetOrder.Cancel();
}
TradableProductsDict[customOrderEvent.Symbol].ManageResult(eventOrderID, trade);
}
else if (eventOrderID == trade.StopLossOrder.OrderId && trade.HoldingQuantity == 0)
{
decimal quantity = trade.HoldingQuantity * -1;
trade.StopLossOrder = StopMarketOrder(trade.Symbol, quantity, trade.StopLossPrice, tag:"Stop Loss");
}
else if (eventOrderID == trade.TargetOrder.OrderId && trade.HoldingQuantity == 0)
{
decimal quantity = trade.HoldingQuantity * -1;
trade.TargetOrder = LimitOrder(trade.Symbol, quantity, trade.TargetPrice, tag:"Target");
}
}
customOrderEvent.Processed = true;
}
}
//Trade Class: Contains all the information about a trade and Stores data for ML methods
public class Trade
{
public Symbol Symbol {get; set;}
public TradeSide Side {get; set;}
public OrderTicket EntryOrder {get; set;}
public decimal StopLossPrice {get; set;}
public decimal TargetPrice {get; set;}
public Result Result {get; set;}
public int Strength {get; set;}
public TrendDirection? Trend {get; set;}
public OrderTicket ExitOrder {get; set;}
public OrderTicket StopLossOrder {get; set;}
public OrderTicket TargetOrder {get; set;}
public int BarSinceEntry {get; set;}
public decimal SignalBarHigh {get; set;}
public decimal SignalBarLow {get; set;}
public decimal HoldingQuantity {get; set;}
public int ID {get; set;}
//Save Rolling Window Data and Pre Entry Variables of the trade for ML methods
public Dictionary<string, List<QuoteBar>> BarRollingWindows {get; set;}
public Dictionary<string, List<decimal>> DecimalIndicatorRollingWindows {get; set;}
public Dictionary<string, List<IndicatorDataPoint>> DataPointIndicatorRollingWindows {get; set;}
public TradableProduct TradableProduct {get; set;}
public Trade(TradableProduct Product, Symbol symbol, OrderTicket EntryOrder, decimal stopLossPrice, decimal targetPrice)
{
Result = Result.InProgress;
this.HoldingQuantity = 0;
this.EntryOrder = EntryOrder;
this.ID = EntryOrder.OrderId;
Symbol = symbol;
this.Side = EntryOrder.Quantity > 0 ? TradeSide.Long : TradeSide.Short;
this.TradableProduct = Product;
Strength = Product.Strength;
Trend = Product.Trend;
BarSinceEntry = 0;
this.SignalBarHigh = Product.SignalBarHigh;
this.SignalBarLow = Product.SignalBarLow;
}
}
//Tradeable Product Class: Manages Trades Internally by Symbol and provides methods for managing trades and Analysing the product
public class TradableProduct
{
public Forex Product {get; set;}
public Symbol Symbol {get; set;}
public TrendDirection Trend {get; set;}
public int Strength {get; set;}
internal IEnumerable<QuoteBar> ResolutionHistory {get; set;}
public bool Watching {get; set;}
internal RollingWindow<QuoteBar> ResolutionWindow {get; set;}
public RollingWindow<QuoteBar> MinuteWindow {get; set;}
public RollingWindow<QuoteBar> DailyWindow {get; set;}
public RollingWindow<QuoteBar> WeeklyWindow {get; set;}
internal QuoteBarConsolidator mConsolidator {get; set;}
internal QuoteBarConsolidator dConsolidator {get; set;}
internal QuoteBarConsolidator wConsolidator {get; set;}
internal bool MinuteBarClosed {get; set;}
internal bool DailyBarClosed {get; set;}
internal bool WeeklyBarClosed {get; set;}
public bool BullishSignalBar {get; set;}
public bool BearishSignalBar {get; set;}
public Dictionary<int, Trade> Trades { get; set; } = new Dictionary<int, Trade>();
public Dictionary<int, Trade> HistoricalTrades {get; set;} = new Dictionary<int, Trade>();
public bool Subscribed {get; set;}
public QCAlgorithm Algorithm {get; set;}
public decimal SignalBarHigh {get; set;}
public decimal SignalBarLow {get; set;}
internal SequentialConsolidator oneDayBar { get; set; }
internal SequentialConsolidator oneWeekBar { get; set; }
public bool TrailingStopEnabled {get; set;}
public decimal TradeSize {get; set;}
public int MinimumTrendStrengthPercent {get; set;}
public bool NewTradeAllowed {get; set;}
public RollingWindow<IndicatorDataPoint> RSI {get; set;}
//public RollingWindow<decimal> AverageBandwidth {get; set;}
public RollingWindow<IndicatorDataPoint> BollingerBands {get; set;}
public BollingerBands BollingerBand {get; set;}
public RelativeStrengthIndex RelativeStrengthIndex {get; set;}
public int TradeNumber {get; set;}
public int MaxTrades {get; set;}
public Trade LatestTrade {get; set;}
public Dictionary<string, CustomOrderEvent> OrderEventsDict = new Dictionary<string, CustomOrderEvent>();
public TradableProduct(QCAlgorithm Algorithm, Forex Forex, Symbol Symbol, int Periods, bool TrailingStopEnabled, decimal TradeSize, int MinimumTrendStrengthPercent)
{
this.Algorithm = Algorithm;
this.Product = Forex;
this.Symbol = Symbol;
Watching = false;
MinuteBarClosed = false;
BullishSignalBar = false;
BearishSignalBar = false;
this.TradeSize = TradeSize;
ResolutionWindow = new RollingWindow<QuoteBar>(Periods);
Trades = new();
HistoricalTrades = new();
Subscribed = false;
this.TrailingStopEnabled = TrailingStopEnabled;
this.MinimumTrendStrengthPercent = MinimumTrendStrengthPercent;
NewTradeAllowed = true;
TradeNumber = 0;
MaxTrades = 1;
Console.WriteLine($"Created {this.Symbol} Tradable Product");
}
public void Liquidate()
{
if (Trades == null || !Trades.Any())
return;
foreach (var trade in Trades)
{
trade.Value.Result = Result.Exit;
Exit(trade.Key, trade.Value);
}
}
public void Exit(int key, Trade trade)
{
if (trade.Result != Result.Exit)
return;
trade.ExitOrder = Algorithm.MarketOrder(trade.Symbol, trade.EntryOrder.Quantity * -1);
ManageResult(key, trade);
}
// ManageResult: Label trades as Win, Loss, or BreakEven
public void ManageResult(int key, Trade trade)
{
if (trade.Result != Result.Exit)
return;
decimal entryFillQuantity = trade.EntryOrder.QuantityFilled;
decimal stopLossFillQuantity = trade.StopLossOrder?.QuantityFilled ?? 0m;
decimal targetFillQuantity = trade.TargetOrder?.QuantityFilled ?? 0m;
decimal exitFillQuantity = trade.ExitOrder?.QuantityFilled ?? 0m;
decimal entryAverageFillPrice = trade.EntryOrder.AverageFillPrice;
decimal stopLossAverageFillPrice = trade.StopLossOrder?.AverageFillPrice ?? 0m;
decimal targetAverageFillPrice = trade.TargetOrder?.AverageFillPrice ?? 0m;
decimal exitAverageFillPrice = trade.ExitOrder?.AverageFillPrice ?? 0m;
// Calculate filled quantities
decimal totalFilledQuantity = stopLossFillQuantity + targetFillQuantity + exitFillQuantity;
// Calculate average fill price
decimal averageFillPrice = ( (stopLossAverageFillPrice * stopLossFillQuantity)
+ (targetAverageFillPrice * targetFillQuantity)
+ (exitAverageFillPrice * exitFillQuantity)) / totalFilledQuantity;
decimal entryPrice = trade.EntryOrder.AverageFillPrice;
// Calculate the result based on the filled quantities and average fill price
trade.Result = CalculateResult(trade.Side, entryPrice, averageFillPrice);
SaveHistoricalTrade(trade);
string logMsg = $"{trade.Symbol} {trade.Side} {trade.Result}";
Algorithm.Log(logMsg);
Console.WriteLine(logMsg);
}
private Result CalculateResult(TradeSide side, decimal entryAverageFillPrice, decimal averageFillPrice)
{
if (side == TradeSide.Long)
{
if (averageFillPrice > entryAverageFillPrice)
return Result.Win;
else if (averageFillPrice < entryAverageFillPrice)
return Result.Loss;
else
return Result.BreakEven;
}
else if (side == TradeSide.Short)
{
if (averageFillPrice < entryAverageFillPrice)
return Result.Win;
else if (averageFillPrice > entryAverageFillPrice)
return Result.Loss;
else
return Result.BreakEven;
}
// Handle the case when the side is not Long or Short
// Add appropriate error handling here
return Result.Error;
}
// SaveHistoricalTrade: Move trade from Trades to HistoricalTrades for record keeping and ML training
public void SaveHistoricalTrade(Trade trade)
{
if (!Trades.ContainsKey(trade.EntryOrder.OrderId))
{
throw new Exception("Unable to save historical trade, trade not found in Trades Dictionary");
}
var HistoricalTrade = trade;
this.HistoricalTrades.Add(trade.EntryOrder.OrderId, HistoricalTrade);
this.Trades.Remove(trade.EntryOrder.OrderId);
TradeNumber--;
Console.WriteLine($"Saved Historical Trade for {this.Symbol}");
}
public void TrailStops(Dictionary<int, Trade> Trades)
{
foreach (var trade in Trades)
{
if (!TrailingStopEnabled)
return;
if (Algorithm.Portfolio[this.Symbol].IsLong)
{
var Distance = trade.Value.EntryOrder.AverageFillPrice - trade.Value.StopLossPrice;
var newStopPrice = Math.Round(trade.Value.EntryOrder.AverageFillPrice - Distance, 5);
if (newStopPrice > trade.Value.StopLossPrice)
{
trade.Value.StopLossPrice = newStopPrice;
if (trade.Value.StopLossOrder != null && trade.Value.StopLossOrder.Status == OrderStatus.Submitted)
{
trade.Value.StopLossOrder.Update(new UpdateOrderFields
{
StopPrice = trade.Value.StopLossPrice
});
}
Algorithm.Log($"{this.Symbol} Stop Loss Updated to {trade.Value.StopLossPrice}");
Console.WriteLine($"{this.Symbol} Stop Loss Updated to {trade.Value.StopLossPrice}");
}
}
if (Algorithm.Portfolio[this.Symbol].IsShort)
{
var Distance = trade.Value.StopLossPrice - trade.Value.EntryOrder.AverageFillPrice;
var newStopPrice = Math.Round(trade.Value.EntryOrder.AverageFillPrice + Distance, 5);
if (newStopPrice < trade.Value.StopLossPrice)
{
trade.Value.StopLossPrice = newStopPrice;
if (trade.Value.StopLossOrder != null && trade.Value.StopLossOrder.Status == OrderStatus.Submitted)
{
trade.Value.StopLossOrder.Update(new UpdateOrderFields
{
StopPrice = trade.Value.StopLossPrice
});
}
Algorithm.Log($"{this.Symbol} Stop Loss Updated to {trade.Value.StopLossPrice}");
Console.WriteLine($"{this.Symbol} Stop Loss Updated to {trade.Value.StopLossPrice}");
}
}
}
}
//Store windows at trade entry to use for later training of ML model using the saved data
public void StoreWindows(Trade trade)
{
trade.BarRollingWindows = new(4);
trade.DecimalIndicatorRollingWindows = new(3);
trade.DataPointIndicatorRollingWindows = new(1);
trade.BarRollingWindows.Add("ResolutionWindow", ResolutionWindow.ToList());
trade.BarRollingWindows.Add("MinuteWindow", MinuteWindow.ToList());
trade.BarRollingWindows.Add("DailyWindow", DailyWindow.ToList());
trade.BarRollingWindows.Add("WeeklyWindow", WeeklyWindow.ToList());
//trade.DecimalIndicatorRollingWindows.Add(AverageBandwidth.ToList(), "AverageBandwidth");
trade.DataPointIndicatorRollingWindows.Add("BollingerBands", BollingerBands.ToList());
trade.DataPointIndicatorRollingWindows.Add("RSI", RSI.ToList());
}
public void EntryMonitor() //Need to make it so can take opposing trade if i am long or short and new trades not allowed
{
if (TradeNumber > MaxTrades)
return;
if (Algorithm.IsWarmingUp)
return;
if (!this.MinuteWindow.IsReady || !this.DailyWindow.IsReady || !this.WeeklyWindow.IsReady || !this.ResolutionWindow.IsReady || !this.RSI.IsReady || !this.NewTradeAllowed)
return;
if (this.Strength < MinimumTrendStrengthPercent )
return;
//Check our strongest Trends and set positions
if (this.Trend == TrendDirection.Bullish && this.BullishSignalBar && RSI[0].Value < 35)
{
if (Algorithm.Portfolio[this.Symbol].IsShort)
{
this.Liquidate();
}
TradeNumber++;
var Price = MinuteWindow[0].Close;
var entry = Algorithm.MarketOrder(this.Symbol, TradeSize, true, tag:"Entry");
decimal stopPrice = Math.Round(Price * 0.9975m, 5);
decimal limitPrice = Math.Round(Price * 1.005m, 5);
Trade trade = new Trade(this, this.Symbol, entry, stopPrice, limitPrice);
StoreWindows(trade);
Trades.Add(trade.EntryOrder.OrderId, trade);
Console.WriteLine($"{trade.Symbol}, Trade {trade.Result}, {trade.EntryOrder.Quantity}, Trend {this.Trend} {this.Strength}");
Algorithm.Log($"{trade.Symbol}, Trade {trade.Result}, {trade.EntryOrder.Quantity}, Trend {this.Trend} {this.Strength}");
}
if (this.Trend == TrendDirection.Bearish && this.BearishSignalBar && RSI[0].Value > 65)
{
if (Algorithm.Portfolio[this.Symbol].IsLong)
{
this.Liquidate();
}
TradeNumber++;
var Price = MinuteWindow[0].Close;
decimal stopPrice = Math.Round(Price * 1.0025m, 5);
decimal limitPrice = Math.Round(Price * 0.995m, 5);
var entry = Algorithm.MarketOrder(this.Symbol, -TradeSize, true, tag:"Entry");
Trade trade = new Trade(this, this.Symbol, entry, stopPrice, limitPrice);
StoreWindows(trade);
Trades.Add(trade.EntryOrder.OrderId, trade);
Console.WriteLine($"{trade.Symbol}, Trade {trade.Result}, {trade.EntryOrder.Quantity}, Trend {this.Trend} {this.Strength}");
Algorithm.Log($"{trade.Symbol}, Trade {trade.Result}, {trade.EntryOrder.Quantity}, Trend {this.Trend} {this.Strength}");
}
}
public void SignalBars(RollingWindow<QuoteBar> rollingWindow)
{
if (!this.MinuteWindow.IsReady || !this.MinuteBarClosed)
return;
if (this.MinuteWindow[0].Low < this.MinuteWindow[1].Low && this.MinuteWindow[0].Close > this.MinuteWindow[1].High
&& this.MinuteWindow[1].Close < this.MinuteWindow[1].Open)
{
this.BullishSignalBar = true;
this.BearishSignalBar = false;
this.SignalBarHigh = this.MinuteWindow[0].High; //try removing this and using opposing high as target
this.SignalBarLow = this.MinuteWindow[0].Low;
}
if (this.MinuteWindow[0].High > this.MinuteWindow[1].High && this.MinuteWindow[0].Close < this.MinuteWindow[1].Low
&& this.MinuteWindow[1].Close > this.MinuteWindow[1].Open)
{
this.BullishSignalBar = false;
this.BearishSignalBar = true;
this.SignalBarHigh = this.MinuteWindow[0].High;
this.SignalBarLow = this.MinuteWindow[0].Low;
}
if (this.BullishSignalBar)
{
if (this.MinuteWindow[0].Close < this.SignalBarLow)
{
this.BullishSignalBar = false;
}
}
if (this.BearishSignalBar)
{
if (this.MinuteWindow[0].Close > this.SignalBarHigh)
{
this.BearishSignalBar = false;
}
}
}
public decimal LowestLow(IEnumerable<QuoteBar> quoteBars, int periods)
{
var lastXBars = quoteBars.TakeLast(periods);
var lowestLow = lastXBars.Min(bar => bar.Low);
return lowestLow;
}
public decimal HighestHigh(IEnumerable<QuoteBar> quoteBars, int periods)
{
var lastXBars = quoteBars.TakeLast(periods);
var highestHigh = lastXBars.Max(bar => bar.High);
return highestHigh;
}
public void Analyse(int lookBackPeriod, int MinimumTrendStrengthPercent)
{
TrendAndStrength(lookBackPeriod, MinimumTrendStrengthPercent);
Algorithm.Log($"{this.Symbol} Analysis Successful");
}
public void Subscribe()
{
Subscriber();
Algorithm.Log($"{this.Symbol} Subscribe Successful");
}
public void Unsubscribe()
{
Unsubscriber();
Algorithm.Log($"{this.Symbol} UnSubscribe Successful");
}
public void ActivateWindows(int Periods)
{
MinuteWindow = new RollingWindow<QuoteBar>(50);
DailyWindow = new RollingWindow<QuoteBar>(31);
WeeklyWindow = new RollingWindow<QuoteBar>(4);
RSI = new RollingWindow<IndicatorDataPoint>(15);
BollingerBands = new RollingWindow<IndicatorDataPoint>(50);
//AverageBandwidth = new RollingWindow<decimal>(100);
BollingerBand = Algorithm.BB(this.Symbol, 30, 2);
RelativeStrengthIndex = Algorithm.RSI(this.Symbol, 14);
mConsolidator = new QuoteBarConsolidator(TimeSpan.FromMinutes(15)); //change minutes per bar here
dConsolidator = new QuoteBarConsolidator(TimeSpan.FromDays(1)); //change days per bar here
wConsolidator = new QuoteBarConsolidator(TimeSpan.FromDays(5)); //change days per bar here
var oneCountConsolidator = new QuoteBarConsolidator(1); //change days per bar here
var fiveCountConsolidator = new QuoteBarConsolidator(5); //change days per bar here
oneDayBar = new SequentialConsolidator(dConsolidator, oneCountConsolidator);
oneWeekBar = new SequentialConsolidator(wConsolidator, fiveCountConsolidator);
mConsolidator.DataConsolidated += mBarHandler;
dConsolidator.DataConsolidated += (sender, consolidated) => dBarHandler(sender, (QuoteBar) consolidated);
wConsolidator.DataConsolidated += (sender, consolidated) => wBarHandler(sender, (QuoteBar) consolidated);
foreach (var bar in this.ResolutionHistory){
this.ResolutionWindow.Add(bar);
}
this.ResolutionHistory = null;
}
internal void mBarHandler(object sender, QuoteBar consolidated)
{
this.MinuteWindow.Add(consolidated);
MinuteBarClosed = true;
SignalBars(this.MinuteWindow);
EntryMonitor();
TrailStops(Trades);
}
internal void dBarHandler(object sender, QuoteBar consolidated)
{
this.DailyWindow.Add(consolidated);
DailyBarClosed = true;
}
internal void wBarHandler(object sender, QuoteBar consolidated)
{
this.WeeklyWindow.Add(consolidated);
WeeklyBarClosed = true;
}
internal void Subscriber()
{
Algorithm.SubscriptionManager.AddConsolidator(this.Symbol, this.mConsolidator);
Algorithm.SubscriptionManager.AddConsolidator(this.Symbol, this.oneDayBar);
Algorithm.SubscriptionManager.AddConsolidator(this.Symbol,this.oneWeekBar);
Algorithm.RegisterIndicator(Symbol ,RelativeStrengthIndex, mConsolidator);
Algorithm.RegisterIndicator(Symbol ,BollingerBand, mConsolidator);
RelativeStrengthIndex.Updated += (sender, updated) => RSI.Add(updated);
BollingerBand.Updated += (sender, updated) => BollingerBands.Add(updated);
this.Subscribed = true;
}
internal void Unsubscriber() //Currently not unsubscribing symbols because History only updates once per day, therefore need to watch all symbols
{
Algorithm.SubscriptionManager.RemoveConsolidator(this.Symbol, this.mConsolidator);
Algorithm.SubscriptionManager.RemoveConsolidator(this.Symbol, this.oneDayBar);
Algorithm.SubscriptionManager.RemoveConsolidator(this.Symbol,this.oneWeekBar);
this.Subscribed = false;
}
//Determine Trend by "How many bars for a new high or low as a percentage of total bars"
internal void TrendAndStrength(int lookBackPeriod, int MinimumTrendStrengthPercent)
{
if (!this.MinuteWindow.IsReady)
return;
decimal minimumTrendStrength = MinimumTrendStrengthPercent / 100m;
int bearTrendBars = 0;
int bullTrendBars = 0;
decimal high = decimal.MinValue;
decimal low = decimal.MaxValue;
if (lookBackPeriod > this.MinuteWindow.Count)
return;
int trendStrengthCriteria = (int)Math.Round(lookBackPeriod * minimumTrendStrength);
for (int i = (lookBackPeriod - 1); i >= 0; i--){
if (this.MinuteWindow[i].High > high)
{
high = this.MinuteWindow[i].High;
bullTrendBars++;
bearTrendBars = 0; // Reset bear trend bars count as a higher high indicates a possible bullish trend
}
if (this.MinuteWindow[i].Low < low)
{
low = this.MinuteWindow[i].Low;
bearTrendBars++;
bullTrendBars = 0; // Reset bull trend bars count as a lower low indicates a possible bearish trend
}
}
if (bullTrendBars >= trendStrengthCriteria)
{
this.Trend = TrendDirection.Bullish;
this.Strength = CalculateStrength(bullTrendBars, lookBackPeriod);
}
else if (bearTrendBars >= trendStrengthCriteria)
{
this.Trend = TrendDirection.Bearish;
this.Strength = CalculateStrength(bearTrendBars, lookBackPeriod);
}
else
{
this.Trend = TrendDirection.Range;
this.Strength = 0;
}
this.Watching = IsWatching(MinimumTrendStrengthPercent);
//Console.WriteLine($"{this.Symbol} Trend: {this.TrendDirection} Strength: {this.Strength}");
}
private int CalculateStrength(int trendBars, int lookBackPeriod)
{
return (int)Math.Round(((double)trendBars / lookBackPeriod) * 100);
}
private bool IsWatching(int MinimumTrendStrengthPercent)
{
if (this.Trend == TrendDirection.Range || this.Trend == TrendDirection.Error)
return false;
if (this.Strength >= MinimumTrendStrengthPercent)
return true;
else
{
this.Strength = 0;
return false;
}
}
// private decimal AverageBollingerBandwidth(int lookBackPeriod)
// {
// var averageBandwidthList = BollingerBands.Take(lookBackPeriod);
// //var CurrentAverage = BollingerBands.;
// return CurrentAverage;
// }
}
}
}
AgedVagabond
I separated everything into seperate Libraries and the VS Code Extension hates me for it ❤️
AgedVagabond
AgedVagabond
realised I accidentally shared my login info in .json so will have to fix link later…. no way to delete post and start over?
AgedVagabond
Well mostly got it working, bracket orders work most of the time but as I make changes they either stop working or at least the thread exits before writeline executes, either way it is close enough for a template.
Trade Class can manage its own order events and update Stop Loss quantity by Target Limit Partial fills and Target and Stop Loss should update by Partial Limit entry fills.
AgedVagabond
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!