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.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>();
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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>
|
<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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user