diff --git a/AssetManager.Infrastructure/Services/MarketDataService.cs b/AssetManager.Infrastructure/Services/MarketDataService.cs index b69faaf..af50e99 100755 --- a/AssetManager.Infrastructure/Services/MarketDataService.cs +++ b/AssetManager.Infrastructure/Services/MarketDataService.cs @@ -1,6 +1,7 @@ using System.Net.Http.Json; using System.Security.Cryptography; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using AssetManager.Data; using AssetManager.Models.DTOs; @@ -202,7 +203,7 @@ public class MarketDataService : IMarketDataService } /// - /// 获取股票历史数据 + /// 获取股票历史数据(优先使用腾讯财经,免费无限制) /// /// 股票代码 /// 时间周期 @@ -212,15 +213,137 @@ public class MarketDataService : IMarketDataService { try { - _logger.LogInformation($"Requesting stock historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}"); + // 优先使用腾讯财经(免费无限制) + var tencentData = await GetStockHistoricalFromTencentAsync(symbol, timeframe, limit); + if (tencentData.Any()) + { + return tencentData; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "腾讯财经历史数据获取失败,降级使用 Tiingo: {Symbol}", symbol); + } + + // 降级使用 Tiingo + return await GetStockHistoricalFromTiingoAsync(symbol, timeframe, limit); + } + + /// + /// 从腾讯财经获取历史K线数据 + /// + private async Task> GetStockHistoricalFromTencentAsync(string symbol, string timeframe, int limit) + { + try + { + _logger.LogInformation("获取腾讯财经历史数据: {Symbol}", symbol); + + // 注册GBK编码支持 + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + + // 腾讯财经K线接口 + // us{symbol} 表示美股,day/week/month 表示日/周/月K线 + var klineType = timeframe switch + { + "1d" or "daily" => "day", + "1w" or "weekly" => "week", + "1M" or "monthly" => "month", + _ => "day" + }; + + var url = $"http://web.ifzq.gtimg.cn/appstock/app/fqkline/get?_var=kline_{klineType}qfq¶m=us{symbol.ToUpper()},{klineType},,{Math.Min(limit, 320)}&qrcode=1&asrd=1"; + + var responseBytes = await _httpClient.GetByteArrayAsync(url); + var response = Encoding.GetEncoding("GBK").GetString(responseBytes); + + if (string.IsNullOrEmpty(response) || !response.Contains("kline")) + { + _logger.LogWarning("腾讯财经返回无效数据: {Symbol}", symbol); + return new List(); + } + + // 解析JSON + var jsonStart = response.IndexOf('{'); + if (jsonStart < 0) return new List(); + + var jsonStr = response.Substring(jsonStart); + var jsonDoc = JsonDocument.Parse(jsonStr); + var root = jsonDoc.RootElement; + + // 数据路径: data -> us{symbol} -> qfq -> {klineType} + var dataPath = $"us{symbol.ToUpper()}"; + if (!root.TryGetProperty("data", out var data) || + !data.TryGetProperty(dataPath, out var stockData) || + !stockData.TryGetProperty("qfq", out var qfq)) + { + _logger.LogWarning("腾讯财经数据结构异常: {Symbol}", symbol); + return new List(); + } + + if (!qfq.TryGetProperty(klineType, out var klineData) || klineData.ValueKind != JsonValueKind.Array) + { + _logger.LogWarning("腾讯财经无K线数据: {Symbol}", symbol); + return new List(); + } + + var result = new List(); + foreach (var item in klineData.EnumerateArray()) + { + // 数据格式: [日期, 开盘, 收盘, 最高, 最低, 成交量] + var arr = item.EnumerateArray().ToList(); + if (arr.Count < 6) continue; + + var dateStr = arr[0].GetString(); + if (!DateTime.TryParse(dateStr, out var date)) continue; + + var open = arr[1].GetDecimal(); + var close = arr[2].GetDecimal(); + var high = arr[3].GetDecimal(); + var low = arr[4].GetDecimal(); + var volume = arr[5].GetDecimal(); + + if (close <= 0) continue; + + result.Add(new MarketDataResponse + { + Symbol = symbol, + Open = open, + High = high, + Low = low, + Close = close, + Volume = volume, + Timestamp = date, + AssetType = "Stock" + }); + } + + // 按日期排序 + result = result.OrderBy(x => x.Timestamp).TakeLast(limit).ToList(); + _logger.LogInformation("腾讯财经获取 {Symbol} 历史数据 {Count} 条", symbol, result.Count); + + return result; + } + catch (Exception ex) + { + _logger.LogError(ex, "腾讯财经历史数据解析失败: {Symbol}", symbol); + return new List(); + } + } + + /// + /// 从 Tiingo 获取历史数据(降级方案) + /// + private async Task> GetStockHistoricalFromTiingoAsync(string symbol, string timeframe, int limit) + { + try + { + _logger.LogInformation($"Requesting stock historical data from Tiingo: {symbol}"); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); - // Tiingo 历史数据端点(和示例一致) 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 GetWithRetryAsync(url); var data = await response.Content.ReadFromJsonAsync>(); @@ -229,8 +352,7 @@ public class MarketDataService : IMarketDataService return new List(); } - // 取最近 limit 条 - var result = data + return data .OrderByDescending(x => x.date) .Take(limit) .OrderBy(x => x.date) @@ -246,12 +368,10 @@ public class MarketDataService : IMarketDataService AssetType = "Stock" }) .ToList(); - - return result; } catch (Exception ex) { - _logger.LogError(ex, $"Error getting stock historical data for {symbol}"); + _logger.LogError(ex, "Tiingo 历史数据获取失败: {Symbol}", symbol); throw; } }