feat: 添加腾讯财经历史K线数据接口
- 新增 GetStockHistoricalFromTencentAsync 方法 - 腾讯财经历史数据免费无限制,解决 Tiingo 429 限流问题 - 获取历史数据时优先使用腾讯财经,失败后降级 Tiingo - 支持日/周/月K线数据 - 数据格式: [日期, 开盘, 收盘, 最高, 最低, 成交量]
This commit is contained in:
parent
da86aa43e6
commit
267b0bd6ba
@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取股票历史数据
|
||||
/// 获取股票历史数据(优先使用腾讯财经,免费无限制)
|
||||
/// </summary>
|
||||
/// <param name="symbol">股票代码</param>
|
||||
/// <param name="timeframe">时间周期</param>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从腾讯财经获取历史K线数据
|
||||
/// </summary>
|
||||
private async Task<List<MarketDataResponse>> 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<MarketDataResponse>();
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
var jsonStart = response.IndexOf('{');
|
||||
if (jsonStart < 0) return new List<MarketDataResponse>();
|
||||
|
||||
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<MarketDataResponse>();
|
||||
}
|
||||
|
||||
if (!qfq.TryGetProperty(klineType, out var klineData) || klineData.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
_logger.LogWarning("腾讯财经无K线数据: {Symbol}", symbol);
|
||||
return new List<MarketDataResponse>();
|
||||
}
|
||||
|
||||
var result = new List<MarketDataResponse>();
|
||||
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<MarketDataResponse>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Tiingo 获取历史数据(降级方案)
|
||||
/// </summary>
|
||||
private async Task<List<MarketDataResponse>> 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<List<TiingoDailyResponse>>();
|
||||
|
||||
@ -229,8 +352,7 @@ public class MarketDataService : IMarketDataService
|
||||
return new List<MarketDataResponse>();
|
||||
}
|
||||
|
||||
// 取最近 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;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user