AssetManager.API/AssetManager.Infrastructure/Services/MarketDataService.cs

353 lines
12 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.Net.Http.Json;
using System.Text.Json;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.Services;
/// <summary>
/// 市场数据服务实现(基于 Tiingo
/// </summary>
public class MarketDataService : IMarketDataService
{
private readonly ILogger<MarketDataService> _logger;
private readonly HttpClient _httpClient;
private readonly string _tiingoApiKey;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="logger">日志记录器</param>
/// <param name="httpClientFactory">HTTP 客户端工厂</param>
public MarketDataService(ILogger<MarketDataService> logger, IHttpClientFactory httpClientFactory, IConfiguration configuration)
{
_logger = logger;
_httpClient = httpClientFactory.CreateClient();
// 从配置读取 Tiingo API Key优先环境变量
_tiingoApiKey = Environment.GetEnvironmentVariable("TIINGO_API_KEY")
?? configuration["Tiingo:ApiKey"]
?? "bd00fee76d3012b047473078904001b33322cb46";
_httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_tiingoApiKey}");
}
/// <summary>
/// 获取股票实时价格
/// </summary>
/// <param name="symbol">股票代码</param>
/// <returns>股票价格信息</returns>
public async Task<MarketPriceResponse> 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<List<TiingoDailyResponse>>(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;
}
}
/// <summary>
/// 获取加密货币实时价格
/// </summary>
/// <param name="symbol">加密货币代码(如 BTC-USDT</param>
/// <returns>加密货币价格信息</returns>
public async Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol)
{
try
{
_logger.LogInformation($"Requesting crypto price for symbol: {symbol}");
// OKX 实时 ticker 接口symbol 格式如 BTC-USDT
var url = $"https://www.okx.com/api/v5/market/ticker?instId={symbol}";
var response = await _httpClient.GetFromJsonAsync<OkxTickerResponse>(url);
if (response == null || response.code != "0" || response.data == null || response.data.Count == 0)
{
throw new Exception($"No data found for {symbol}, code: {response?.code}, msg: {response?.msg}");
}
var latest = response.data[0];
return new MarketPriceResponse
{
Symbol = symbol,
Price = decimal.TryParse(latest.last, out var price) ? price : 0,
PreviousClose = decimal.TryParse(latest.sodUtc0, out var prevClose) ? prevClose : 0, // UTC0 开盘价作为昨日收盘价
Timestamp = DateTime.UtcNow,
AssetType = "Crypto"
};
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting crypto price for {symbol}");
throw;
}
}
/// <summary>
/// 获取股票历史数据
/// </summary>
/// <param name="symbol">股票代码</param>
/// <param name="timeframe">时间周期</param>
/// <param name="limit">数据点数量</param>
/// <returns>历史数据列表</returns>
public async Task<List<MarketDataResponse>> 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<List<TiingoDailyResponse>>(url);
if (response == null)
{
return new List<MarketDataResponse>();
}
// 取最近 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;
}
}
/// <summary>
/// 获取加密货币历史数据
/// </summary>
/// <param name="symbol">加密货币代码(如 BTC-USDT</param>
/// <param name="timeframe">时间周期</param>
/// <param name="limit">数据点数量</param>
/// <returns>历史数据列表</returns>
public async Task<List<MarketDataResponse>> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit)
{
try
{
_logger.LogInformation($"Requesting crypto historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}");
// 转换 timeframe 为 OKX 支持的粒度
var bar = ConvertToOkxBar(timeframe);
// OKX K线接口
var url = $"https://www.okx.com/api/v5/market/candles?instId={symbol}&bar={bar}&limit={limit}";
var response = await _httpClient.GetFromJsonAsync<OkxCandlesResponse>(url);
if (response == null || response.code != "0" || response.data == null)
{
throw new Exception($"No data found for {symbol}, code: {response?.code}, msg: {response?.msg}");
}
var result = new List<MarketDataResponse>();
foreach (var item in response.data)
{
if (item.Length < 6) continue;
if (long.TryParse(item[0], out var ts) &&
decimal.TryParse(item[1], out var open) &&
decimal.TryParse(item[2], out var high) &&
decimal.TryParse(item[3], out var low) &&
decimal.TryParse(item[4], out var close) &&
decimal.TryParse(item[5], out var volume))
{
result.Add(new MarketDataResponse
{
Symbol = symbol,
Open = open,
High = high,
Low = low,
Close = close,
Volume = volume,
Timestamp = DateTimeOffset.FromUnixTimeMilliseconds(ts).UtcDateTime,
AssetType = "Crypto"
});
}
}
// 按时间升序排列
result = result.OrderBy(x => x.Timestamp).ToList();
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Error getting crypto historical data for {symbol}");
throw;
}
}
/// <summary>
/// 转换 timeframe 为 OKX 支持的 bar 格式
/// </summary>
private string ConvertToOkxBar(string timeframe)
{
return timeframe.ToLower() switch
{
"1min" => "1m",
"5min" => "5m",
"15min" => "15m",
"1h" => "1H",
"4h" => "4H",
"1d" => "1D",
"1w" => "1W",
"1m" => "1M",
_ => "1D"
};
}
/// <summary>
/// 获取实时价格(自动根据资产类型路由到对应数据源)
/// </summary>
public async Task<MarketPriceResponse> GetPriceAsync(string symbol, string assetType)
{
_logger.LogInformation("获取实时价格: {Symbol}, 资产类型: {AssetType}", symbol, assetType);
if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase))
{
return await GetCryptoPriceAsync(symbol);
}
else
{
// 默认走股票数据源
return await GetStockPriceAsync(symbol);
}
}
/// <summary>
/// 获取历史数据(自动根据资产类型路由到对应数据源)
/// </summary>
public async Task<List<MarketDataResponse>> GetHistoricalDataAsync(string symbol, string assetType, string timeframe, int limit)
{
_logger.LogInformation("获取历史数据: {Symbol}, 资产类型: {AssetType}, 周期: {Timeframe}, 数量: {Limit}",
symbol, assetType, timeframe, limit);
if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase))
{
return await GetCryptoHistoricalDataAsync(symbol, timeframe, limit);
}
else
{
// 默认走股票数据源
return await GetStockHistoricalDataAsync(symbol, timeframe, limit);
}
}
/// <summary>
/// 计算开始日期
/// </summary>
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<TiingoCryptoBar>? 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; }
}
// OKX 响应模型
internal class OkxTickerResponse
{
public string code { get; set; }
public string msg { get; set; }
public List<OkxTickerData> data { get; set; }
}
internal class OkxTickerData
{
public string instId { get; set; }
public string last { get; set; }
public string sodUtc0 { get; set; } // UTC0 开盘价,作为昨日收盘价
}
internal class OkxCandlesResponse
{
public string code { get; set; }
public string msg { get; set; }
public List<string[]> data { get; set; }
}