diff --git a/src/Managing.Application.Abstractions/Services/IExchangeService.cs b/src/Managing.Application.Abstractions/Services/IExchangeService.cs index e5d9c9ed..93afadc8 100644 --- a/src/Managing.Application.Abstractions/Services/IExchangeService.cs +++ b/src/Managing.Application.Abstractions/Services/IExchangeService.cs @@ -26,6 +26,7 @@ public interface IExchangeService Task GetBalance(Account account, bool isForPaperTrading = false); Task> GetBalances(Account account, bool isForPaperTrading = false); Task GetPrice(Account account, Ticker ticker, DateTime date); + Task GetCurrentPrice(Account account, Ticker ticker); Task GetTrade(Account account, string order, Ticker ticker); Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval, diff --git a/src/Managing.Application.Tests/TradingBaseTests.cs b/src/Managing.Application.Tests/TradingBaseTests.cs index 7e50e22c..e718c824 100644 --- a/src/Managing.Application.Tests/TradingBaseTests.cs +++ b/src/Managing.Application.Tests/TradingBaseTests.cs @@ -91,7 +91,8 @@ namespace Managing.Application.Tests { var settings = new Web3ProxySettings { BaseUrl = baseUrl }; var options = Options.Create(settings); - return new Web3ProxyService(options); + var logger = new Mock>().Object; + return new Web3ProxyService(options, logger); } } } \ No newline at end of file diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index b7bb7f06..355676db 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -615,7 +615,7 @@ public class TradingBotBase : ITradingBot await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => { - currentPrice = await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + currentPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); var riskResult = default(SynthRiskResult); await ServiceScopeHelpers.WithScopedService(_scopeFactory, async tradingService => @@ -664,7 +664,7 @@ public class TradingBotBase : ITradingBot { return Config.IsForBacktest ? LastCandle?.Close ?? 0 - : await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + : await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); if (openedPosition != null) @@ -825,7 +825,7 @@ public class TradingBotBase : ITradingBot { currentPrice = Config.IsForBacktest ? LastCandle?.Close ?? 0 - : await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + : await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); @@ -1157,7 +1157,7 @@ public class TradingBotBase : ITradingBot await ServiceScopeHelpers.WithScopedService(_scopeFactory, async exchangeService => { - closingPrice = await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + closingPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker); }); bool isManualCloseProfitable = position.OriginDirection == TradeDirection.Long @@ -1541,7 +1541,7 @@ public class TradingBotBase : ITradingBot await ServiceScopeHelpers.WithScopedServices(_scopeFactory, async (tradingService, exchangeService) => { - var currentPrice = await exchangeService.GetPrice(Account, Config.Ticker, DateTime.UtcNow); + var currentPrice = await exchangeService.GetCurrentPrice(Account, Config.Ticker); var signalValidationResult = await tradingService.ValidateSynthSignalAsync( signal, diff --git a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs index ac1a7f94..abee65c8 100644 --- a/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Abstractions/IExchangeProcessor.cs @@ -27,6 +27,7 @@ public interface IExchangeProcessor Task GetBalance(Account account, bool isForPaperTrading = false); Task> GetBalances(Account account, bool isForPaperTrading = false); Task GetPrice(Account account, Ticker ticker, DateTime date); + Task GetCurrentPrice(Account account, Ticker ticker); Task GetTrade(Account account, string order, Ticker ticker); Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval); decimal GetVolume(Account account, Ticker ticker); diff --git a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs index 3d620785..9eff8a0e 100644 --- a/src/Managing.Infrastructure.Exchanges/ExchangeService.cs +++ b/src/Managing.Infrastructure.Exchanges/ExchangeService.cs @@ -249,9 +249,34 @@ namespace Managing.Infrastructure.Exchanges } public async Task GetPrice(Account account, Ticker ticker, DateTime date) + { + try + { + var processor = GetProcessor(account); + return await processor.GetPrice(account, ticker, date); + } + catch (InvalidOperationException ex) when (ex.Message.Contains("no candle data available")) + { + _logger.LogWarning($"Primary price source failed for {ticker} at {date:yyyy-MM-dd HH:mm:ss}. Attempting fallback to current price."); + + // Fallback: Try to get current price instead of historical price + try + { + var processor = GetProcessor(account); + return await processor.GetCurrentPrice(account, ticker); + } + catch (Exception fallbackEx) + { + _logger.LogError(fallbackEx, $"Fallback price retrieval also failed for {ticker} at {date:yyyy-MM-dd HH:mm:ss}"); + throw new InvalidOperationException($"Unable to retrieve price for {ticker}. Both primary and fallback methods failed. Please check if the ticker is valid and data sources are accessible.", ex); + } + } + } + + public async Task GetCurrentPrice(Account account, Ticker ticker) { var processor = GetProcessor(account); - return await processor.GetPrice(account, ticker, date); + return await processor.GetCurrentPrice(account, ticker); } public async Task GetCandle(Account account, Ticker ticker, DateTime date) diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs index 2ac551da..14e6a537 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/BaseProcessor.cs @@ -17,6 +17,7 @@ namespace Managing.Infrastructure.Exchanges.Exchanges public abstract Task> GetCandles(Account account, Ticker ticker, DateTime startDate, Timeframe interval); public abstract decimal GetFee(Account account, bool isForPaperTrading = false); public abstract Task GetPrice(Account account, Ticker ticker, DateTime date); + public abstract Task GetCurrentPrice(Account account, Ticker ticker); public abstract Task GetQuantityInPosition(Account account, Ticker ticker); public abstract Task GetTrade(Account account, string order, Ticker ticker); public abstract Task> GetTrades(Account account, Ticker ticker); diff --git a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs index 4f4d7ad0..b1361582 100644 --- a/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs +++ b/src/Managing.Infrastructure.Exchanges/Exchanges/EvmProcessor.cs @@ -85,7 +85,59 @@ public class EvmProcessor : BaseProcessor public override async Task GetPrice(Account account, Ticker ticker, DateTime date) { - return (await GetCandles(account, ticker, date, Timeframe.OneMinute, true)).Last().Close; + var candles = await GetCandles(account, ticker, date, Timeframe.OneMinute, true); + + if (candles == null || !candles.Any()) + { + _logger.LogError($"No candles available for ticker {ticker} at date {date:yyyy-MM-dd HH:mm:ss}. This could indicate a data source issue or invalid ticker."); + throw new InvalidOperationException($"Cannot get price for {ticker} - no candle data available for the requested date {date:yyyy-MM-dd HH:mm:ss}. Please check if the ticker is valid and data source is accessible."); + } + + return candles.Last().Close; + } + + /// + /// Gets the current/latest price from the broker for live trading purposes. + /// This method is optimized for getting real-time prices and handles empty data gracefully. + /// + /// The trading account + /// The ticker symbol + /// The current price from the broker + public override async Task GetCurrentPrice(Account account, Ticker ticker) + { + try + { + // Try to get the most recent candle data (last 5 minutes) + var recentDate = DateTime.UtcNow.AddMinutes(-5); + var candles = await GetCandles(account, ticker, recentDate, Timeframe.OneMinute, true); + + if (candles != null && candles.Any()) + { + var latestCandle = candles.OrderByDescending(c => c.Date).First(); + _logger.LogDebug($"Retrieved current price {latestCandle.Close} for {ticker} from candle data"); + return latestCandle.Close; + } + + // Fallback: Try to get price from a broader time range + var fallbackDate = DateTime.UtcNow.AddHours(-1); + var fallbackCandles = await GetCandles(account, ticker, fallbackDate, Timeframe.OneMinute, true); + + if (fallbackCandles != null && fallbackCandles.Any()) + { + var latestFallbackCandle = fallbackCandles.OrderByDescending(c => c.Date).First(); + _logger.LogWarning($"Using fallback price {latestFallbackCandle.Close} for {ticker} from 1-hour old data"); + return latestFallbackCandle.Close; + } + + // If all else fails, throw a descriptive error + _logger.LogError($"No price data available for {ticker} from any source"); + throw new InvalidOperationException($"Cannot get current price for {ticker} - no price data available from broker. Please check if the ticker is valid and the broker is accessible."); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Error getting current price for {ticker}"); + throw new InvalidOperationException($"Failed to get current price for {ticker}: {ex.Message}", ex); + } } public override async Task GetQuantityInPosition(Account account, Ticker ticker) diff --git a/src/Managing.Infrastructure.Tests/EvmManagerTests.cs b/src/Managing.Infrastructure.Tests/EvmManagerTests.cs index a255f513..84c1f186 100644 --- a/src/Managing.Infrastructure.Tests/EvmManagerTests.cs +++ b/src/Managing.Infrastructure.Tests/EvmManagerTests.cs @@ -5,6 +5,7 @@ using Managing.Domain.Evm; using Managing.Infrastructure.Evm; using Managing.Infrastructure.Evm.Abstractions; using Managing.Infrastructure.Evm.Services; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -299,6 +300,7 @@ public class EvmManagerTests { var settings = new Web3ProxySettings { BaseUrl = baseUrl }; var options = Options.Create(settings); - return new Web3ProxyService(options); + var logger = new Mock>().Object; + return new Web3ProxyService(options, logger); } } \ No newline at end of file diff --git a/src/Managing.Infrastructure.Web3/EvmManager.cs b/src/Managing.Infrastructure.Web3/EvmManager.cs index e5f7629c..d9763e7d 100644 --- a/src/Managing.Infrastructure.Web3/EvmManager.cs +++ b/src/Managing.Infrastructure.Web3/EvmManager.cs @@ -441,12 +441,27 @@ public class EvmManager : IEvmManager } if (gmxPrices == null) - return null; + { + Console.WriteLine($"Warning: GMX API returned null for ticker {ticker}, timeframe {timeframe}, startDate {startDate:yyyy-MM-dd HH:mm:ss}"); + return new List(); + } + + if (gmxPrices.Candles == null || !gmxPrices.Candles.Any()) + { + Console.WriteLine($"Warning: GMX API returned empty candles array for ticker {ticker}, timeframe {timeframe}, startDate {startDate:yyyy-MM-dd HH:mm:ss}"); + return new List(); + } var filteredCandles = gmxPrices.Candles.Where(p => p[0] >= startDate.AddMinutes(-1).ToUnixTimestamp()).ToList(); + + if (!filteredCandles.Any()) + { + Console.WriteLine($"Warning: No candles found after filtering for ticker {ticker}, timeframe {timeframe}, startDate {startDate:yyyy-MM-dd HH:mm:ss}. Total candles before filtering: {gmxPrices.Candles.Count}"); + return new List(); + } + var candles = new List(); - var timeBetweenCandles = - gmxPrices.Candles.Count > 2 ? gmxPrices.Candles[0][0] - gmxPrices.Candles[1][0] : 900; // Default 15 minutes + var timeBetweenCandles = CandleHelpers.GetBaseIntervalInSeconds(timeframe); for (int i = 0; i < filteredCandles.Count; i++) {