using System.Net.Http.Json; using AssetManager.Models.DTOs; using Microsoft.Extensions.Logging; namespace AssetManager.Infrastructure.Services; /// /// Tiingo 市场数据服务接口 /// public interface ITiingoMarketService { /// /// 获取股票实时价格 /// Task GetStockPriceAsync(string symbol); /// /// 获取股票历史数据 /// Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit); } /// /// Tiingo 市场数据服务实现 /// public class TiingoMarketService : ITiingoMarketService { private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly string _apiKey; public TiingoMarketService(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; _httpClient = httpClientFactory.CreateClient(); // 从环境变量读取 Tiingo API Key(必填) _apiKey = Environment.GetEnvironmentVariable("Tiingo__ApiKey") ?? Environment.GetEnvironmentVariable("TIINGO_API_KEY") ?? throw new InvalidOperationException("Tiingo__ApiKey 环境变量未配置"); _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_apiKey}"); } public async Task GetStockPriceAsync(string symbol) { _logger.LogInformation("Tiingo 获取股票价格: {Symbol}", symbol); // Tiingo 日线最新价格端点(取最近1条) var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?token={_apiKey}"; var response = await _httpClient.GetFromJsonAsync>(url); if (response == null || response.Count == 0) { throw new Exception($"No data found for {symbol}"); } var latest = response[^1]; decimal? prevClose = response.Count >= 2 ? response[^2].close : null; return new MarketPriceResponse { Symbol = symbol, Price = latest.close ?? 0, PreviousClose = prevClose ?? 0, Timestamp = latest.date ?? DateTime.UtcNow, AssetType = "Stock" }; } public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) { _logger.LogInformation("Tiingo 获取历史数据: {Symbol}", symbol); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&token={_apiKey}"; var response = await GetWithRetryAsync(url); var data = await response.Content.ReadFromJsonAsync>(); if (data == null) { return new List(); } return data .OrderByDescending(x => x.date) .Take(limit) .OrderBy(x => x.date) .Select(x => new MarketDataResponse { Symbol = symbol, Open = x.open ?? 0, High = x.high ?? 0, Low = x.low ?? 0, Close = x.close ?? 0, Volume = x.volume ?? 0, Timestamp = x.date ?? DateTime.UtcNow, AssetType = "Stock" }) .ToList(); } /// /// 带重试的 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) { 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} 次"); } private DateTime CalculateStartDate(DateTime endDate, string timeframe, int limit) { return timeframe.ToLower() switch { "1min" => endDate.AddMinutes(-limit), "5min" => endDate.AddMinutes(-limit * 5), "15min" => endDate.AddMinutes(-limit * 15), "1h" => endDate.AddHours(-limit), "1d" => endDate.AddDays(-limit), "1w" => endDate.AddDays(-limit * 7), "1m" => endDate.AddMonths(-limit), _ => endDate.AddDays(-limit) }; } } // Tiingo 响应模型 internal class TiingoDailyResponse { public decimal? open { get; set; } public decimal? high { get; set; } public decimal? low { get; set; } public decimal? close { get; set; } public decimal? volume { get; set; } public DateTime? date { get; set; } }