fix: 行情获取失败时不写入错误数据,添加 429 限流重试机制
1. GetHistoricalPriceAsync 返回 decimal? 而非 decimal - 价格获取失败时返回 null 而非 0 2. BackfillNavHistoryInternalAsync 检查价格有效性 - 任何持仓价格获取失败时跳过该日期 - 不写入 totalValue=0 的错误数据 3. MarketDataService 添加 GetWithRetryAsync 方法 - 处理 429 Too Many Requests 限流 - 最多重试 3 次,指数退避
This commit is contained in:
parent
6a757f56da
commit
da86aa43e6
@ -167,6 +167,40 @@ public class MarketDataService : IMarketDataService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 带重试的 HTTP GET 请求(处理 429 限流)
|
||||
/// </summary>
|
||||
private async Task<HttpResponseMessage> 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} 次");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取股票历史数据
|
||||
/// </summary>
|
||||
@ -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<List<TiingoDailyResponse>>(url);
|
||||
|
||||
// 使用带重试的请求
|
||||
var response = await GetWithRetryAsync(url);
|
||||
var data = await response.Content.ReadFromJsonAsync<List<TiingoDailyResponse>>();
|
||||
|
||||
if (response == null)
|
||||
if (data == null)
|
||||
{
|
||||
return new List<MarketDataResponse>();
|
||||
}
|
||||
|
||||
// 取最近 limit 条
|
||||
var result = response
|
||||
var result = data
|
||||
.OrderByDescending(x => x.date)
|
||||
.Take(limit)
|
||||
.OrderBy(x => x.date)
|
||||
|
||||
@ -373,15 +373,34 @@ public class PortfolioNavService : IPortfolioNavService
|
||||
|
||||
// 计算当日市值
|
||||
decimal totalValue = 0;
|
||||
bool hasValidPrice = true;
|
||||
List<string> failedSymbols = new List<string>();
|
||||
|
||||
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
|
||||
/// <summary>
|
||||
/// 获取历史价格
|
||||
/// </summary>
|
||||
private async Task<decimal> GetHistoricalPriceAsync(string symbol, string assetType, DateTime date)
|
||||
private async Task<decimal?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user