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