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.Net.Http.Json;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using AssetManager.Data;
|
using AssetManager.Data;
|
||||||
using AssetManager.Models.DTOs;
|
using AssetManager.Models.DTOs;
|
||||||
@ -202,7 +203,7 @@ public class MarketDataService : IMarketDataService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取股票历史数据
|
/// 获取股票历史数据(优先使用腾讯财经,免费无限制)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="symbol">股票代码</param>
|
/// <param name="symbol">股票代码</param>
|
||||||
/// <param name="timeframe">时间周期</param>
|
/// <param name="timeframe">时间周期</param>
|
||||||
@ -212,15 +213,137 @@ public class MarketDataService : IMarketDataService
|
|||||||
{
|
{
|
||||||
try
|
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 endDate = DateTime.UtcNow;
|
||||||
var startDate = CalculateStartDate(endDate, timeframe, limit);
|
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 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 response = await GetWithRetryAsync(url);
|
||||||
var data = await response.Content.ReadFromJsonAsync<List<TiingoDailyResponse>>();
|
var data = await response.Content.ReadFromJsonAsync<List<TiingoDailyResponse>>();
|
||||||
|
|
||||||
@ -229,8 +352,7 @@ public class MarketDataService : IMarketDataService
|
|||||||
return new List<MarketDataResponse>();
|
return new List<MarketDataResponse>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 取最近 limit 条
|
return data
|
||||||
var result = data
|
|
||||||
.OrderByDescending(x => x.date)
|
.OrderByDescending(x => x.date)
|
||||||
.Take(limit)
|
.Take(limit)
|
||||||
.OrderBy(x => x.date)
|
.OrderBy(x => x.date)
|
||||||
@ -246,12 +368,10 @@ public class MarketDataService : IMarketDataService
|
|||||||
AssetType = "Stock"
|
AssetType = "Stock"
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, $"Error getting stock historical data for {symbol}");
|
_logger.LogError(ex, "Tiingo 历史数据获取失败: {Symbol}", symbol);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user