diff --git a/AssetManager.API/Program.cs b/AssetManager.API/Program.cs index 5ce69f7..814b32d 100755 --- a/AssetManager.API/Program.cs +++ b/AssetManager.API/Program.cs @@ -93,6 +93,7 @@ builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj b/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj index 0fd5970..d346f50 100755 --- a/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj +++ b/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj @@ -6,9 +6,10 @@ - - - + + + + diff --git a/AssetManager.Infrastructure/Services/MarketDataService.cs b/AssetManager.Infrastructure/Services/MarketDataService.cs index 5c1bbf5..32f6035 100755 --- a/AssetManager.Infrastructure/Services/MarketDataService.cs +++ b/AssetManager.Infrastructure/Services/MarketDataService.cs @@ -6,12 +6,13 @@ using Microsoft.Extensions.Logging; namespace AssetManager.Infrastructure.Services; /// -/// 市场数据服务实现(组合模式:腾讯财经优先,Tiingo降级,OKX处理加密货币) +/// 市场数据服务实现(组合模式:Yahoo财经优先,腾讯财经降级,Tiingo最终降级,OKX处理加密货币) /// public class MarketDataService : IMarketDataService { private readonly ILogger _logger; private readonly ITencentMarketService _tencentService; + private readonly IYahooMarketService _yahooService; private readonly ITiingoMarketService _tiingoService; private readonly IOkxMarketService _okxService; private readonly IMarketDataRepository _marketDataRepo; @@ -19,12 +20,14 @@ public class MarketDataService : IMarketDataService public MarketDataService( ILogger logger, ITencentMarketService tencentService, + IYahooMarketService yahooService, ITiingoMarketService tiingoService, IOkxMarketService okxService, IMarketDataRepository marketDataRepo) { _logger = logger; _tencentService = tencentService; + _yahooService = yahooService; _tiingoService = tiingoService; _okxService = okxService; _marketDataRepo = marketDataRepo; @@ -64,17 +67,26 @@ public class MarketDataService : IMarketDataService } else { - // 股票:优先腾讯财经,失败降级 Tiingo + // 股票:优先Yahoo财经,失败降级腾讯财经,最后降级 Tiingo try { - response = await _tencentService.GetStockPriceAsync(symbol); - source = "Tencent"; + response = await _yahooService.GetStockPriceAsync(symbol); + source = "Yahoo"; } catch (Exception ex) { - _logger.LogWarning(ex, "腾讯财经获取失败,降级使用 Tiingo: {Symbol}", symbol); - response = await _tiingoService.GetStockPriceAsync(symbol); - source = "Tiingo"; + _logger.LogWarning(ex, "Yahoo财经获取失败,降级使用 腾讯: {Symbol}", symbol); + try + { + response = await _tencentService.GetStockPriceAsync(symbol); + source = "Tencent"; + } + catch (Exception tencentEx) + { + _logger.LogWarning(tencentEx, "腾讯财经获取失败,降级使用 Tiingo: {Symbol}", symbol); + response = await _tiingoService.GetStockPriceAsync(symbol); + source = "Tiingo"; + } } } @@ -123,24 +135,40 @@ public class MarketDataService : IMarketDataService } else { - // 股票:优先腾讯财经,失败降级 Tiingo + // 股票:优先Yahoo财经,失败降级腾讯财经,最后降级 Tiingo try { - response = await _tencentService.GetStockHistoricalDataAsync(symbol, timeframe, limit); + response = await _yahooService.GetStockHistoricalDataAsync(symbol, timeframe, limit); if (response.Count > 0) { - source = "Tencent"; + source = "Yahoo"; } else { - throw new Exception("腾讯财经返回空数据"); + throw new Exception("Yahoo财经返回空数据"); } } catch (Exception ex) { - _logger.LogWarning(ex, "腾讯财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol); - response = await _tiingoService.GetStockHistoricalDataAsync(symbol, timeframe, limit); - source = "Tiingo"; + _logger.LogWarning(ex, "Yahoo财经历史数据获取失败,降级使用 腾讯: {Symbol}", symbol); + try + { + response = await _tencentService.GetStockHistoricalDataAsync(symbol, timeframe, limit); + if (response.Count > 0) + { + source = "Tencent"; + } + else + { + throw new Exception("腾讯财经返回空数据"); + } + } + catch (Exception tencentEx) + { + _logger.LogWarning(tencentEx, "腾讯财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol); + response = await _tiingoService.GetStockHistoricalDataAsync(symbol, timeframe, limit); + source = "Tiingo"; + } } } diff --git a/AssetManager.Infrastructure/Services/YahooMarketService.cs b/AssetManager.Infrastructure/Services/YahooMarketService.cs new file mode 100644 index 0000000..079573c --- /dev/null +++ b/AssetManager.Infrastructure/Services/YahooMarketService.cs @@ -0,0 +1,114 @@ +using NodaTime; +using YahooQuotesApi; +using AssetManager.Data; +using AssetManager.Models.DTOs; +using Microsoft.Extensions.Logging; +using System.Collections.Immutable; + +namespace AssetManager.Infrastructure.Services; + + +/// +/// Yahoo财经市场数据服务接口 +/// +public interface IYahooMarketService +{ + /// + /// 获取股票实时价格 + /// + Task GetStockPriceAsync(string symbol); + + /// + /// 获取股票历史K线数据 + /// + Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit); +} +/// +/// Yahoo财经市场数据服务实现 +/// +public class YahooMarketService : IYahooMarketService +{ + private readonly ILogger _logger; + private readonly YahooQuotes _yahooQuotes; + + public YahooMarketService(ILogger logger) + { + _logger = logger; + _yahooQuotes = new YahooQuotesBuilder().Build(); + } + + public async Task GetStockPriceAsync(string symbol) + { + _logger.LogInformation("Yahoo获取股票价格: {Symbol}", symbol); + + Snapshot? snapshot = await _yahooQuotes.GetSnapshotAsync(symbol); + if (snapshot is null) + throw new Exception($"Yahoo未知标的: {symbol}"); + + decimal price = snapshot.RegularMarketPrice; + if (price <= 0) + throw new Exception($"Yahoo获取价格失败,标的: {symbol}"); + + decimal previousClose = snapshot.RegularMarketPreviousClose; + if (previousClose <= 0) + previousClose = price; + + _logger.LogDebug("Yahoo接口返回 {Symbol}:最新价 {CurrentPrice},昨收价 {PrevClose}", + symbol, price, previousClose); + + return new MarketPriceResponse + { + Symbol = symbol, + Price = price, + PreviousClose = previousClose, + Timestamp = DateTime.UtcNow, + AssetType = "Stock" + }; + } + + public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) + { + _logger.LogInformation("Yahoo获取历史数据: {Symbol}, {Timeframe}, {Limit}", symbol, timeframe, limit); + + // 计算历史数据的开始时间 + Instant startDate = Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(-limit * 2)); + + YahooQuotes yahooQuotes = new YahooQuotesBuilder() + .WithHistoryStartDate(startDate) + .Build(); + + Result result = await yahooQuotes.GetHistoryAsync(symbol); + if (result.Value == null) + throw new Exception($"Yahoo获取历史数据失败,标的: {symbol}"); + + History history = result.Value; + var ticks = history.Ticks; + + var marketDataList = new List(); + + foreach (var tick in ticks) + { + marketDataList.Add(new MarketDataResponse + { + Symbol = symbol, + Timestamp = tick.Date.ToDateTimeUtc(), + Open = (decimal)tick.Open, + High = (decimal)tick.High, + Low = (decimal)tick.Low, + Close = (decimal)tick.Close, + Volume = (decimal)tick.Volume, + AssetType = "Stock" + }); + } + + // 按时间戳排序并取最近的limit条 + var sortedData = marketDataList + .OrderBy(x => x.Timestamp) + .TakeLast(limit) + .ToList(); + + _logger.LogInformation("Yahoo获取 {Symbol} 历史数据 {Count} 条", symbol, sortedData.Count); + + return sortedData; + } +} \ No newline at end of file diff --git a/AssetManager.Services/AssetManager.Services.csproj b/AssetManager.Services/AssetManager.Services.csproj index 179441e..8d2d087 100755 --- a/AssetManager.Services/AssetManager.Services.csproj +++ b/AssetManager.Services/AssetManager.Services.csproj @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + diff --git a/AssetManager.Tests/Services/MarketDataServiceTests.cs b/AssetManager.Tests/Services/MarketDataServiceTests.cs index 0e7333d..487122b 100644 --- a/AssetManager.Tests/Services/MarketDataServiceTests.cs +++ b/AssetManager.Tests/Services/MarketDataServiceTests.cs @@ -15,6 +15,7 @@ public class MarketDataServiceTests { private readonly Mock _tencentMock; private readonly Mock _tiingoMock; + private readonly Mock _yahooService; private readonly Mock _okxMock; private readonly Mock _repoMock; private readonly Mock> _loggerMock; @@ -24,6 +25,7 @@ public class MarketDataServiceTests { _tencentMock = new Mock(); _tiingoMock = new Mock(); + _yahooService = new Mock(); _okxMock = new Mock(); _repoMock = new Mock(); _loggerMock = new Mock>(); @@ -31,6 +33,7 @@ public class MarketDataServiceTests _service = new MarketDataService( _loggerMock.Object, _tencentMock.Object, + _yahooService.Object, _tiingoMock.Object, _okxMock.Object, _repoMock.Object