fix: 复用YahooQuotes实例,添加并发限制防止429错误

This commit is contained in:
OpenClaw Agent 2026-03-17 06:56:42 +00:00
parent 2edac30fd8
commit b39044bfe1

View File

@ -25,14 +25,23 @@ public interface IYahooMarketService
}
/// <summary>
/// Yahoo财经市场数据服务实现
/// <para>⚠️ Yahoo API 有频率限制,频繁请求会触发 429 错误</para>
/// <para>建议:依赖缓存机制减少 API 调用</para>
/// </summary>
public class YahooMarketService : IYahooMarketService
{
private readonly ILogger<YahooMarketService> _logger;
private readonly YahooQuotes _yahooQuotes;
private readonly SemaphoreSlim _semaphore = new(1, 1); // 限制并发请求
public YahooMarketService(ILogger<YahooMarketService> logger)
{
_logger = logger;
// 复用单个实例设置足够长的历史范围2年
// YahooQuotes 内部会缓存 cookie/crumb避免频繁请求
_yahooQuotes = new YahooQuotesBuilder()
.WithHistoryStartDate(Instant.FromDateTimeUtc(DateTime.UtcNow.AddDays(-730)))
.Build();
}
/// <summary>
@ -50,56 +59,43 @@ public class YahooMarketService : IYahooMarketService
return symbol.ToUpper().Replace(".", "-");
}
/// <summary>
/// 根据 timeframe 和 limit 计算需要的开始日期
/// </summary>
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<MarketPriceResponse> 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<List<MarketDataResponse>> 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<History> 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<MarketDataResponse>();
foreach (var tick in ticks)
// 限制并发,避免触发 429
await _semaphore.WaitAsync();
try
{
marketDataList.Add(new MarketDataResponse
Result<History> 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<MarketDataResponse>();
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;
}
}