185 lines
6.6 KiB
C#
185 lines
6.6 KiB
C#
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¶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<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;
|
||
}
|
||
}
|