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线数据
///
Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit);
}
///
/// 腾讯财经市场数据服务实现(免费无限制,支持盘前盘后)
///
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) || !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;
}
}