From b39044bfe177a2d094bb6f88ca6e8fad7d45ef50 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Tue, 17 Mar 2026 06:56:42 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=A4=8D=E7=94=A8YahooQuotes=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=EF=BC=8C=E6=B7=BB=E5=8A=A0=E5=B9=B6=E5=8F=91=E9=99=90?= =?UTF-8?q?=E5=88=B6=E9=98=B2=E6=AD=A2429=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/YahooMarketService.cs | 161 +++++++++--------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/AssetManager.Infrastructure/Services/YahooMarketService.cs b/AssetManager.Infrastructure/Services/YahooMarketService.cs index 2225d0c..c9f286d 100644 --- a/AssetManager.Infrastructure/Services/YahooMarketService.cs +++ b/AssetManager.Infrastructure/Services/YahooMarketService.cs @@ -25,14 +25,23 @@ public interface IYahooMarketService } /// /// Yahoo财经市场数据服务实现 +/// ⚠️ Yahoo API 有频率限制,频繁请求会触发 429 错误 +/// 建议:依赖缓存机制减少 API 调用 /// public class YahooMarketService : IYahooMarketService { private readonly ILogger _logger; + private readonly YahooQuotes _yahooQuotes; + private readonly SemaphoreSlim _semaphore = new(1, 1); // 限制并发请求 public YahooMarketService(ILogger logger) { _logger = logger; + // 复用单个实例,设置足够长的历史范围(2年) + // YahooQuotes 内部会缓存 cookie/crumb,避免频繁请求 + _yahooQuotes = new YahooQuotesBuilder() + .WithHistoryStartDate(Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(-730))) + .Build(); } /// @@ -50,56 +59,43 @@ public class YahooMarketService : IYahooMarketService return symbol.ToUpper().Replace(".", "-"); } - /// - /// 根据 timeframe 和 limit 计算需要的开始日期 - /// - 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 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 + // 限制并发,避免触发 429 + await _semaphore.WaitAsync(); + try { - Symbol = symbol, // 返回原始代码 - Price = price, - PreviousClose = previousClose, - Timestamp = DateTime.UtcNow, - AssetType = "Stock" - }; + 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" + }; + } + finally + { + _semaphore.Release(); + } } public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) @@ -107,46 +103,47 @@ public class YahooMarketService : IYahooMarketService 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 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) + // 限制并发,避免触发 429 + await _semaphore.WaitAsync(); + try { - marketDataList.Add(new MarketDataResponse + 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) { - 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" - }); + 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; + } + finally + { + _semaphore.Release(); } - - // 按时间戳排序并取最近的limit条 - var sortedData = marketDataList - .OrderBy(x => x.Timestamp) - .TakeLast(limit) - .ToList(); - - _logger.LogInformation("Yahoo获取 {Symbol} 历史数据 {Count} 条(请求 {Limit} 条)", symbol, sortedData.Count, limit); - - return sortedData; } } \ No newline at end of file