using System.Text.Json;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
///
/// 市场数据服务实现(基于 Tiingo)
///
public class MarketDataService : IMarketDataService
{
private readonly ILogger _logger;
private readonly HttpClient _httpClient;
private readonly string _tiingoApiKey;
///
/// 构造函数
///
/// 日志记录器
/// HTTP 客户端工厂
public MarketDataService(ILogger logger, IHttpClientFactory httpClientFactory)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
// TODO: 从配置读取 Tiingo API Key
_tiingoApiKey = Environment.GetEnvironmentVariable("TIINGO_API_KEY") ?? "bd00fee76d3012b047473078904001b33322cb46";
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_tiingoApiKey}");
}
///
/// 获取股票实时价格
///
/// 股票代码
/// 股票价格信息
public async Task GetStockPriceAsync(string symbol)
{
try
{
_logger.LogInformation($"Requesting stock price for symbol: {symbol}");
// Tiingo 日线最新价格端点(取最近1条)
var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?token={_tiingoApiKey}";
var response = await _httpClient.GetFromJsonAsync>(url);
if (response == null || response.Count == 0)
{
throw new Exception($"No data found for {symbol}");
}
var latest = response[^1];
decimal? prevClose = response.Count >= 2 ? response[^2].close : null;
return new MarketPriceResponse
{
Symbol = symbol,
Price = latest.close ?? 0,
PreviousClose = prevClose ?? 0,
Timestamp = latest.date ?? DateTime.UtcNow,
AssetType = "Stock"
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting stock price for {symbol}");
throw;
}
}
///
/// 获取加密货币实时价格
///
/// 加密货币代码(如 BTCUSD)
/// 加密货币价格信息
public async Task GetCryptoPriceAsync(string symbol)
{
// 待接入 OKX API
throw new NotImplementedException("加密货币数据源待接入 OKX API");
}
///
/// 获取股票历史数据
///
/// 股票代码
/// 时间周期
/// 数据点数量
/// 历史数据列表
public async Task> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)
{
try
{
_logger.LogInformation($"Requesting stock historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}");
var endDate = DateTime.UtcNow;
var startDate = CalculateStartDate(endDate, timeframe, limit);
// Tiingo 历史数据端点(和示例一致)
var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&token={_tiingoApiKey}";
var response = await _httpClient.GetFromJsonAsync>(url);
if (response == null)
{
return new List();
}
// 取最近 limit 条
var result = response
.OrderByDescending(x => x.date)
.Take(limit)
.OrderBy(x => x.date)
.Select(x => new MarketDataResponse
{
Symbol = symbol,
Open = x.open ?? 0,
High = x.high ?? 0,
Low = x.low ?? 0,
Close = x.close ?? 0,
Volume = x.volume ?? 0,
Timestamp = x.date ?? DateTime.UtcNow,
AssetType = "Stock"
})
.ToList();
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting stock historical data for {symbol}");
throw;
}
}
///
/// 获取加密货币历史数据
///
/// 加密货币代码
/// 时间周期
/// 数据点数量
/// 历史数据列表
public async Task> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit)
{
// 待接入 OKX API
throw new NotImplementedException("加密货币数据源待接入 OKX API");
}
///
/// 计算开始日期
///
private DateTime CalculateStartDate(DateTime endDate, string timeframe, int limit)
{
return timeframe.ToLower() switch
{
"1min" => endDate.AddMinutes(-limit),
"5min" => endDate.AddMinutes(-limit * 5),
"15min" => endDate.AddMinutes(-limit * 15),
"1h" => endDate.AddHours(-limit),
"1d" => endDate.AddDays(-limit),
"1w" => endDate.AddDays(-limit * 7),
"1m" => endDate.AddMonths(-limit),
_ => endDate.AddDays(-limit)
};
}
}
// Tiingo 响应模型
internal class TiingoPriceResponse
{
public decimal? tngoLast { get; set; }
public decimal? close { get; set; }
public decimal? prevClose { get; set; }
public DateTime? date { get; set; }
}
internal class TiingoDailyResponse
{
public decimal? open { get; set; }
public decimal? high { get; set; }
public decimal? low { get; set; }
public decimal? close { get; set; }
public decimal? volume { get; set; }
public DateTime? date { get; set; }
}
internal class TiingoCryptoPriceResponse
{
public List? priceData { get; set; }
}
internal class TiingoCryptoBar
{
public decimal? open { get; set; }
public decimal? high { get; set; }
public decimal? low { get; set; }
public decimal? close { get; set; }
public decimal? volume { get; set; }
public DateTime? date { get; set; }
}