feat(市场数据): 添加Yahoo财经服务并设为优先数据源
- 新增YahooMarketService实现股票实时价格和历史数据获取 - 更新MarketDataService优先使用Yahoo服务,腾讯财经降级为第二选择 - 添加YahooQuotesApi依赖并更新相关NuGet包版本 - 补充Yahoo服务测试用例
This commit is contained in:
parent
5bc318725d
commit
2a6512ff48
@ -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>();
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
114
AssetManager.Infrastructure/Services/YahooMarketService.cs
Normal file
114
AssetManager.Infrastructure/Services/YahooMarketService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user