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.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.ITiingoMarketService, AssetManager.Infrastructure.Services.TiingoMarketService>();
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IOkxMarketService, AssetManager.Infrastructure.Services.OkxMarketService>(); builder.Services.AddScoped<AssetManager.Infrastructure.Services.IOkxMarketService, AssetManager.Infrastructure.Services.OkxMarketService>();

View File

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

View File

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

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> <ItemGroup>
<ProjectReference Include="..\AssetManager.Data\AssetManager.Data.csproj" /> <ProjectReference Include="..\AssetManager.Data\AssetManager.Data.csproj" />
@ -7,7 +7,7 @@
</ItemGroup> </ItemGroup>
<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" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.16.0" />
</ItemGroup> </ItemGroup>

View File

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