using System.Text.Json; using AssetManager.Models.DTOs; using Microsoft.Extensions.Logging; namespace AssetManager.Infrastructure.Services; /// /// 市场数据服务实现(基于 Tiingo) /// public class MarketDataService : IMarketDataService { private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly string _tiingoApiKey; /// /// 构造函数 /// /// 日志记录器 /// HTTP 客户端工厂 public MarketDataService(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; _httpClient = httpClientFactory.CreateClient(); // TODO: 从配置读取 Tiingo API Key _tiingoApiKey = Environment.GetEnvironmentVariable("TIINGO_API_KEY") ?? "bd00fee76d3012b047473078904001b33322cb46"; _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_tiingoApiKey}"); } /// /// 获取股票实时价格 /// /// 股票代码 /// 股票价格信息 public async Task GetStockPriceAsync(string symbol) { try { _logger.LogInformation($"Requesting stock price for symbol: {symbol}"); // Tiingo 日线最新价格端点(取最近1条) var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?token={_tiingoApiKey}"; 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" }; } catch (Exception ex) { _logger.LogError(ex, $"Error getting stock price for {symbol}"); throw; } } /// /// 获取加密货币实时价格 /// /// 加密货币代码(如 BTCUSD) /// 加密货币价格信息 public async Task GetCryptoPriceAsync(string symbol) { try { _logger.LogInformation($"Requesting crypto price for symbol: {symbol}"); // Tiingo 加密货币最新价格端点 var url = $"https://api.tiingo.com/tiingo/crypto/prices?tickers={symbol}&token={_tiingoApiKey}"; var response = await _httpClient.GetFromJsonAsync>(url); if (response == null || response.Count == 0 || response[0].priceData == null || response[0].priceData.Count == 0) { throw new Exception($"No data found for {symbol}"); } var latest = response[0].priceData[0]; return new MarketPriceResponse { Symbol = symbol, Price = latest.close ?? 0, PreviousClose = 0, // Tiingo crypto 端点没有 prevClose,暂时用 0 Timestamp = latest.date ?? DateTime.UtcNow, AssetType = "Crypto" }; } catch (Exception ex) { _logger.LogError(ex, $"Error getting crypto price for {symbol}"); throw; } } /// /// 获取股票历史数据 /// /// 股票代码 /// 时间周期 /// 数据点数量 /// 历史数据列表 public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) { try { _logger.LogInformation($"Requesting stock historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}"); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); var resampleFreq = GetTiingoResampleFreq(timeframe); // Tiingo 历史数据端点 var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&resampleFreq={resampleFreq}&token={_tiingoApiKey}"; var response = await _httpClient.GetFromJsonAsync>(url); if (response == null) { return new List(); } // 取最近 limit 条 var result = response .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(); return result; } catch (Exception ex) { _logger.LogError(ex, $"Error getting stock historical data for {symbol}"); throw; } } /// /// 获取加密货币历史数据 /// /// 加密货币代码 /// 时间周期 /// 数据点数量 /// 历史数据列表 public async Task> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit) { try { _logger.LogInformation($"Requesting crypto historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}"); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); var resampleFreq = GetTiingoResampleFreq(timeframe); // Tiingo 加密货币历史数据端点 var url = $"https://api.tiingo.com/tiingo/crypto/prices?tickers={symbol}&startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&resampleFreq={resampleFreq}&token={_tiingoApiKey}"; var response = await _httpClient.GetFromJsonAsync>(url); if (response == null || response.Count == 0 || response[0].priceData == null) { return new List(); } // 取最近 limit 条 var result = response[0].priceData! .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 = "Crypto" }) .ToList(); return result; } catch (Exception ex) { _logger.LogError(ex, $"Error getting crypto historical data for {symbol}"); throw; } } /// /// 转换为 Tiingo 的 resampleFreq /// private string GetTiingoResampleFreq(string timeframe) { return timeframe.ToLower() switch { "1min" => "1min", "5min" => "5min", "15min" => "15min", "1h" => "1hour", "1d" => "daily", "1w" => "weekly", "1m" => "monthly", _ => "daily" }; } /// /// 计算开始日期 /// 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 TiingoPriceResponse { public decimal? tngoLast { get; set; } public decimal? close { get; set; } public decimal? prevClose { get; set; } public DateTime? date { get; set; } } 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; } } internal class TiingoCryptoPriceResponse { public List? priceData { get; set; } } internal class TiingoCryptoBar { 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; } }