AssetManager.API/AssetManager.Infrastructure/Services/YahooMarketService.cs
niannian zheng 2a6512ff48 feat(市场数据): 添加Yahoo财经服务并设为优先数据源
- 新增YahooMarketService实现股票实时价格和历史数据获取
- 更新MarketDataService优先使用Yahoo服务,腾讯财经降级为第二选择
- 添加YahooQuotesApi依赖并更新相关NuGet包版本
- 补充Yahoo服务测试用例
2026-03-17 12:06:47 +08:00

114 lines
3.7 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}