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>
|
||||||
/// 获取股票历史数据
|
/// 获取股票历史数据
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -185,15 +219,18 @@ public class MarketDataService : IMarketDataService
|
|||||||
|
|
||||||
// Tiingo 历史数据端点(和示例一致)
|
// Tiingo 历史数据端点(和示例一致)
|
||||||
var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&token={_tiingoApiKey}";
|
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>();
|
return new List<MarketDataResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取最近 limit 条
|
// 取最近 limit 条
|
||||||
var result = response
|
var result = data
|
||||||
.OrderByDescending(x => x.date)
|
.OrderByDescending(x => x.date)
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.OrderBy(x => x.date)
|
.OrderBy(x => x.date)
|
||||||
|
|||||||
@ -373,15 +373,34 @@ public class PortfolioNavService : IPortfolioNavService
|
|||||||
|
|
||||||
// 计算当日市值
|
// 计算当日市值
|
||||||
decimal totalValue = 0;
|
decimal totalValue = 0;
|
||||||
|
bool hasValidPrice = true;
|
||||||
|
List<string> failedSymbols = new List<string>();
|
||||||
|
|
||||||
foreach (var (stockCode, (shares, cost, currency, assetType)) in holdings)
|
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 positionValue = shares * price;
|
||||||
decimal positionValueInTarget = await _exchangeRateService.ConvertAmountAsync(
|
decimal positionValueInTarget = await _exchangeRateService.ConvertAmountAsync(
|
||||||
positionValue, currency ?? targetCurrency, targetCurrency);
|
positionValue, currency ?? targetCurrency, targetCurrency);
|
||||||
totalValue += positionValueInTarget;
|
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 nav = cumulativeCost > 0 ? totalValue / cumulativeCost : 1.0m;
|
||||||
decimal cumulativeReturn = cumulativeCost > 0 ? (totalValue - cumulativeCost) / cumulativeCost * 100 : 0;
|
decimal cumulativeReturn = cumulativeCost > 0 ? (totalValue - cumulativeCost) / cumulativeCost * 100 : 0;
|
||||||
@ -452,7 +471,7 @@ public class PortfolioNavService : IPortfolioNavService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取历史价格
|
/// 获取历史价格
|
||||||
/// </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
|
try
|
||||||
{
|
{
|
||||||
@ -478,12 +497,19 @@ public class PortfolioNavService : IPortfolioNavService
|
|||||||
|
|
||||||
// 最后尝试获取实时价格
|
// 最后尝试获取实时价格
|
||||||
var currentPrice = await _marketDataService.GetPriceAsync(symbol, assetType);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogWarning(ex, "获取历史价格失败: {Symbol}, {Date}", symbol, date);
|
_logger.LogWarning(ex, "获取历史价格失败: {Symbol}, {Date}", symbol, date);
|
||||||
return 0;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user