using Managing.Application.Abstractions; using Managing.Application.Abstractions.Services; using Managing.Application.Trading; using Managing.Application.Trading.Commands; using Managing.Core.FixedSizedQueue; using Managing.Domain.Accounts; using Managing.Domain.Bots; using Managing.Domain.Candles; using Managing.Domain.MoneyManagements; using Managing.Domain.Scenarios; using Managing.Domain.Shared.Helpers; using Managing.Domain.Strategies; using Managing.Domain.Strategies.Base; using Managing.Domain.Trades; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using static Managing.Common.Enums; namespace Managing.Application.Bots; public class TradingBot : Bot, ITradingBot { public readonly ILogger Logger; public readonly IExchangeService ExchangeService; public readonly IMessengerService MessengerService; public readonly IAccountService AccountService; private readonly ITradingService TradingService; private readonly IBotService BotService; public Account Account { get; set; } public HashSet Strategies { get; set; } public FixedSizeQueue OptimizedCandles { get; set; } public HashSet Candles { get; set; } public HashSet Signals { get; set; } public List Positions { get; set; } public Ticker Ticker { get; set; } public string ScenarioName { get; set; } public string AccountName { get; set; } public MoneyManagement MoneyManagement { get; set; } public Timeframe Timeframe { get; set; } public bool IsForBacktest { get; set; } public DateTime PreloadSince { get; set; } public bool IsForWatchingOnly { get; set; } public bool FlipPosition { get; set; } public int PreloadedCandlesCount { get; set; } public BotType BotType { get; set; } public decimal Fee { get; set; } public Scenario Scenario { get; set; } public Dictionary WalletBalances { get; set; } public Dictionary StrategiesValues { get; set; } public TradingBot( string accountName, MoneyManagement moneyManagement, string name, Ticker ticker, string scenarioName, IExchangeService exchangeService, ILogger logger, ITradingService tradingService, Timeframe timeframe, IAccountService accountService, IMessengerService messengerService, IBotService botService, bool isForBacktest = false, bool isForWatchingOnly = false, bool flipPosition = false) : base(name) { ExchangeService = exchangeService; AccountService = accountService; MessengerService = messengerService; TradingService = tradingService; BotService = botService; IsForWatchingOnly = isForWatchingOnly; FlipPosition = flipPosition; AccountName = accountName; MoneyManagement = moneyManagement; Ticker = ticker; ScenarioName = scenarioName; Timeframe = timeframe; IsForBacktest = isForBacktest; Logger = logger; Strategies = new HashSet(); Signals = new HashSet(); OptimizedCandles = new FixedSizeQueue(600); Candles = new HashSet(); Positions = new List(); WalletBalances = new Dictionary(); StrategiesValues = new Dictionary(); if (!isForBacktest) { Interval = CandleExtensions.GetIntervalFromTimeframe(timeframe); PreloadSince = CandleExtensions.GetBotPreloadSinceFromTimeframe(timeframe); } } public override async void Start() { base.Start(); // Load account synchronously await LoadAccount(); if (!IsForBacktest) { LoadScenario(ScenarioName); await PreloadCandles(); await CancelAllOrders(); try { // await MessengerService.SendMessage( // $"Hey everyone! I'm about to start {Name}. 🚀\n" + // $"I'll post an update here each time a signal is triggered by the following strategies: {string.Join(", ", Strategies.Select(s => s.Name))}." // ); } catch (Exception ex) { Logger.LogError(ex, ex.Message); } await InitWorker(Run); } Fee = TradingService.GetFee(Account, IsForBacktest); } public async Task LoadAccount() { var account = await AccountService.GetAccount(AccountName, false, true); if (account == null) { Logger.LogWarning($"No account found for this {AccountName}"); Stop(); } else { Account = account; } } public void LoadScenario(string scenarioName) { var scenario = TradingService.GetScenarioByName(scenarioName); if (scenario == null) { Logger.LogWarning("No scenario found for this scenario name"); Stop(); } else { Scenario = scenario; LoadStrategies(ScenarioHelpers.GetStrategiesFromScenario(scenario)); } } public void LoadStrategies(IEnumerable strategies) { foreach (var strategy in strategies) { Strategies.Add(strategy); } } public async Task Run() { if (!IsForBacktest) { Logger.LogInformation($"____________________{Name}____________________"); Logger.LogInformation( $"Time : {DateTime.Now} - Server time {DateTime.Now.ToUniversalTime()} - Last candle : {OptimizedCandles.Last().Date} - Bot : {Name} - Type {BotType} - Ticker : {Ticker}"); } var previousLastCandle = OptimizedCandles.LastOrDefault(); if (!IsForBacktest) await UpdateCandles(); var currentLastCandle = OptimizedCandles.LastOrDefault(); if (currentLastCandle != previousLastCandle || IsForBacktest) await UpdateSignals(OptimizedCandles); else Logger.LogInformation($"No need to update signals for {Ticker}"); if (!IsForWatchingOnly) await ManagePositions(); if (!IsForBacktest) { SaveBackup(); UpdateStrategiesValues(); } await UpdateWalletBalances(); if (OptimizedCandles.Count % 100 == 0) // Log every 10th execution { Logger.LogInformation($"Candle date : {OptimizedCandles.Last().Date:u}"); Logger.LogInformation($"Signals : {Signals.Count}"); Logger.LogInformation($"ExecutionCount : {ExecutionCount}"); Logger.LogInformation($"Positions : {Positions.Count}"); Logger.LogInformation("__________________________________________________"); } } public void UpdateStrategiesValues() { foreach (var strategy in Strategies) { StrategiesValues[strategy.Type] = ((Strategy)strategy).GetStrategyValues(); } } private async Task PreloadCandles() { if (OptimizedCandles.Any()) return; var candles = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, PreloadSince, Timeframe); foreach (var candle in candles.Where(c => c.Date < DateTime.Now.ToUniversalTime())) { if (!OptimizedCandles.Any(c => c.Date == candle.Date)) { OptimizedCandles.Enqueue(candle); Candles.Add(candle); await UpdateSignals(OptimizedCandles); } } PreloadedCandlesCount = OptimizedCandles.Count(); } private async Task UpdateSignals(FixedSizeQueue candles) { var signal = TradingBox.GetSignal(candles.ToHashSet(), Strategies, Signals, Scenario.LoopbackPeriod); if (signal == null) return; signal.User = Account.User; await AddSignal(signal); } private async Task AddSignal(Signal signal) { Signals.Add(signal); // if (!IsForBacktest) // TradingService.InsertSignal(signal); if (IsForWatchingOnly || (ExecutionCount < 1 && !IsForBacktest)) signal.Status = SignalStatus.Expired; var signalText = $"{ScenarioName} trigger a signal. Signal told you " + $"to {signal.Direction} {Ticker} on {Timeframe}. The confidence in this signal is {signal.Confidence}. Identifier : {signal.Identifier}"; Logger.LogInformation(signalText); if (IsForWatchingOnly && !IsForBacktest && ExecutionCount > 0) { await MessengerService.SendSignal(signalText, Account.Exchange, Ticker, signal.Direction, Timeframe); } } protected async Task UpdateCandles() { if (OptimizedCandles.Count == 0 || ExecutionCount == 0) return; var lastCandle = OptimizedCandles.Last(); var newCandle = await ExchangeService.GetCandlesInflux(Account.Exchange, Ticker, lastCandle.Date, Timeframe); foreach (var candle in newCandle.Where(c => c.Date < DateTime.Now.ToUniversalTime())) { OptimizedCandles.Enqueue(candle); Candles.Add(candle); } } private async Task ManagePositions() { if (!IsForBacktest && ExecutionCount < 1) return; // Update position foreach (var signal in Signals.Where(s => s.Status == SignalStatus.PositionOpen)) { var positionForSignal = Positions.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier); await UpdatePosition(signal, positionForSignal); } // Open position for signal waiting for a position open foreach (var signal in Signals.Where(s => s.Status == SignalStatus.WaitingForPosition)) { Task.Run(() => OpenPosition(signal)).GetAwaiter().GetResult(); } } private async Task UpdateWalletBalances() { var lastCandle = OptimizedCandles.LastOrDefault(); if (lastCandle == null) return; var date = lastCandle.Date; if (WalletBalances.Count == 0) { WalletBalances[date] = await ExchangeService.GetBalance(Account, IsForBacktest); return; } if (!WalletBalances.ContainsKey(date)) { var previousBalance = WalletBalances.First().Value; WalletBalances[date] = previousBalance + GetProfitAndLoss(); } } private async Task UpdatePosition(Signal signal, Position positionForSignal) { try { Logger.LogInformation($"Updating position {positionForSignal.SignalIdentifier}"); var position = IsForBacktest ? positionForSignal : TradingService.GetPositionByIdentifier(positionForSignal.Identifier); var positionsExchange = IsForBacktest ? new List{position} : await TradingService.GetBrokerPositions(Account); if (!IsForBacktest) { position = positionsExchange.FirstOrDefault(p => p.Ticker == Ticker); } if (position.Status == PositionStatus.New) { var orders = await ExchangeService.GetOpenOrders(Account, Ticker); if (orders.Any()) { await LogInformation($"Cannot update Position. Position is still waiting for opening. There is {orders.Count()} open orders."); } else { await LogWarning($"Cannot update Position. No position on exchange and no orders. Position {signal.Identifier} might be closed already."); await HandleClosedPosition(positionForSignal); } } else if (position.Status == (PositionStatus.Finished | PositionStatus.Flipped)) { await HandleClosedPosition(positionForSignal); } else if (position.Status == (PositionStatus.Filled | PositionStatus.PartiallyFilled)) { // For backtesting or force close if not executed on exchange : // check if position is still open // Check status, if still open update the status of the position // Position might be partially filled, meaning that TPSL havent been sended yet // But the position might already been closed by the exchange so we have to check should be closed var lastCandle = IsForBacktest ? OptimizedCandles.Last() : ExchangeService.GetCandle(Account, Ticker, DateTime.UtcNow); if (positionForSignal.OriginDirection == TradeDirection.Long) { if (positionForSignal.StopLoss.Price >= lastCandle.Low) { await LogInformation( $"Closing position - SL {positionForSignal.StopLoss.Price} >= Price {lastCandle.Low}"); await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, positionForSignal.StopLoss.Price, true); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); } else if (positionForSignal.TakeProfit1.Price <= lastCandle.High && positionForSignal.TakeProfit1.Status != TradeStatus.Filled) { await LogInformation( $"Closing position - TP1 {positionForSignal.TakeProfit1.Price} <= Price {lastCandle.High}"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); } else if (positionForSignal.TakeProfit2?.Price <= lastCandle.High) { await LogInformation( $"Closing position - TP2 {positionForSignal.TakeProfit2.Price} <= Price {lastCandle.High}"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, positionForSignal.TakeProfit2.Price, true); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); } else { Logger.LogInformation( $"Position {signal.Identifier} don't need to be update. Position still opened"); } } if (positionForSignal.OriginDirection == TradeDirection.Short) { if (positionForSignal.StopLoss.Price <= lastCandle.High) { await LogInformation( $"Closing position - SL {positionForSignal.StopLoss.Price} <= Price {lastCandle.High}"); await CloseTrade(signal, positionForSignal, positionForSignal.StopLoss, positionForSignal.StopLoss.Price, true); positionForSignal.StopLoss.SetStatus(TradeStatus.Filled); } else if (positionForSignal.TakeProfit1.Price >= lastCandle.Low && positionForSignal.TakeProfit1.Status != TradeStatus.Filled) { await LogInformation( $"Closing position - TP1 {positionForSignal.TakeProfit1.Price} >= Price {lastCandle.Low}"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit1, positionForSignal.TakeProfit1.Price, positionForSignal.TakeProfit2 == null); positionForSignal.TakeProfit1.SetStatus(TradeStatus.Filled); } else if (positionForSignal.TakeProfit2?.Price >= lastCandle.Low) { await LogInformation( $"Closing position - TP2 {positionForSignal.TakeProfit2.Price} >= Price {lastCandle.Low}"); await CloseTrade(signal, positionForSignal, positionForSignal.TakeProfit2, positionForSignal.TakeProfit2.Price, true); positionForSignal.TakeProfit2.SetStatus(TradeStatus.Filled); } else { Logger.LogInformation( $"Position {signal.Identifier} don't need to be update. Position still opened"); } } } else if (position.Status == (PositionStatus.Rejected | PositionStatus.Canceled)) { await LogWarning($"Open position trade is rejected for signal {signal.Identifier}"); // if position is not open // Re-open the trade for the signal only if signal still up if (signal.Status == SignalStatus.PositionOpen) { Logger.LogInformation($"Try to re-open position"); await OpenPosition(signal); } } } catch (Exception ex) { Logger.LogError(ex, ex.Message); SentrySdk.CaptureException(ex); return; } } private async Task OpenPosition(Signal signal) { // Check if a position is already open Logger.LogInformation($"Opening position for {signal.Identifier}"); var openedPosition = Positions.FirstOrDefault(p => p.Status == PositionStatus.Filled && p.SignalIdentifier != signal.Identifier); var lastPrice = IsForBacktest ? OptimizedCandles.Last().Close : ExchangeService.GetPrice(Account, Ticker, DateTime.UtcNow); // If position open if (openedPosition != null) { var previousSignal = Signals.First(s => s.Identifier == openedPosition.SignalIdentifier); // Check if signal is the opposite side => flip the position if (openedPosition.OriginDirection == signal.Direction) { // An operation is already open for the same direction await LogInformation( $"Signal {signal.Identifier} try to open a position but {previousSignal.Identifier} is already open for the same direction"); SetSignalStatus(signal.Identifier, SignalStatus.Expired); } else { // An operation is already open for the opposite direction // ==> Flip the position if (FlipPosition) { await LogInformation("Try to flip the position because of an opposite direction signal"); await CloseTrade(previousSignal, openedPosition, openedPosition.Open, lastPrice, true); await SetPositionStatus(previousSignal.Identifier, PositionStatus.Flipped); await OpenPosition(signal); await LogInformation( $"Position {previousSignal.Identifier} flipped by {signal.Identifier} at {lastPrice}$"); } else { await LogWarning( $"A position is already open for signal {previousSignal.Identifier}. Position flipping is currently not enable, the position will not be flipped."); SetSignalStatus(signal.Identifier, SignalStatus.Expired); } } } else { if (!CanOpenPosition(signal)) { await LogInformation( "Tried to open position but last position was a loss. Wait for an opposition direction side or wait x candles to open a new position"); SetSignalStatus(signal.Identifier, SignalStatus.Expired); return; } await LogInformation( $"Open position - Date: {signal.Date:T} - SignalIdentifier : {signal.Identifier} - Strategie : {signal.StrategyType}"); try { var command = new OpenPositionRequest( AccountName, MoneyManagement, signal.Direction, Ticker, IsForBacktest ? PositionInitiator.PaperTrading : PositionInitiator.Bot, signal.Date, IsForBacktest, lastPrice, balance: WalletBalances.LastOrDefault().Value, fee: Fee); var position = await new OpenPositionCommandHandler(ExchangeService, AccountService, TradingService) .Handle(command); if (position != null) { position.SignalIdentifier = signal.Identifier; Positions.Add(position); if (position.Open.Status != TradeStatus.Cancelled) { SetSignalStatus(signal.Identifier, SignalStatus.PositionOpen); if (!IsForBacktest) { await MessengerService.SendPosition(position); } Logger.LogInformation($"Position requested"); } else { await SetPositionStatus(signal.Identifier, PositionStatus.Rejected); SetSignalStatus(signal.Identifier, SignalStatus.Expired); } } } catch (Exception ex) { // Keep signal open for debug purpose //SetSignalStatus(signal.Identifier, SignalStatus.Expired); SetSignalStatus(signal.Identifier, SignalStatus.Expired); await LogWarning($"Cannot open trade : {ex.Message}, stackTrace : {ex.StackTrace}"); } } } private bool CanOpenPosition(Signal signal) { if (Positions.Count == 0) return true; var lastPosition = Positions.LastOrDefault(p => p.IsFinished() && p.SignalIdentifier != signal.Identifier && p.ProfitAndLoss.Realized < 0 && p.OriginDirection == signal.Direction); if (lastPosition == null) return true; var tenCandleAgo = OptimizedCandles.TakeLast(10).First(); var positionSignal = Signals.FirstOrDefault(s => s.Identifier == lastPosition.SignalIdentifier); return positionSignal.Date < tenCandleAgo.Date; } private async Task CloseTrade(Signal signal, Position position, Trade tradeToClose, decimal lastPrice, bool tradeClosingPosition = false) { if (position.TakeProfit2 != null && position.TakeProfit1.Status == TradeStatus.Filled && tradeToClose.TradeType == TradeType.StopMarket) { // If trade is the 2nd Take profit tradeToClose.Quantity = position.TakeProfit2.Quantity; } await LogInformation( $"Trying to close trade {Ticker} at {lastPrice} - Type : {tradeToClose.TradeType} - Quantity : {tradeToClose.Quantity} " + $"- Closing Position : {tradeClosingPosition}"); // Get status of position before closing it. The position might be already close by the exchange if (!IsForBacktest && await ExchangeService.GetQuantityInPosition(Account, Ticker) == 0) { Logger.LogInformation($"Trade already close on exchange"); await HandleClosedPosition(position); } else { var command = new ClosePositionCommand(position, lastPrice); try { var closedPosition = (new ClosePositionCommandHandler(ExchangeService, AccountService, TradingService) .Handle(command)).Result; if (closedPosition.Status == (PositionStatus.Finished | PositionStatus.Flipped)) { if (tradeClosingPosition) { await SetPositionStatus(signal.Identifier, PositionStatus.Finished); } await HandleClosedPosition(closedPosition); } else { throw new Exception($"Wrong position status : {closedPosition.Status}"); } } catch (Exception ex) { await LogWarning($"Position {signal.Identifier} not closed : {ex.Message}"); if (position.Status == (PositionStatus.Canceled | PositionStatus.Rejected)) { // Trade close on exchange => Should close trade manually await SetPositionStatus(signal.Identifier, PositionStatus.Finished); } } } } private async Task HandleClosedPosition(Position position) { if (Positions.Any(p => p.Identifier == position.Identifier)) { var previousPosition = Positions.First(p => p.Identifier == position.Identifier); var positionIndex = Positions.IndexOf(previousPosition); position.SignalIdentifier = previousPosition.SignalIdentifier; Positions[positionIndex] = position; SetSignalStatus(position.SignalIdentifier, SignalStatus.Expired); Logger.LogInformation( $"Position {position.SignalIdentifier} type correctly close. Pnl on position : {position.ProfitAndLoss.Realized}"); } else { await LogWarning("Weird things happen - Trying to update position status, but no position found"); } if (!IsForBacktest) { await MessengerService.SendClosingPosition(position); } await CancelAllOrders(); } private async Task CancelAllOrders() { if (!IsForBacktest && !IsForWatchingOnly) { try { var openOrders = await ExchangeService.GetOpenOrders(Account, Ticker); if (openOrders.Any()) { var openPositions = (await ExchangeService.GetBrokerPositions(Account)) .Where(p => p.Ticker == Ticker); var cancelClose = openPositions.Any(); if (cancelClose) { Logger.LogInformation($"Position still open, cancel close orders&"); } else { Logger.LogInformation($"Canceling all orders for {Ticker}"); await ExchangeService.CancelOrder(Account, Ticker); var closePendingOrderStatus = await ExchangeService.CancelOrder(Account, Ticker); Logger.LogInformation($"Closing all {Ticker} orders status : {closePendingOrderStatus}"); } } else { Logger.LogInformation($"No need to cancel orders for {Ticker}"); } } catch (Exception ex) { Logger.LogError(ex, "Error during cancelOrders"); } } } private async Task SetPositionStatus(string signalIdentifier, PositionStatus positionStatus) { await LogInformation($"Position {signalIdentifier} is now {positionStatus}"); Positions.First(p => p.SignalIdentifier == signalIdentifier).Status = positionStatus; SetSignalStatus(signalIdentifier, SignalStatus.Expired); } private void SetSignalStatus(string signalIdentifier, SignalStatus signalStatus) { if (Signals.Any(s => s.Identifier == signalIdentifier)) { Signals.First(s => s.Identifier == signalIdentifier).Status = signalStatus; Logger.LogInformation($"Signal {signalIdentifier} is now {signalStatus}"); } } public int GetWinRate() { var succeededPositions = Positions.Where(p => p.IsFinished()).Count(p => p.ProfitAndLoss?.Realized > 0); var total = Positions.Where(p => p.IsFinished()).Count(); if (total == 0) return 0; return (succeededPositions * 100) / total; } public decimal GetProfitAndLoss() { var pnl = Positions.Where(p => p.ProfitAndLoss != null).Sum(p => p.ProfitAndLoss.Realized); return pnl - GetTotalFees(); } /// /// Calculates the total fees paid by the trading bot for each position. /// /// Returns the total fees paid as a decimal value. public decimal GetTotalFees() { decimal fees = 0; foreach (var position in Positions.Where(p => p.Open.Fee > 0)) { fees += position.Open.Fee; fees += position.StopLoss.Status == TradeStatus.Filled ? position.StopLoss.Fee : 0; fees += position.TakeProfit1.Status == TradeStatus.Filled ? position.TakeProfit1.Fee : 0; if (position.IsFinished() && position.StopLoss.Status != TradeStatus.Filled && position.TakeProfit1.Status != TradeStatus.Filled) fees += position.Open.Fee; if (position.TakeProfit2 != null) fees += position.TakeProfit2.Fee; } return fees; } public async Task ToggleIsForWatchOnly() { IsForWatchingOnly = (!IsForWatchingOnly); await LogInformation($"Watch only toggle for bot : {Name} - Watch only : {IsForWatchingOnly}"); } private async Task LogInformation(string message) { Logger.LogInformation(message); await SendTradeMessage(message); } private async Task LogWarning(string message) { Logger.LogWarning(message); SentrySdk.CaptureException(new Exception(message)); await SendTradeMessage(message, true); } private async Task SendTradeMessage(string message, bool isBadBehavior = false) { if (!IsForBacktest) { await MessengerService.SendTradeMessage(message, isBadBehavior); } } public override void SaveBackup() { var data = new TradingBotBackup { Name = Name, BotType = BotType, Signals = Signals, Positions = Positions, Timeframe = Timeframe, Ticker = Ticker, ScenarioName = ScenarioName, AccountName = AccountName, IsForWatchingOnly = IsForWatchingOnly, WalletBalances = WalletBalances, MoneyManagement = MoneyManagement }; BotService.SaveOrUpdateBotBackup(Name, BotType, JsonConvert.SerializeObject(data)); } public override void LoadBackup(BotBackup backup) { var data = JsonConvert.DeserializeObject(backup.Data); Signals = data.Signals; Positions = data.Positions; WalletBalances = data.WalletBalances; // MoneyManagement = data.MoneyManagement; => loaded from database Timeframe = data.Timeframe; Ticker = data.Ticker; ScenarioName = data.ScenarioName; AccountName = data.AccountName; IsForWatchingOnly = data.IsForWatchingOnly; } /// /// Manually opens a position using the bot's settings and a generated signal. /// Relies on the bot's MoneyManagement for Stop Loss and Take Profit placement. /// /// The direction of the trade (Long/Short). /// The created Position object. /// Throws if no candles are available or position opening fails. public async Task OpenPositionManually(TradeDirection direction) { var lastCandle = OptimizedCandles.LastOrDefault(); if (lastCandle == null) { throw new Exception("No candles available to open position"); } // Create a fake signal for manual position opening var signal = new Signal(Ticker, direction, Confidence.Low, lastCandle, lastCandle.Date, TradingExchanges.GmxV2, StrategyType.Stc, SignalType.Signal); signal.Status = SignalStatus.WaitingForPosition; // Ensure status is correct signal.User = Account.User; // Assign user // Add the signal to our collection await AddSignal(signal); // Open the position using the generated signal (SL/TP handled by MoneyManagement) await OpenPosition(signal); // Get the opened position var position = Positions.FirstOrDefault(p => p.SignalIdentifier == signal.Identifier); if (position == null) { // Clean up the signal if position creation failed SetSignalStatus(signal.Identifier, SignalStatus.Expired); throw new Exception("Failed to open position"); } // Removed manual setting of SL/TP, as MoneyManagement should handle it // position.StopLoss.Price = stopLossPrice; // position.TakeProfit1.Price = takeProfitPrice; Logger.LogInformation($"Manually opened position {position.Identifier} for signal {signal.Identifier}"); return position; } } public class TradingBotBackup { public string Name { get; set; } public BotType BotType { get; set; } public HashSet Signals { get; set; } public List Positions { get; set; } public Timeframe Timeframe { get; set; } public Ticker Ticker { get; set; } public string ScenarioName { get; set; } public string AccountName { get; set; } public bool IsForWatchingOnly { get; set; } public Dictionary WalletBalances { get; set; } public MoneyManagement MoneyManagement { get; set; } }