using AssetManager.Data; using AssetManager.Data.Repositories; using AssetManager.Models.DTOs; using Microsoft.Extensions.Logging; namespace AssetManager.Infrastructure.Services; /// /// 市场数据服务实现(组合模式) /// 数据源优先级: /// - 实时价格:Yahoo → 腾讯 → Tiingo /// - 历史K线:Yahoo → Tiingo(腾讯历史接口已废弃) /// - 加密货币:OKX /// public class MarketDataService : IMarketDataService { private readonly ILogger _logger; private readonly ITencentMarketService _tencentService; private readonly IYahooMarketService _yahooService; private readonly ITiingoMarketService _tiingoService; private readonly IOkxMarketService _okxService; private readonly IMarketDataRepository _marketDataRepo; public MarketDataService( ILogger logger, ITencentMarketService tencentService, IYahooMarketService yahooService, ITiingoMarketService tiingoService, IOkxMarketService okxService, IMarketDataRepository marketDataRepo) { _logger = logger; _tencentService = tencentService; _yahooService = yahooService; _tiingoService = tiingoService; _okxService = okxService; _marketDataRepo = marketDataRepo; } /// /// 获取实时价格(自动根据资产类型路由到对应数据源) /// public async Task GetPriceAsync(string symbol, string assetType) { _logger.LogInformation("获取实时价格: {Symbol}, 资产类型: {AssetType}", symbol, assetType); // 先查缓存 var cached = await _marketDataRepo.GetPriceCacheAsync(symbol, assetType); if (cached != null) { _logger.LogDebug("缓存命中: {Symbol} {AssetType}, 价格: {Price}", symbol, assetType, cached.Price); return new MarketPriceResponse { Symbol = cached.Symbol, Price = cached.Price, PreviousClose = cached.PreviousClose ?? 0, Timestamp = cached.FetchedAt, AssetType = cached.AssetType }; } // 缓存未命中,调用API MarketPriceResponse response; string source; if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) { response = await _okxService.GetCryptoPriceAsync(symbol); source = "OKX"; } else { // 股票:优先Yahoo财经,失败降级腾讯财经,最后降级 Tiingo try { response = await _yahooService.GetStockPriceAsync(symbol); source = "Yahoo"; } catch (Exception ex) { _logger.LogWarning(ex, "Yahoo财经获取失败,降级使用 腾讯: {Symbol}", symbol); try { response = await _tencentService.GetStockPriceAsync(symbol); source = "Tencent"; } catch (Exception tencentEx) { _logger.LogWarning(tencentEx, "腾讯财经获取失败,降级使用 Tiingo: {Symbol}", symbol); response = await _tiingoService.GetStockPriceAsync(symbol); source = "Tiingo"; } } } // 写入缓存 await SavePriceCacheAsync(symbol, assetType, response, source); return response; } /// /// 获取历史数据(自动根据资产类型路由到对应数据源) /// public async Task> GetHistoricalDataAsync(string symbol, string assetType, string timeframe, int limit) { _logger.LogInformation("获取历史数据: {Symbol}, 资产类型: {AssetType}, 周期: {Timeframe}, 数量: {Limit}", symbol, assetType, timeframe, limit); // 先查缓存 var cachedKlines = await _marketDataRepo.GetKlineCacheAsync(symbol, assetType, timeframe, limit); // 缓存足够,直接返回 if (cachedKlines.Count >= limit) { _logger.LogDebug("历史K线缓存命中: {Symbol} {AssetType} {Timeframe}, 数量: {Count}", symbol, assetType, timeframe, cachedKlines.Count); return cachedKlines.Select(k => new MarketDataResponse { Symbol = k.Symbol, Open = k.Open, High = k.High, Low = k.Low, Close = k.Close, Volume = k.Volume ?? 0, Timestamp = k.Timestamp, AssetType = k.AssetType }).OrderBy(k => k.Timestamp).ToList(); } // 缓存不足,调用API补全 List response; string source; if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase)) { response = await _okxService.GetCryptoHistoricalDataAsync(symbol, timeframe, limit); source = "OKX"; } else { // 股票:优先Yahoo财经,失败降级 Tiingo // 注:腾讯财经历史K线接口已废弃,不再作为降级数据源 try { response = await _yahooService.GetStockHistoricalDataAsync(symbol, timeframe, limit); if (response.Count > 0) { source = "Yahoo"; } else { throw new InvalidOperationException("Yahoo财经返回空数据"); } } catch (Exception ex) { _logger.LogWarning(ex, "Yahoo财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol); response = await _tiingoService.GetStockHistoricalDataAsync(symbol, timeframe, limit); source = "Tiingo"; } } // 批量写入缓存 await SaveKlineCacheAsync(symbol, assetType, timeframe, response, source); return response; } /// /// 获取股票实时价格 /// public async Task GetStockPriceAsync(string symbol) { return await GetPriceAsync(symbol, "Stock"); } /// /// 获取加密货币实时价格 /// public async Task GetCryptoPriceAsync(string symbol) { return await GetPriceAsync(symbol, "Crypto"); } /// /// 获取股票历史数据 /// public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit) { return await GetHistoricalDataAsync(symbol, "Stock", timeframe, limit); } /// /// 获取加密货币历史数据 /// public async Task> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit) { return await GetHistoricalDataAsync(symbol, "Crypto", timeframe, limit); } // ===== 私有辅助方法 ===== private async Task SavePriceCacheAsync(string symbol, string assetType, MarketPriceResponse response, string source) { var cacheEntity = new MarketPriceCache { Symbol = symbol.ToUpper(), AssetType = assetType.ToUpper(), Price = Math.Round(response.Price, 8), PreviousClose = response.PreviousClose > 0 ? Math.Round(response.PreviousClose, 8) : null, Source = source, FetchedAt = DateTime.Now, ExpiredAt = GetCacheExpirationTime(assetType) }; await _marketDataRepo.SavePriceCacheAsync(cacheEntity); _logger.LogDebug("缓存写入: {Symbol} {AssetType}, 过期时间: {ExpiredAt}", symbol, assetType, cacheEntity.ExpiredAt); } private async Task SaveKlineCacheAsync(string symbol, string assetType, string timeframe, List data, string source) { var cacheEntities = data.Select(k => new MarketKlineCache { Symbol = symbol.ToUpper(), AssetType = assetType.ToUpper(), Timeframe = timeframe.ToUpper(), Timestamp = k.Timestamp, Open = k.Open, High = k.High, Low = k.Low, Close = k.Close, Volume = k.Volume, Source = source, FetchedAt = DateTime.Now }).ToList(); await _marketDataRepo.SaveKlineCacheBatchAsync(cacheEntities); _logger.LogDebug("历史K线缓存写入: {Symbol} {AssetType} {Timeframe}, 数量: {Count}", symbol, assetType, timeframe, cacheEntities.Count); } private DateTime GetCacheExpirationTime(string assetType) { return assetType.ToLower() switch { "crypto" => DateTime.Now.AddMinutes(1), _ => DateTime.Now.AddMinutes(5) }; } }