using System.Text;
using System.Text.Json;
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
///
/// 腾讯财经市场数据服务接口
///
public interface ITencentMarketService
{
///
/// 获取股票实时价格
///
Task GetStockPriceAsync(string symbol);
///
/// 获取股票历史K线数据
/// ⚠️ 注意:腾讯历史K线接口 (web.ifzq.gtimg.cn) 已废弃,此方法会抛出异常
///
[Obsolete("腾讯历史K线接口已废弃,请使用 Yahoo 或 Tiingo")]
Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit);
}
///
/// 腾讯财经市场数据服务实现
/// 实时价格接口 (qt.gtimg.cn) 正常可用
/// 历史K线接口 (web.ifzq.gtimg.cn) 已废弃
///
public class TencentMarketService : ITencentMarketService
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
public TencentMarketService(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
// 注册GBK编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
public async Task 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> 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¶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))
{
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();
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;
}
}