AssetManager.API/AssetManager.Infrastructure/Services/TencentMarketService.cs
OpenClaw Agent 4ce29a1036 refactor: 架构优化 P0-P3
P0 - 安全修复:
- 移除硬编码 API Key,启动时校验必填环境变量

P1 - 高优先级:
- Entity 拆分:Position.cs, Transaction.cs 独立文件
- Controller Facade 封装:IPortfolioFacade 减少依赖注入

P2 - 中优先级:
- Repository 抽象:IPortfolioRepository, IMarketDataRepository
- MarketDataService 拆分:组合模式整合 Tencent/Tiingo/OKX

P3 - 低优先级:
- DTO 命名规范:统一 PascalCase
- 单元测试框架:xUnit + Moq + FluentAssertions
2026-03-15 12:54:05 +00:00

173 lines
6.1 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) || !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;
}
}