feat(市场数据): 添加Yahoo财经服务并设为优先数据源

- 新增YahooMarketService实现股票实时价格和历史数据获取
- 更新MarketDataService优先使用Yahoo服务,腾讯财经降级为第二选择
- 添加YahooQuotesApi依赖并更新相关NuGet包版本
- 补充Yahoo服务测试用例
This commit is contained in:
niannian zheng 2026-03-17 12:06:47 +08:00
parent 5bc318725d
commit 2a6512ff48
6 changed files with 166 additions and 19 deletions

View File

@ -93,6 +93,7 @@ builder.Services.AddScoped<AssetManager.Services.IPortfolioFacade, AssetManager.
// 市场数据子服务(组合模式)
builder.Services.AddScoped<AssetManager.Infrastructure.Services.ITencentMarketService, AssetManager.Infrastructure.Services.TencentMarketService>();
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IYahooMarketService, AssetManager.Infrastructure.Services.YahooMarketService>();
builder.Services.AddScoped<AssetManager.Infrastructure.Services.ITiingoMarketService, AssetManager.Infrastructure.Services.TiingoMarketService>();
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IOkxMarketService, AssetManager.Infrastructure.Services.OkxMarketService>();

View File

@ -6,9 +6,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="YahooQuotesApi" Version="7.0.6" />
</ItemGroup>
<PropertyGroup>

View File

@ -6,12 +6,13 @@ using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 市场数据服务实现(组合模式:腾讯财经优先Tiingo降级OKX处理加密货币
/// 市场数据服务实现(组合模式:Yahoo财经优先腾讯财经降级Tiingo最终降级OKX处理加密货币
/// </summary>
public class MarketDataService : IMarketDataService
{
private readonly ILogger<MarketDataService> _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<MarketDataService> logger,
ITencentMarketService tencentService,
IYahooMarketService yahooService,
ITiingoMarketService tiingoService,
IOkxMarketService okxService,
IMarketDataRepository marketDataRepo)
{
_logger = logger;
_tencentService = tencentService;
_yahooService = yahooService;
_tiingoService = tiingoService;
_okxService = okxService;
_marketDataRepo = marketDataRepo;
@ -64,19 +67,28 @@ public class MarketDataService : IMarketDataService
}
else
{
// 股票:优先腾讯财经,失败降级 Tiingo
// 股票优先Yahoo财经失败降级腾讯财经最后降级 Tiingo
try
{
response = await _yahooService.GetStockPriceAsync(symbol);
source = "Yahoo";
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Yahoo财经获取失败降级使用 腾讯: {Symbol}", symbol);
try
{
response = await _tencentService.GetStockPriceAsync(symbol);
source = "Tencent";
}
catch (Exception ex)
catch (Exception tencentEx)
{
_logger.LogWarning(ex, "腾讯财经获取失败,降级使用 Tiingo: {Symbol}", symbol);
_logger.LogWarning(tencentEx, "腾讯财经获取失败,降级使用 Tiingo: {Symbol}", symbol);
response = await _tiingoService.GetStockPriceAsync(symbol);
source = "Tiingo";
}
}
}
// 写入缓存
await SavePriceCacheAsync(symbol, assetType, response, source);
@ -123,7 +135,22 @@ public class MarketDataService : IMarketDataService
}
else
{
// 股票:优先腾讯财经,失败降级 Tiingo
// 股票优先Yahoo财经失败降级腾讯财经最后降级 Tiingo
try
{
response = await _yahooService.GetStockHistoricalDataAsync(symbol, timeframe, limit);
if (response.Count > 0)
{
source = "Yahoo";
}
else
{
throw new Exception("Yahoo财经返回空数据");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Yahoo财经历史数据获取失败降级使用 腾讯: {Symbol}", symbol);
try
{
response = await _tencentService.GetStockHistoricalDataAsync(symbol, timeframe, limit);
@ -136,13 +163,14 @@ public class MarketDataService : IMarketDataService
throw new Exception("腾讯财经返回空数据");
}
}
catch (Exception ex)
catch (Exception tencentEx)
{
_logger.LogWarning(ex, "腾讯财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol);
_logger.LogWarning(tencentEx, "腾讯财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol);
response = await _tiingoService.GetStockHistoricalDataAsync(symbol, timeframe, limit);
source = "Tiingo";
}
}
}
// 批量写入缓存
await SaveKlineCacheAsync(symbol, assetType, timeframe, response, source);

View File

@ -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;
/// <summary>
/// Yahoo财经市场数据服务接口
/// </summary>
public interface IYahooMarketService
{
/// <summary>
/// 获取股票实时价格
/// </summary>
Task<MarketPriceResponse> GetStockPriceAsync(string symbol);
/// <summary>
/// 获取股票历史K线数据
/// </summary>
Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit);
}
/// <summary>
/// Yahoo财经市场数据服务实现
/// </summary>
public class YahooMarketService : IYahooMarketService
{
private readonly ILogger<YahooMarketService> _logger;
private readonly YahooQuotes _yahooQuotes;
public YahooMarketService(ILogger<YahooMarketService> logger)
{
_logger = logger;
_yahooQuotes = new YahooQuotesBuilder().Build();
}
public async Task<MarketPriceResponse> 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<List<MarketDataResponse>> 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<History> 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<MarketDataResponse>();
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;
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\AssetManager.Data\AssetManager.Data.csproj" />
@ -7,7 +7,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.5" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup>

View File

@ -15,6 +15,7 @@ public class MarketDataServiceTests
{
private readonly Mock<ITencentMarketService> _tencentMock;
private readonly Mock<ITiingoMarketService> _tiingoMock;
private readonly Mock<IYahooMarketService> _yahooService;
private readonly Mock<IOkxMarketService> _okxMock;
private readonly Mock<IMarketDataRepository> _repoMock;
private readonly Mock<ILogger<MarketDataService>> _loggerMock;
@ -24,6 +25,7 @@ public class MarketDataServiceTests
{
_tencentMock = new Mock<ITencentMarketService>();
_tiingoMock = new Mock<ITiingoMarketService>();
_yahooService = new Mock<IYahooMarketService>();
_okxMock = new Mock<IOkxMarketService>();
_repoMock = new Mock<IMarketDataRepository>();
_loggerMock = new Mock<ILogger<MarketDataService>>();
@ -31,6 +33,7 @@ public class MarketDataServiceTests
_service = new MarketDataService(
_loggerMock.Object,
_tencentMock.Object,
_yahooService.Object,
_tiingoMock.Object,
_okxMock.Object,
_repoMock.Object