#region Using declarations using System.ComponentModel.DataAnnotations; using System.Net.Http; using NinjaTrader.Cbi; using NinjaTrader.Gui; using NinjaTrader.Gui.Chart; using NinjaTrader.NinjaScript.DrawingTools; using SharpDX; using SharpDX.Direct2D1; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Media; using System.Security.Cryptography; using System.Text; using System.Runtime.InteropServices; using SolidColorBrush = System.Windows.Media.SolidColorBrush; #endregion namespace NinjaTrader.NinjaScript.Strategies { [CategoryOrder("Setup", 0)] [CategoryOrder("GlobalSettings", 1)] [CategoryOrder("PositionSettings", 2)] [CategoryOrder("TimeWindowSettings", 3)] [CategoryOrder("EntrySettings", 4)] [CategoryOrder("FilterSettings", 5)] [CategoryOrder("ExitSettings", 6)] public class IFVGBOTVincereTrading : Strategy { private const string SystemVersion = " V1.4"; private const string StrategyName = "IFVG Bot - Vincere Trading"; // Whop license validation private bool isValidated = false; private static Dictionary licenseCache = new Dictionary(); private const int CACHE_DURATION_HOURS = 24; private string cachedHardwareId = null; [NinjaScriptProperty] [Display(Name = "License Key", Description = "Enter your Whop license key", Order = 1, GroupName = "License")] public string LicenseKey { get; set; } // P/Invoke declarations for hardware info [DllImport("kernel32.dll")] private static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); [StructLayout(LayoutKind.Sequential)] private struct SYSTEM_INFO { public ushort wProcessorArchitecture; public ushort wReserved; public uint dwPageSize; public IntPtr lpMinimumApplicationAddress; public IntPtr lpMaximumApplicationAddress; public IntPtr dwActiveProcessorMask; public uint dwNumberOfProcessors; public uint dwProcessorType; public uint dwAllocationGranularity; public ushort wProcessorLevel; public ushort wProcessorRevision; } private string GetHardwareId() { if (!string.IsNullOrEmpty(cachedHardwareId)) return cachedHardwareId; try { var hardwareInfo = new StringBuilder(); // Get system information using P/Invoke GetSystemInfo(out SYSTEM_INFO sysInfo); // Add processor information hardwareInfo.Append(sysInfo.dwProcessorType) .Append(sysInfo.wProcessorArchitecture) .Append(sysInfo.wProcessorLevel) .Append(sysInfo.wProcessorRevision) .Append(sysInfo.dwNumberOfProcessors); // Add environment-specific information hardwareInfo.Append(Environment.ProcessorCount) .Append(Environment.MachineName) .Append(Environment.OSVersion.Version.ToString()) .Append(Environment.UserName) .Append(Environment.SystemDirectory); // Hash the hardware info for consistency and privacy using (var sha256 = SHA256.Create()) { var bytes = Encoding.UTF8.GetBytes(hardwareInfo.ToString()); var hash = sha256.ComputeHash(bytes); cachedHardwareId = BitConverter.ToString(hash).Replace("-", ""); } return cachedHardwareId; } catch (Exception ex) { Log($"Error generating hardware ID: {ex.Message}", LogLevel.Error); return null; } } private bool ValidateLicense() { if (string.IsNullOrEmpty(LicenseKey)) { Log("License key is required. Please enter your Whop license key in the strategy properties.", LogLevel.Error); return false; } // Get hardware ID first var hwid = GetHardwareId(); if (string.IsNullOrEmpty(hwid)) { Log("Unable to generate hardware ID. License validation cannot proceed.", LogLevel.Error); return false; } // Check cache first with HWID validation if (licenseCache.ContainsKey(LicenseKey)) { var cacheTime = licenseCache[LicenseKey]; if (DateTime.Now.Subtract(cacheTime).TotalHours < CACHE_DURATION_HOURS) { Log("Using cached license validation.", LogLevel.Information); isValidated = true; return true; } else { licenseCache.Remove(LicenseKey); } } try { using (var client = new System.Net.Http.HttpClient()) { // Hardcoded API key const string apiKey = "A6FHGMmhg3PXodjW1XFmYx7Tg5cQgQlEETvTckfIQ1o"; client.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}"); // Create the request content with HWID in metadata var hwidMetadata = $"{{\"metadata\": {{\"HWID\": \"{hwid}\"}}}}"; var content = new StringContent( hwidMetadata, System.Text.Encoding.UTF8, "application/json" ); // Make POST request to validate license var requestUrl = $"https://api.whop.com/api/v2/memberships/{LicenseKey}/validate_license"; Log($"Making request to: {requestUrl}", LogLevel.Information); var response = client.PostAsync(requestUrl, content) .Result.Content.ReadAsStringAsync().Result; Log($"Raw API Response: {response}", LogLevel.Information); // Parse the response using string contains to handle all validation cases bool isValid = (response.Contains("\"status\":\"active\"") || response.Contains("\"status\":\"trialing\"") || response.Contains("\"status\":\"completed\"")) && response.Contains("\"valid\":true"); if (isValid) { Log("License validated successfully.", LogLevel.Information); // Cache the successful validation licenseCache[LicenseKey] = DateTime.Now; isValidated = true; return true; } else { Log("License validation failed. Response: " + response, LogLevel.Error); isValidated = false; return false; } } } catch (Exception ex) { // On connection error, check if we have a recently expired cache entry if (licenseCache.ContainsKey(LicenseKey)) { var cacheTime = licenseCache[LicenseKey]; // Allow a grace period of additional 24 hours if offline if (DateTime.Now.Subtract(cacheTime).TotalHours < (CACHE_DURATION_HOURS * 2)) { Log("Using grace period for offline validation.", LogLevel.Warning); isValidated = true; return true; } } Log($"License validation error: {ex.Message}", LogLevel.Error); isValidated = false; return false; } } private const string GlobalSettings = "Global Settings"; private const string EntrySettings = "Entry Settings"; private const string FilterSettings = "Filter Settings"; private const string ExitSettings = "Exit Settings"; private const string PositionSettings = "Position Settings"; private const string TimeWindowSettings = "Time Settings"; private const string EnterLongName = "Enter Long"; private const string EnterShortName = "Enter Short"; private const string ExitLongStopName = "Stop Long"; private const string ExitShortStopName = "Stop Short"; private const string ExitLongProfitName1 = "PT1-Long"; private const string ExitShortProfitName1 = "PT1-Short"; private const string MarketCloseLong = "Exit Long Market"; private const string MarketCloseShort = "Exit Short Market"; [NinjaScriptProperty] [Display(Name = "Log", GroupName = GlobalSettings, Order = 5)] public bool LogIsOn { get; set; } [Display(Name = "Plot Stops", Order = 6, GroupName = GlobalSettings)] public bool PlotStops { get; set; } [NinjaScriptProperty] [Display(Name = "Trade Window", GroupName = TimeWindowSettings, Order = 13)] public bool TradeWindow1IsOn { get; set; } [PropertyEditor("NinjaTrader.Gui.Tools.AutoCloseTimeEditorKey")] [NinjaScriptProperty] [Display(Name = "Trade Start 1", GroupName = TimeWindowSettings, Order = 14)] public DateTime TradeStart1 { get; set; } [PropertyEditor("NinjaTrader.Gui.Tools.AutoCloseTimeEditorKey")] [NinjaScriptProperty] [Display(Name = "Trade End 1", GroupName = TimeWindowSettings, Order = 15)] public DateTime TradeEnd1 { get; set; } [NinjaScriptProperty] [Display(Name = "Min Gap Size Ticks", GroupName = EntrySettings, Order = 9)] public int MinGapSizeTicks { get; set; } [NinjaScriptProperty] [Display(Name = "Inversion Max Bars Wait", GroupName = EntrySettings, Order = 10)] public int MaxBarsWait { get; set; } [NinjaScriptProperty] [Display(Name = "Profit Target 1 Ratio", GroupName = ExitSettings, Order = 15)] public double ProfitTarget1Ratio { get; set; } [NinjaScriptProperty] [Display(Name = "Stop Offset Ticks", GroupName = ExitSettings, Order = 20)] public int StopOffsetTicks { get; set; } [NinjaScriptProperty] [Display(Name = "Break Even", GroupName = ExitSettings, Order = 21)] public bool BreakEvenIsOn { get; set; } [NinjaScriptProperty] [Display(Name = "Position Sizing Mode", GroupName = PositionSettings, Order = 3)] public PositionSizingMode MyPositionSizingMode { get; set; } [NinjaScriptProperty] [Display(Name = "Max Total Risk USD", GroupName = PositionSettings, Order = 4)] public double MaxTotalRiskUSD { get; set; } [NinjaScriptProperty] [Display(Name = "Pos Size 1", GroupName = PositionSettings, Order = 5)] public int PosSize1 { get; set; } // Private fields private List _gaps; private bool _enterLong; private bool _enterShort; private SolidColorBrush _bgColor; private int _barsCount; private bool _breakEvenApplied; private double _initialStopSize; private Order _exitMarketOrder; private Order _entryOrder; private Order _exitStopOrder; private Order _entryOrderShort; private Order _entryOrderLong; private Order _exitProfitOrder1; private double _stop; private double _profit1; protected override void OnStateChange() { try { base.OnStateChange(); switch (State) { case State.SetDefaults: Name = StrategyName + SystemVersion; Description = $"IFVG Bot - Vincere Trading Strategy Version {SystemVersion}"; Calculate = Calculate.OnBarClose; EntriesPerDirection = 1; EntryHandling = EntryHandling.AllEntries; IsExitOnSessionCloseStrategy = false; IsFillLimitOnTouch = false; IsInstantiatedOnEachOptimizationIteration = true; IsUnmanaged = true; OrderFillResolution = OrderFillResolution.Standard; BarsRequiredToTrade = 20; RealtimeErrorHandling = RealtimeErrorHandling.StopCancelCloseIgnoreRejects; StopTargetHandling = StopTargetHandling.PerEntryExecution; TimeInForce = TimeInForce.Gtc; // Initialize license key LicenseKey = string.Empty; // Initialize collections (except static cache) _gaps = new List(); // Set default parameters PosSize1 = 1; ProfitTarget1Ratio = 2; MinGapSizeTicks = 10; StopOffsetTicks = 0; MaxTotalRiskUSD = 500; TradeStart1 = new DateTime(2020, 01, 01, 9, 30, 0); TradeEnd1 = new DateTime(2020, 01, 01, 16, 30, 0); MaxBarsWait = 10; // Initialize trading flags _enterLong = false; _enterShort = false; _barsCount = 0; _breakEvenApplied = false; _initialStopSize = 0; _stop = 0; _profit1 = 0; // Initialize orders _exitMarketOrder = null; _entryOrder = null; _exitStopOrder = null; _entryOrderShort = null; _entryOrderLong = null; _exitProfitOrder1 = null; break; case State.Configure: // Perform license validation first if (!ValidateLicense()) { Log("Invalid or expired license key. Please enter a valid license key in the strategy properties.", LogLevel.Error); StopStrategy(); return; } if (ChartControl != null) { // Initialize UI elements _bgColor = new SolidColorBrush(Colors.LightGreen) { Opacity = 0.35 }; _bgColor.Freeze(); } // Force calculation mode Calculate = Calculate.OnBarClose; IsExitOnSessionCloseStrategy = false; TraceOrders = false; break; case State.DataLoaded: case State.Historical: case State.Realtime: // Only check if validation was successful if (!isValidated) { Log("License validation required. Strategy cannot proceed.", LogLevel.Error); StopStrategy(); return; } // Safe background brush handling for DataLoaded state if (State == State.DataLoaded) { try { if (ChartControl != null && Bars != null && CurrentBar >= 0) { BackBrush = null; Log("Initial background state cleared", LogLevel.Information); } } catch (Exception ex) { Log($"Error clearing background: {ex.Message}", LogLevel.Warning); } } break; } } catch (Exception ex) { Log($"Error in OnStateChange ({State}): {ex.Message}", LogLevel.Error); if (ex.StackTrace != null) { Log($"Stack trace: {ex.StackTrace}", LogLevel.Error); } StopStrategy(); } } private void SuperImpose() { if (Calculate == Calculate.OnEachTick || Calculate == Calculate.OnPriceChange) Calculate = Calculate.OnBarClose; if (IsExitOnSessionCloseStrategy) IsExitOnSessionCloseStrategy = false; TraceOrders = false; } private void InitIndicators() { // Initialize any indicators if needed } protected override void OnBarUpdate() { if (CurrentBar < 3) return; try { // Process trade window first if (TradeWindow1IsOn) { var isInWindow = IsInsideTradeWindow(); UpdateBackgroundColor(isInWindow); PrintLog($"Trade window active - Can trade: {isInWindow}"); // Create gaps regardless of trade window to maintain tracking CreateGaps(); // Only execute trades if we're inside the window if (isInWindow) { BreakEven(Close[0]); PlotExitLevels(); } else { PrintLog("Outside trading window - skipping trade execution"); } } else { BackBrush = null; PrintLog("Trade window disabled - proceeding with normal operation"); // When trade window is disabled, execute normally CreateGaps(); BreakEven(Close[0]); PlotExitLevels(); } } catch (Exception ex) { Log($"Error in OnBarUpdate: {ex.Message}", LogLevel.Error); } } private void UpdateBackgroundColor(bool isInWindow) { try { if (isInWindow) { BackBrush = _bgColor; PrintLog("Inside trading window - green background applied"); } else { BackBrush = null; PrintLog("Outside trading window - background cleared"); } } catch (Exception ex) { Log($"Error updating background color: {ex.Message}", LogLevel.Error); } } private bool IsInsideTradeWindow() { try { if (!TradeWindow1IsOn) { PrintLog("Trade window disabled - allowing trades"); return true; } var timeNow = ToTime(Time[0]); var startTime = ToTime(TradeStart1); var endTime = ToTime(TradeEnd1); PrintLog($"Trade window check - Current: {timeNow:HH:mm:ss}, Start: {startTime:HH:mm:ss}, End: {endTime:HH:mm:ss}"); bool isInWindow = false; if (startTime < endTime) { isInWindow = timeNow >= startTime && timeNow <= endTime; } else if (startTime > endTime) { isInWindow = timeNow >= startTime || timeNow <= endTime; } PrintLog($"Trade window status: {(isInWindow ? "Inside window - allowing trades" : "Outside window - blocking trades")}"); return isInWindow; } catch (Exception ex) { Log($"Error in IsInsideTradeWindow: {ex.Message}", LogLevel.Error); return false; // Fail-safe: don't trade if we can't determine the time window } } private void BreakEven(double price) { if (!BreakEvenIsOn || Position.MarketPosition == MarketPosition.Flat) { _breakEvenApplied = false; return; } if (_breakEvenApplied) return; if (Position.MarketPosition == MarketPosition.Long) { if (Close[0] > Position.AveragePrice + _initialStopSize) { _stop = Position.AveragePrice; ChangeStopExitOrder(); _breakEvenApplied = true; } } if (Position.MarketPosition == MarketPosition.Short) { if (Close[0] < Position.AveragePrice - _initialStopSize) { _stop = Position.AveragePrice; ChangeStopExitOrder(); _breakEvenApplied = true; } } } private class FairValueGap { public bool IsBullish { get; set; } public bool IsBearish { get; set; } public double TopValue { get; set; } public double BottomValue { get; set; } public bool IsValid { get; set; } public int BarStart { get; set; } public int BarEnd { get; set; } public DateTime DateTimeStart { get; set; } public DateTime DateTimeEnd { get; set; } } private void CreateGaps() { if (_gaps.Count == 0) { if (CreateBullishGap()) return; if (CreateBearishGap()) return; return; } var gap = _gaps[_gaps.Count - 1]; if (gap.IsValid) { if (gap.IsBullish) { _barsCount++; if (Close[0] < gap.BottomValue) { // Only execute trades if we're in the trading window or it's disabled if (Position.MarketPosition == MarketPosition.Flat && (!TradeWindow1IsOn || IsInsideTradeWindow()) && _barsCount <= MaxBarsWait) { PrintLog("Placing bearish market entry order"); _stop = gap.TopValue + TickSize * StopOffsetTicks; PlaceMarketEntryOrder(false); } gap.IsValid = false; _barsCount = 0; } else { if (!CreateBullishGap()) { _gaps[_gaps.Count - 1].BarEnd = CurrentBar; _gaps[_gaps.Count - 1].DateTimeEnd = Time[0]; } else { _barsCount = 0; } } } else if (gap.IsBearish) { _barsCount++; if (Close[0] > gap.TopValue) { // Only execute trades if we're in the trading window or it's disabled if (Position.MarketPosition == MarketPosition.Flat && (!TradeWindow1IsOn || IsInsideTradeWindow()) && _barsCount <= MaxBarsWait) { PrintLog("Placing bullish market entry order"); _stop = gap.BottomValue - TickSize * StopOffsetTicks; PlaceMarketEntryOrder(true); } gap.IsValid = false; _barsCount = 0; } else { if (!CreateBearishGap()) { _gaps[_gaps.Count - 1].BarEnd = CurrentBar; _gaps[_gaps.Count - 1].DateTimeEnd = Time[0]; } else { _barsCount = 0; } } } } else { if (CreateBullishGap()) return; if (CreateBearishGap()) return; } } private bool CreateBullishGap() { if (Low[0] - High[2] >= TickSize * MinGapSizeTicks) { var gap = new FairValueGap() { IsBullish = true, TopValue = Low[0], BottomValue = High[2], IsValid = true, BarStart = CurrentBar - 2, BarEnd = CurrentBar, DateTimeStart = Time[2], DateTimeEnd = Time[0] }; if (_gaps.Count > 0) { _gaps[_gaps.Count - 1].IsValid = false; } _gaps.Add(gap); return true; } return false; } private bool CreateBearishGap() { if (Low[2] - High[0] >= TickSize * MinGapSizeTicks) { var gap = new FairValueGap() { IsBearish = true, TopValue = Low[2], BottomValue = High[0], IsValid = true, BarStart = CurrentBar - 2, BarEnd = CurrentBar, DateTimeStart = Time[2], DateTimeEnd = Time[0] }; if (_gaps.Count > 0) { _gaps[_gaps.Count - 1].IsValid = false; } _gaps.Add(gap); return true; } return false; } private void PlaceMarketEntryOrder(bool isLong) { var posSize = GetPosSize(); if (posSize == 0) return; if (isLong) { _entryOrder = SubmitOrderUnmanaged(0, OrderAction.Buy, OrderType.Market, posSize, 0, 0, "", EnterLongName); } if (!isLong) { _entryOrder = SubmitOrderUnmanaged(0, OrderAction.SellShort, OrderType.Market, posSize, 0, 0, "", EnterShortName); } } private int GetPosSize() { if (MyPositionSizingMode == PositionSizingMode.Manual) { return PosSize1; } if (MyPositionSizingMode == PositionSizingMode.Dynamic) { var stopSize = Math.Abs(_stop - Close[0]); var singleRisk = stopSize * Instrument.MasterInstrument.PointValue; if (singleRisk > MaxTotalRiskUSD) return 0; var maxContracts = Math.Floor(MaxTotalRiskUSD / singleRisk); PosSize1 = (int)maxContracts; if (maxContracts < 1) return 0; return (int)maxContracts; } return 1; } private void PlaceMarketExitOrder(bool isLong) { if (isLong) { _exitMarketOrder = SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Market, Position.Quantity, 0, 0, "", MarketCloseLong); } else if (!isLong) { _exitMarketOrder = SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Market, Position.Quantity, 0, 0, "", MarketCloseShort); } } private void ChangeStopExitOrder() { ChangeOrder(_exitStopOrder, Position.Quantity, 0, _stop); } private void PlaceStopExitOrders(bool isStopLong) { if (isStopLong) { _exitStopOrder = SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.StopMarket, Position.Quantity, 0, _stop, "", ExitLongStopName); } if (!isStopLong) { _exitStopOrder = SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.StopMarket, Position.Quantity, 0, _stop, "", ExitShortStopName); } } private void PlaceLimitProfitExitOrder1(bool isProfitOrderLong) { if (isProfitOrderLong) { _exitProfitOrder1 = SubmitOrderUnmanaged(0, OrderAction.Sell, OrderType.Limit, PosSize1, _profit1, 0, "", ExitLongProfitName1); } if (!isProfitOrderLong) { _exitProfitOrder1 = SubmitOrderUnmanaged(0, OrderAction.BuyToCover, OrderType.Limit, PosSize1, _profit1, 0, "", ExitShortProfitName1); } } private bool _orderIgnored; protected override void OnOrderTrace(DateTime timestamp, string message) { if (message.Contains("Ignored")) { _orderIgnored = true; } } protected override void OnExecutionUpdate(Execution ex, string executionId, double price, int quantity, MarketPosition marketPosition, string orderId, DateTime time) { if (!IsFilled(ex.Order)) return; ManageOrders(ex); } private bool IsFilled(Order order) { var expected = ExpectedRemainedPositionQnt(order.Name); if (expected == Position.Quantity) return true; return false; } private int ExpectedRemainedPositionQnt(string orderName) { if (orderName.Contains(EnterLongName) || orderName.Contains(EnterShortName)) { return PosSize1; } if (orderName == ExitLongStopName || orderName == ExitShortStopName) { return 0; } if (orderName == ExitLongProfitName1 || orderName == ExitShortProfitName1) { return 0; } if (orderName == MarketCloseLong || orderName == MarketCloseShort) { return 0; } return 0; } private void ManageOrders(Execution ex) { var orderName = ex.Order.Name; if (orderName == EnterLongName || orderName == EnterShortName) { var stopSize = Math.Abs(_stop - ex.Order.AverageFillPrice); var profitSize1 = ProfitTarget1Ratio * stopSize; _breakEvenApplied = false; _initialStopSize = stopSize; if (orderName == EnterLongName) { _stop = ex.Order.AverageFillPrice - stopSize; _profit1 = ex.Order.AverageFillPrice + profitSize1; PlaceStopExitOrders(true); PlaceLimitProfitExitOrder1(true); } if (orderName == EnterShortName) { _stop = ex.Order.AverageFillPrice + stopSize; _profit1 = ex.Order.AverageFillPrice - profitSize1; PlaceStopExitOrders(false); PlaceLimitProfitExitOrder1(false); } } if (orderName == ExitLongStopName || orderName == ExitShortStopName) { if (_exitProfitOrder1 != null) CancelOrder(_exitProfitOrder1); } if (orderName == ExitLongProfitName1 || orderName == ExitShortProfitName1) { if (_exitStopOrder != null) CancelOrder(_exitStopOrder); } if (orderName == MarketCloseLong || orderName == MarketCloseShort) { if (_exitStopOrder != null) CancelOrder(_exitStopOrder); if (_exitProfitOrder1 != null) CancelOrder(_exitProfitOrder1); } } protected override void OnRender(ChartControl chartControl, ChartScale chartScale) { try { if (ChartControl == null || Bars == null || Bars.Instrument == null || !IsVisible) return; var dxStrokeStyleProperties = new StrokeStyleProperties { DashStyle = SharpDX.Direct2D1.DashStyle.DashDot, }; var dxStrokeStyle = new StrokeStyle(Core.Globals.D2DFactory, dxStrokeStyleProperties); SharpDX.Direct2D1.Brush zagColor = Brushes.Blue.ToDxBrush(RenderTarget); for (int i = 0; i < _gaps.Count; i++) { var topLineStartX = chartControl.GetXByBarIndex(ChartBars, _gaps[i].BarStart); var topLineStartY = chartScale.GetYByValue(_gaps[i].TopValue); var topLineEndX = chartControl.GetXByBarIndex(ChartBars, _gaps[i].BarEnd); var topLineEndY = chartScale.GetYByValue(_gaps[i].TopValue); var topLineStart = new Vector2(topLineStartX, topLineStartY); var topLineEnd = new Vector2(topLineEndX, topLineEndY); RenderTarget.DrawLine(topLineStart, topLineEnd, zagColor, 4, dxStrokeStyle); var bottomLineStartX = chartControl.GetXByBarIndex(ChartBars, _gaps[i].BarStart); var bottomLineStartY = chartScale.GetYByValue(_gaps[i].BottomValue); var bottomLineEndX = chartControl.GetXByBarIndex(ChartBars, _gaps[i].BarEnd); var bottomLineEndY = chartScale.GetYByValue(_gaps[i].BottomValue); var bottomLineStart = new Vector2(bottomLineStartX, bottomLineStartY); var bottomLineEnd = new Vector2(bottomLineEndX, bottomLineEndY); RenderTarget.DrawLine(bottomLineStart, bottomLineEnd, zagColor, 4, dxStrokeStyle); } } catch (Exception e) { Log($"Render error: {e.Message}", LogLevel.Error); } } private DateTime ToTime(DateTime dateTime) { return new DateTime(2020, 1, 1, dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond); } private void PrintLog(string message) { if (!LogIsOn) return; Print(Time[0] + " " + message); } private void PlotExitLevels() { if (Position.MarketPosition == MarketPosition.Flat || !PlotStops) return; if (_stop > 0) { Draw.Line(this, CurrentBar + "StopLoss", false, 1, _stop, 0, _stop, Brushes.Red, DashStyleHelper.Dash, 2); } } private void ClearOutputWindow() { } public enum PositionSizingMode { Manual, Dynamic } private void StopStrategy() { try { // Force stop any execution if (State != State.SetDefaults && State != State.Terminated) { Log("Strategy stopping due to invalid license.", LogLevel.Error); IsEnabled = false; // Prevent strategy from running State = State.Terminated; } } catch (Exception ex) { Log($"Error in StopStrategy: {ex.Message}", LogLevel.Error); } } } }