I have a c# strategy that trades external symbols. When I run the strategy with "Run backtest" from strategy window, it generate correct orders. However when the strategy is run in SM by using "Run Now", I always miss limit profit taking exits. The screenshot gives an example: the profit taking exit for CHRO and BCTX are not present when running the strategy in SM. Any idea what's wrong here?
Run backtest in strategy window:

Run now in SM:
Run backtest in strategy window:
Run now in SM:
Rename
It's hard to say why you're getting that result. Why does NVNI only have a Limit exit in both cases? The logic would tell, so we'd need to see the code and your Trading Preferences.
Here is the code for the strategy. It's not a normal strategy. It reads a csv file with signals which are generated from other methods and place orders or backtests. The reason why NVNI has only profit taking is that the stop loss from the csv is <0 so in the code i ignored it.
Here is a csv to test:
CODE:
public class CsvStrategy2 : UserStrategyBase { private List<SignalData> _signals = new List<SignalData>(); private Dictionary<object, ExitConditions> _positionExits = new Dictionary<object, ExitConditions>(); private Dictionary<string, BarHistory> _symbolHistories = new Dictionary<string, BarHistory>(); private string _signalFilePath; public class SignalData { public DateTime Date { get; set; } public string Ticker { get; set; } public string Action { get; set; } // BTO or STO public int Shares { get; set; } // renamed from Quantity public string OrderType { get; set; } // LO or MKT public double? LimitPrice { get; set; } public double? ProfitTarget { get; set; } // stored as decimal (1.04 etc) public double? StopAmount { get; set; } // stored as absolute amount public DateTime? TimeExit { get; set; } // specific date public string StrategyName { get; set; } // renamed from Strat } public class ExitConditions { public double? TakeProfitPct { get; set; } public double? StopAmount { get; set; } // Changed from StopLossPct public DateTime? TimeExitDate { get; set; } // Changed from TimeExitDays } public CsvStrategy2() : base() { StartIndex = 0; _signalFilePath = Path.Combine(Environment.CurrentDirectory, "csv_strategy.csv"); LoadSignalsFromCsv(_signalFilePath); } public override void Initialize(BarHistory bars) { PlotStopsAndLimits(3); // Load histories for all unique symbols in signals var uniqueSymbols = _signals.Select(s => s.Ticker).Distinct(); foreach (var symbol in uniqueSymbols) { if (symbol != bars.Symbol) // Don't get history for the primary symbol { var history = GetHistory(bars, symbol); if (history != null) { _symbolHistories[symbol] = history; WriteToDebugLog($"Loaded history for {symbol}"); } else { WriteToDebugLog($"Failed to load history for {symbol}"); } } } } public override void Execute(BarHistory bars, int idx) { var currentDate = bars.DateTimes[idx]; // Process all signals for the current date var todaySignals = _signals.Where(s => s.Date.Date == currentDate.Date).ToList(); foreach (var signal in todaySignals) { // Get the correct BarHistory for this signal BarHistory targetBars = signal.Ticker == bars.Symbol ? bars : _symbolHistories.GetValueOrDefault(signal.Ticker); if (targetBars == null) { WriteToDebugLog($"No history found for {signal.Ticker}"); continue; } // Check for existing position Position openPosition = findOpenPositionsForSymbol(signal.Ticker); if (openPosition == null && (signal.Action == "buy" || signal.Action == "short")) { // Entry logic Backtester.CancelationCode = 1; var transType = signal.Action == "buy" ? TransactionType.Buy : TransactionType.Short; var orderType = signal.OrderType.ToLower() == "limit" ? OrderType.Limit : OrderType.Market; var limitPrice = signal.LimitPrice ?? 0; Transaction currentTrade = PlaceTrade(targetBars, transType, orderType, limitPrice, $"{signal.Action} {signal.OrderType} ({signal.StrategyName})"); currentTrade.Quantity = signal.Shares; currentTrade.Tag = $"{signal.StrategyName} {signal.Ticker} {currentDate.Date} {currentTrade.TransactionID}"; WriteToDebugLog($"Placed trade for {signal.Ticker} with ID {currentTrade.TransactionID}"); // Store exit conditions if (currentTrade != null) { // Convert profit target from decimal to percentage double? takeProfitPct = signal.ProfitTarget.HasValue ? (signal.ProfitTarget.Value - 1) * 100 : null; _positionExits[currentTrade.Tag] = new ExitConditions { TakeProfitPct = takeProfitPct, StopAmount = signal.StopAmount, TimeExitDate = signal.TimeExit }; WriteToDebugLog($"Stored exit conditions for {signal.Ticker} with ID {currentTrade.TransactionID}, exit conditions: {takeProfitPct}, {signal.StopAmount}, {signal.TimeExit}"); } } } // Check exit conditions for all open positions foreach (var position in OpenPositionsAllSymbols) { if (_positionExits.TryGetValue(position.Tag, out var exitConditions)) { bool isLong = position.PositionType == PositionType.Long; var entryPrice = position.EntryPrice; WriteToDebugLog($"Entry price for {position.Symbol}: {entryPrice}"); // Take profit exit (percentage-based) if (exitConditions.TakeProfitPct.HasValue) { var takeProfitPrice = entryPrice * (1 + (exitConditions.TakeProfitPct.Value / 100.0) * (isLong ? 1 : -1)); Backtester.CancelationCode = 1; ClosePosition(position, OrderType.Limit, takeProfitPrice, "Take Profit Exit"); } // Stop loss exit (absolute amount-based) if (exitConditions.StopAmount.HasValue) { var stopPrice = isLong ? entryPrice - exitConditions.StopAmount.Value : entryPrice + exitConditions.StopAmount.Value; if (stopPrice > 0) { Backtester.CancelationCode = 1; ClosePosition(position, OrderType.Stop, stopPrice, "Stop Loss Exit"); } } // Time-based exit (specific date) if (exitConditions.TimeExitDate.HasValue && currentDate.Date >= exitConditions.TimeExitDate.Value.Date) { Backtester.CancelationCode = 1; ClosePosition(position, OrderType.Market, 0, "Time Exit (Date)"); _positionExits.Remove(position.Tag); } } } } public override void AssignAutoStopTargetPrices(Transaction t, double basisPrice, double executionPrice) { // Take profit, get the percentage from the signal double? takeProfitPct = _positionExits[t.Tag].TakeProfitPct; if (takeProfitPct.HasValue) { double price; if (t.PositionType == PositionType.Short) { price = executionPrice * (1.0 - takeProfitPct.Value / 100.0); } else { price = executionPrice * (1.0 + takeProfitPct.Value / 100.0); } t.AssignAutoProfitTargetPrice(price); } // Stop loss double? stopLossPct = _positionExits[t.Tag].StopAmount; if (stopLossPct.HasValue) { double price; if (t.PositionType == PositionType.Short) { price = executionPrice * (1.0 - stopLossPct.Value / 100.0); } else { price = executionPrice * (1.0 + stopLossPct.Value / 100.0); } t.AssignAutoStopLossPrice(price); } } private void LoadSignalsFromCsv(string filePath) { if (!File.Exists(filePath)) { WriteToDebugLog($"Signal file not found: {filePath}"); return; } using (var reader = new StreamReader(filePath)) { string line; bool isHeader = true; while ((line = reader.ReadLine()) != null) { if (isHeader) { isHeader = false; continue; } var parts = line.Split(','); try { // Debug log to see the actual values WriteToDebugLog($"Parsing line with {parts.Length} columns: {line}"); // Column indices based on CSV format: // 0: Date // 1: Ticker // 2: Action (BTO/STO) // 3: Shares // 4: Type (LO/MKT) // 5: Limit or Stop price // 6: Profit Target // 7: Stop Loss // 8: Time Exit // 9: Strategy var action = parts[2].Trim().ToUpper(); var orderType = parts[4].Trim().ToUpper(); // Parse profit target (now in column 6) double? profitTarget = string.IsNullOrEmpty(parts[6].Trim()) ? null : double.Parse(parts[6].Trim()); // Parse stop loss (now in column 7) double? stopAmount = null; if (!string.IsNullOrEmpty(parts[7].Trim())) { var stopStr = parts[7].Trim(); if (stopStr.StartsWith("(") && stopStr.EndsWith(")")) { stopStr = stopStr.Trim('(', ')'); stopAmount = double.Parse(stopStr); } } // Parse time exit (now in column 8) DateTime? timeExit = null; if (!string.IsNullOrEmpty(parts[8].Trim())) { timeExit = DateTime.Parse(parts[8].Trim()); } var signal = new SignalData { Date = DateTime.Parse(parts[0].Trim()), Ticker = parts[1].Trim(), Action = action == "BTO" ? "buy" : "short", Shares = int.Parse(parts[3].Trim()), OrderType = orderType == "LO" ? "limit" : "market", LimitPrice = string.IsNullOrEmpty(parts[5].Trim()) ? null : double.Parse(parts[5].Trim()), ProfitTarget = profitTarget, StopAmount = stopAmount, TimeExit = timeExit, StrategyName = parts[9].Trim() }; _signals.Add(signal); // Debug log for successful parsing WriteToDebugLog($"Successfully parsed signal for {signal.Ticker} with action {action}"); } catch (Exception ex) { WriteToDebugLog($"Error parsing line: {line}"); WriteToDebugLog($"Detailed error: {ex.Message}"); WriteToDebugLog($"Stack trace: {ex.StackTrace}"); } } } WriteToDebugLog($"Loaded {_signals.Count} signals from {filePath}"); } public Position findOpenPositionsForSymbol(string symbol) { return OpenPositionsAllSymbols.FirstOrDefault(p => p.Bars.Symbol == symbol); } }
Here is a csv to test:
CODE:
Date,Ticker,Action,Shares,Type,Limit or Stop,Profit Target,Stop Loss,Time Exit,Strategy,Notes 2025-02-04,CHRO,BTO,331,LO,1.60,1.04,(1.33),2025-02-12,Strat_6,mr entry 2025-02-04,BCTX,BTO,847,LO,3.81,1.04,(3.08),2025-02-12,Strat_6,mr entry 2025-02-04,CAN,BTO,9966,LO,1.69,1.04,(0.55),2025-02-12,Strat_6,mr entry 2025-02-04,VITL,BTO,775,LO,35.35,1.04,(6.00),2025-02-12,Strat_6,mr entry 2025-02-04,IDAI,BTO,4608,LO,2.93,1.04,(1.18),2025-02-12,Strat_6,mr entry 2025-02-04,INTZ,BTO,2215,LO,1.48,1.04,(0.53),2025-02-12,Strat_6,mr entry 2025-02-04,SNTI,BTO,585,LO,3.59,1.04,(0.88),2025-02-12,Strat_6,mr entry 2025-02-05,SMX,BTO,2800,LO,2.89,1.04,(1.95),2025-02-12,Strat_6,mr entry 2025-02-05,FCUV,BTO,1428,LO,4.63,1.04,(3.58),2025-02-12,Strat_6,mr entry 2025-02-05,NUKK,BTO,705,LO,19.36,1.04,(7.75),2025-02-12,Strat_6,mr entry 2025-02-05,SPCB,BTO,473,LO,9.02,1.04,(5.15),2025-02-12,Strat_6,mr entry 2025-02-05,VITL,BTO,779,LO,35.05,1.04,(5.95),2025-02-12,Strat_6,mr entry 2025-02-05,CAN,BTO,8652,LO,1.68,1.04,(0.53),2025-02-12,Strat_6,mr entry 2025-02-06,NVNI,BTO,1107,LO,4.85,1.04,(4.93),2025-02-14,Strat_6,mr entry 2025-02-06,CYCN,BTO,2479,LO,3.95,1.04,(2.20),2025-02-14,Strat_6,mr entry 2025-02-06,ATCH,BTO,2567,LO,3.34,1.04,(2.13),2025-02-14,Strat_6,mr entry 2025-02-06,BCTX,BTO,1604,LO,3.77,1.04,(3.40),2025-02-14,Strat_6,mr entry 2025-02-06,SPCB,BTO,1355,LO,8.71,1.04,(4.03),2025-02-14,Strat_6,mr entry 2025-02-06,SOPA,BTO,4742,LO,1.93,1.04,(1.15),2025-02-14,Strat_6,mr entry 2025-02-07,NOTV,BTO,2709,LO,3.26,1.04,(1.08),2025-02-17,Strat_6,mr entry 2025-02-07,KAPA,BTO,2217,LO,1.28,1.04,(0.40),2025-02-17,Strat_6,mr entry 2025-02-07,SPCB,BTO,1458,LO,8.15,1.04,(3.75),2025-02-17,Strat_6,mr entry 2025-02-07,SMTC,BTO,336,LO,50.69,1.04,(16.28),2025-02-17,Strat_6,mr entry 2025-02-07,BNRG,BTO,1841,LO,1.03,1.04,(0.50),2025-02-17,Strat_6,mr entry 2025-02-07,ABAT,BTO,7316,LO,1.22,1.04,(0.43),2025-02-17,Strat_6,mr entry 2025-02-07,LUNR,BTO,1042,LO,17.11,1.04,(5.25),2025-02-17,Strat_6,mr entry 2025-02-07,FUBO,BTO,6250,LO,3.67,1.04,(0.88),2025-02-17,Strat_6,mr entry
This one's a mystery for now.
Is there a reason you need to run it in the S. Monitor?
Is there a reason you need to run it in the S. Monitor?
Were you able to reproduce it? I think the logic here is no different than other strategies with stop loss and profit target. I want to be able to auto place the orders at given time each day, there’s another process to to update the csv before that every day.
Yes, I saw it. But that strategy is a lot different than most strategies for which WealthLab manages the BarHistories of participants. Nonetheless, I don't see why it doesn't work and reached a dead end. Glitch said he'd take a look.
Glitch found the bug. Fixed for Build 111.
Thanks for the report!
Thanks for the report!
Great! Thanks
Your Response
Post
Edit Post
Login is required