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;
}
}