AssetManager.API/AssetManager.Infrastructure/Services/YahooMarketService.cs

152 lines
5.5 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;
public YahooMarketService(ILogger<YahooMarketService> logger)
{
_logger = logger;
}
/// <summary>
/// 转换股票代码为 Yahoo Finance 格式
/// <para>BRK.B → BRK-B伯克希尔B类股</para>
/// <para>BRK.A → BRK-A伯克希尔A类股</para>
/// </summary>
private string ConvertToYahooSymbol(string symbol)
{
if (string.IsNullOrEmpty(symbol))
return symbol;
// Yahoo Finance 用连字符表示类别股,而非点号
// BRK.B → BRK-B, BRK.A → BRK-A
return symbol.ToUpper().Replace(".", "-");
}
/// <summary>
/// 根据 timeframe 和 limit 计算需要的开始日期
/// </summary>
private DateTime CalculateStartDate(string timeframe, int limit)
{
var now = DateTime.UtcNow;
// 根据周期类型计算需要的天数,预留缓冲
return timeframe.ToLower() switch
{
"1d" or "daily" or "day" => now.AddDays(-limit * 1.5), // 日线limit * 1.5 天
"1w" or "weekly" or "week" => now.AddDays(-limit * 8), // 周线limit * 8 天
"1m" or "monthly" or "month" => now.AddDays(-limit * 35), // 月线limit * 35 天
_ => now.AddDays(-Math.Max(limit * 2, 365)) // 默认至少365天
};
}
public async Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
{
var yahooSymbol = ConvertToYahooSymbol(symbol);
_logger.LogInformation("Yahoo获取股票价格: {Symbol} -> {YahooSymbol}", symbol, yahooSymbol);
// 实时价格不需要历史数据,使用最小范围
var yahooQuotes = new YahooQuotesBuilder()
.WithHistoryStartDate(Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(-7)))
.Build();
Snapshot? snapshot = await yahooQuotes.GetSnapshotAsync(yahooSymbol);
if (snapshot is null)
throw new InvalidOperationException($"Yahoo未知标的: {symbol}");
decimal price = snapshot.RegularMarketPrice;
if (price <= 0)
throw new InvalidOperationException($"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)
{
var yahooSymbol = ConvertToYahooSymbol(symbol);
_logger.LogInformation("Yahoo获取历史数据: {Symbol} -> {YahooSymbol}, {Timeframe}, {Limit}", symbol, yahooSymbol, timeframe, limit);
// 根据请求参数动态计算开始日期
var startDate = CalculateStartDate(timeframe, limit);
_logger.LogDebug("历史数据开始日期: {StartDate}", startDate.ToString("yyyy-MM-dd"));
var yahooQuotes = new YahooQuotesBuilder()
.WithHistoryStartDate(Instant.FromDateTimeUtc(startDate))
.Build();
Result<History> result = await yahooQuotes.GetHistoryAsync(yahooSymbol);
if (result.Value == null)
throw new InvalidOperationException($"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} 条(请求 {Limit} 条)", symbol, sortedData.Count, limit);
return sortedData;
}
}