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
145 lines
4.6 KiB
C#
145 lines
4.6 KiB
C#
using System.Net.Http.Json;
|
||
using AssetManager.Models.DTOs;
|
||
using Microsoft.Extensions.Logging;
|
||
|
||
namespace AssetManager.Infrastructure.Services;
|
||
|
||
/// <summary>
|
||
/// OKX 加密货币市场数据服务接口
|
||
/// </summary>
|
||
public interface IOkxMarketService
|
||
{
|
||
/// <summary>
|
||
/// 获取加密货币实时价格
|
||
/// </summary>
|
||
Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol);
|
||
|
||
/// <summary>
|
||
/// 获取加密货币历史K线数据
|
||
/// </summary>
|
||
Task<List<MarketDataResponse>> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit);
|
||
}
|
||
|
||
/// <summary>
|
||
/// OKX 加密货币市场数据服务实现
|
||
/// </summary>
|
||
public class OkxMarketService : IOkxMarketService
|
||
{
|
||
private readonly ILogger<OkxMarketService> _logger;
|
||
private readonly HttpClient _httpClient;
|
||
|
||
public OkxMarketService(ILogger<OkxMarketService> logger, IHttpClientFactory httpClientFactory)
|
||
{
|
||
_logger = logger;
|
||
_httpClient = httpClientFactory.CreateClient();
|
||
}
|
||
|
||
public async Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol)
|
||
{
|
||
_logger.LogInformation("OKX 获取加密货币价格: {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,
|
||
Timestamp = DateTime.UtcNow,
|
||
AssetType = "Crypto"
|
||
};
|
||
}
|
||
|
||
public async Task<List<MarketDataResponse>> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit)
|
||
{
|
||
_logger.LogInformation("OKX 获取历史数据: {Symbol}, 周期: {Timeframe}, 数量: {Limit}", symbol, timeframe, 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"
|
||
});
|
||
}
|
||
}
|
||
|
||
// 按时间升序排列
|
||
return result.OrderBy(x => x.Timestamp).ToList();
|
||
}
|
||
|
||
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"
|
||
};
|
||
}
|
||
}
|
||
|
||
// 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; }
|
||
}
|
||
|
||
internal class OkxCandlesResponse
|
||
{
|
||
public string code { get; set; }
|
||
public string msg { get; set; }
|
||
public List<string[]> data { get; set; }
|
||
}
|