From da86aa43e61cc7c10732c2934b239d45adc8c0d6 Mon Sep 17 00:00:00 2001 From: OpenClaw Agent Date: Sun, 15 Mar 2026 10:02:52 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=A1=8C=E6=83=85=E8=8E=B7=E5=8F=96?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E4=B8=8D=E5=86=99=E5=85=A5=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=95=B0=E6=8D=AE=EF=BC=8C=E6=B7=BB=E5=8A=A0=20429=20?= =?UTF-8?q?=E9=99=90=E6=B5=81=E9=87=8D=E8=AF=95=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. GetHistoricalPriceAsync 返回 decimal? 而非 decimal - 价格获取失败时返回 null 而非 0 2. BackfillNavHistoryInternalAsync 检查价格有效性 - 任何持仓价格获取失败时跳过该日期 - 不写入 totalValue=0 的错误数据 3. MarketDataService 添加 GetWithRetryAsync 方法 - 处理 429 Too Many Requests 限流 - 最多重试 3 次,指数退避 --- .../Services/MarketDataService.cs | 43 +++++++++++++++++-- AssetManager.Services/PortfolioNavService.cs | 34 +++++++++++++-- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/AssetManager.Infrastructure/Services/MarketDataService.cs b/AssetManager.Infrastructure/Services/MarketDataService.cs index 3e8dba9..b69faaf 100755 --- a/AssetManager.Infrastructure/Services/MarketDataService.cs +++ b/AssetManager.Infrastructure/Services/MarketDataService.cs @@ -167,6 +167,40 @@ public class MarketDataService : IMarketDataService } } + /// + /// 带重试的 HTTP GET 请求(处理 429 限流) + /// + private async Task GetWithRetryAsync(string url, int maxRetries = 3) + { + for (int i = 0; i < maxRetries; i++) + { + try + { + var response = await _httpClient.GetAsync(url); + + if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests) + { + // 429 限流,等待后重试 + var retryAfter = response.Headers.RetryAfter?.Delta?.TotalSeconds ?? 2 * (i + 1); + _logger.LogWarning("Tiingo API 限流,等待 {Seconds} 秒后重试 (第 {Attempt}/{Max} 次)", + retryAfter, i + 1, maxRetries); + await Task.Delay((int)(retryAfter * 1000)); + continue; + } + + response.EnsureSuccessStatusCode(); + return response; + } + catch (HttpRequestException ex) when (i < maxRetries - 1) + { + _logger.LogWarning(ex, "HTTP 请求失败,重试中 (第 {Attempt}/{Max} 次)", i + 1, maxRetries); + await Task.Delay(1000 * (i + 1)); + } + } + + throw new HttpRequestException($"请求失败,已重试 {maxRetries} 次"); + } + /// /// 获取股票历史数据 /// @@ -185,15 +219,18 @@ public class MarketDataService : IMarketDataService // Tiingo 历史数据端点(和示例一致) var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&token={_tiingoApiKey}"; - var response = await _httpClient.GetFromJsonAsync>(url); + + // 使用带重试的请求 + var response = await GetWithRetryAsync(url); + var data = await response.Content.ReadFromJsonAsync>(); - if (response == null) + if (data == null) { return new List(); } // 取最近 limit 条 - var result = response + var result = data .OrderByDescending(x => x.date) .Take(limit) .OrderBy(x => x.date) diff --git a/AssetManager.Services/PortfolioNavService.cs b/AssetManager.Services/PortfolioNavService.cs index 6afacd2..cd1b1bf 100644 --- a/AssetManager.Services/PortfolioNavService.cs +++ b/AssetManager.Services/PortfolioNavService.cs @@ -373,15 +373,34 @@ public class PortfolioNavService : IPortfolioNavService // 计算当日市值 decimal totalValue = 0; + bool hasValidPrice = true; + List failedSymbols = new List(); + foreach (var (stockCode, (shares, cost, currency, assetType)) in holdings) { - decimal price = await GetHistoricalPriceAsync(stockCode, assetType ?? "Stock", date); + var priceResult = await GetHistoricalPriceAsync(stockCode, assetType ?? "Stock", date); + if (priceResult == null || priceResult <= 0) + { + hasValidPrice = false; + failedSymbols.Add(stockCode); + continue; + } + + decimal price = priceResult.Value; decimal positionValue = shares * price; decimal positionValueInTarget = await _exchangeRateService.ConvertAmountAsync( positionValue, currency ?? targetCurrency, targetCurrency); totalValue += positionValueInTarget; } + // 如果有任何持仓价格获取失败,跳过该日期,不写入错误数据 + if (!hasValidPrice) + { + _logger.LogWarning("跳过日期 {Date},以下持仓价格获取失败: {Symbols}", + date.ToString("yyyy-MM-dd"), string.Join(", ", failedSymbols)); + continue; + } + // 计算净值 decimal nav = cumulativeCost > 0 ? totalValue / cumulativeCost : 1.0m; decimal cumulativeReturn = cumulativeCost > 0 ? (totalValue - cumulativeCost) / cumulativeCost * 100 : 0; @@ -452,7 +471,7 @@ public class PortfolioNavService : IPortfolioNavService /// /// 获取历史价格 /// - private async Task GetHistoricalPriceAsync(string symbol, string assetType, DateTime date) + private async Task GetHistoricalPriceAsync(string symbol, string assetType, DateTime date) { try { @@ -478,12 +497,19 @@ public class PortfolioNavService : IPortfolioNavService // 最后尝试获取实时价格 var currentPrice = await _marketDataService.GetPriceAsync(symbol, assetType); - return currentPrice.Price; + if (currentPrice != null && currentPrice.Price > 0) + { + return currentPrice.Price; + } + + // 无法获取有效价格 + _logger.LogWarning("无法获取有效价格: {Symbol}, {Date}", symbol, date); + return null; } catch (Exception ex) { _logger.LogWarning(ex, "获取历史价格失败: {Symbol}, {Date}", symbol, date); - return 0; + return null; } }