using NodaTime; using YahooQuotesApi; using AssetManager.Data; using AssetManager.Models.DTOs; using Microsoft.Extensions.Logging; using System.Collections.Immutable; namespace AssetManager.Infrastructure.Services; /// /// Yahoo财经市场数据服务接口 /// public interface IYahooMarketService { /// /// 获取股票实时价格 /// Task GetStockPriceAsync(string symbol); /// /// 获取股票历史K线数据 /// Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit); } /// /// Yahoo财经市场数据服务实现 /// public class YahooMarketService : IYahooMarketService { private readonly ILogger _logger; private readonly YahooQuotes _yahooQuotes; public YahooMarketService(ILogger logger) { _logger = logger; // 默认获取最近60天的历史数据 _yahooQuotes = new YahooQuotesBuilder() .WithHistoryStartDate(Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(-60))) .Build(); } /// /// 转换股票代码为 Yahoo Finance 格式 /// BRK.B → BRK-B(伯克希尔B类股) /// BRK.A → BRK-A(伯克希尔A类股) /// 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(".", "-"); } public async Task GetStockPriceAsync(string symbol) { var yahooSymbol = ConvertToYahooSymbol(symbol); _logger.LogInformation("Yahoo获取股票价格: {Symbol} -> {YahooSymbol}", symbol, yahooSymbol); 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> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) { var yahooSymbol = ConvertToYahooSymbol(symbol); _logger.LogInformation("Yahoo获取历史数据: {Symbol} -> {YahooSymbol}, {Timeframe}, {Limit}", symbol, yahooSymbol, timeframe, limit); Result 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(); 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; } }