AssetManager.API/AssetManager.Infrastructure/Services/TencentMarketService.cs

185 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
using System.Text.Json;
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 腾讯财经市场数据服务接口
/// </summary>
public interface ITencentMarketService
{
/// <summary>
/// 获取股票实时价格
/// </summary>
Task<MarketPriceResponse> GetStockPriceAsync(string symbol);
/// <summary>
/// 获取股票历史K线数据
/// </summary>
Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit);
}
/// <summary>
/// 腾讯财经市场数据服务实现(免费无限制,支持盘前盘后)
/// </summary>
public class TencentMarketService : ITencentMarketService
{
private readonly ILogger<TencentMarketService> _logger;
private readonly HttpClient _httpClient;
public TencentMarketService(ILogger<TencentMarketService> logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
// 注册GBK编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public async Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
{
_logger.LogInformation("腾讯财经获取股票价格: {Symbol}", symbol);
// 腾讯财经美股接口前缀us大写代码
var url = $"http://qt.gtimg.cn/q=us{symbol.ToUpper()}";
var responseBytes = await _httpClient.GetByteArrayAsync(url);
var response = Encoding.GetEncoding("GBK").GetString(responseBytes);
if (string.IsNullOrEmpty(response) || !response.Contains("~"))
{
throw new Exception($"腾讯财经接口返回无效数据,标的: {symbol}");
}
// 解析返回数据
var parts = response.Split('"')[1].Split('~');
if (parts.Length < 36)
{
throw new Exception($"腾讯财经返回字段不足,标的: {symbol}");
}
// 提取字段:[3]=最新价 [4]=昨收价
if (!decimal.TryParse(parts[3], out var currentPrice) || currentPrice <= 0)
{
throw new Exception($"解析最新价失败,标的: {symbol},返回值: {parts[3]}");
}
if (!decimal.TryParse(parts[4], out var prevClose) || prevClose <= 0)
{
prevClose = currentPrice; // 解析失败用当前价当昨收
}
_logger.LogDebug("腾讯财经接口返回 {Symbol}:最新价 {CurrentPrice},昨收价 {PrevClose}",
symbol, currentPrice, prevClose);
return new MarketPriceResponse
{
Symbol = symbol,
Price = currentPrice,
PreviousClose = prevClose,
Timestamp = DateTime.UtcNow,
AssetType = "Stock"
};
}
public async Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)
{
_logger.LogInformation("获取腾讯财经历史数据: {Symbol}", symbol);
// 腾讯财经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&param=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))
{
throw new Exception($"腾讯财经历史数据接口返回空数据,标的: {symbol}");
}
// 解析JSON
var jsonStart = response.IndexOf('{');
if (jsonStart < 0)
{
throw new Exception($"腾讯财经历史数据接口返回非JSON格式标的: {symbol}");
}
var jsonStr = response.Substring(jsonStart);
using var jsonDoc = JsonDocument.Parse(jsonStr);
var root = jsonDoc.RootElement;
// 检查API错误响应
if (root.TryGetProperty("code", out var codeEl) && codeEl.GetInt32() != 0)
{
var errMsg = root.TryGetProperty("msg", out var msgEl) ? msgEl.GetString() : "未知错误";
throw new Exception($"腾讯财经历史数据接口错误: {errMsg},标的: {symbol}");
}
// 检查 data 字段类型(错误响应时 data 是空数组 []
if (!root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object)
{
throw new Exception($"腾讯财经历史数据接口返回无效数据结构,标的: {symbol}");
}
// 数据路径: data -> us{symbol} -> qfq -> {klineType}
var dataPath = $"us{symbol.ToUpper()}";
if (!data.TryGetProperty(dataPath, out var stockData) ||
!stockData.TryGetProperty("qfq", out var qfq))
{
throw new Exception($"腾讯财经历史数据接口无该标的K线数据标的: {symbol}");
}
if (!qfq.TryGetProperty(klineType, out var klineData) || klineData.ValueKind != JsonValueKind.Array)
{
throw new Exception($"腾讯财经历史数据接口K线数据格式异常标的: {symbol}");
}
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;
}
}