diff --git a/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj b/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj index 8dcb99f..6d75fac 100644 --- a/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj +++ b/AssetManager.Infrastructure/AssetManager.Infrastructure.csproj @@ -6,8 +6,8 @@ - + diff --git a/AssetManager.Infrastructure/Services/MarketDataService.cs b/AssetManager.Infrastructure/Services/MarketDataService.cs index 132dbeb..d46ae06 100644 --- a/AssetManager.Infrastructure/Services/MarketDataService.cs +++ b/AssetManager.Infrastructure/Services/MarketDataService.cs @@ -1,30 +1,30 @@ -using Alpaca.Markets; +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 IAlpacaDataClient _dataClient; + private readonly HttpClient _httpClient; + private readonly string _tiingoApiKey; /// /// 构造函数 /// /// 日志记录器 - public MarketDataService(ILogger logger) + /// HTTP 客户端工厂 + public MarketDataService(ILogger logger, IHttpClientFactory httpClientFactory) { _logger = logger; - - // 初始化 Alpaca 客户端 (7.2 版本) - var secretKey = new SecretKey("YOUR_API_KEY", "YOUR_SECRET_KEY"); - - // 使用 Paper Trading 环境进行测试,生产环境请使用 Environments.Live - _dataClient = Environments.Paper.GetAlpacaDataClient(secretKey); + _httpClient = httpClientFactory.CreateClient(); + // TODO: 从配置读取 Tiingo API Key + _tiingoApiKey = Environment.GetEnvironmentVariable("TIINGO_API_KEY") ?? "YOUR_TIINGO_API_KEY"; + _httpClient.DefaultRequestHeaders.Add("Authorization", $"Token {_tiingoApiKey}"); } /// @@ -38,14 +38,22 @@ public class MarketDataService : IMarketDataService { _logger.LogInformation($"Requesting stock price for symbol: {symbol}"); - var request = new LatestMarketDataRequest(symbol); - var latestTrade = await _dataClient.GetLatestTradeAsync(request); + // Tiingo 最新价格端点 + var url = $"https://api.tiingo.com/iex/{symbol}?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[0]; return new MarketPriceResponse { Symbol = symbol, - Price = latestTrade.Price, - Timestamp = latestTrade.TimestampUtc, + Price = latest.tngoLast ?? latest.close ?? 0, + PreviousClose = latest.prevClose ?? 0, + Timestamp = latest.date ?? DateTime.UtcNow, AssetType = "Stock" }; } @@ -59,7 +67,7 @@ public class MarketDataService : IMarketDataService /// /// 获取加密货币实时价格 /// - /// 加密货币代码 + /// 加密货币代码(如 BTCUSD) /// 加密货币价格信息 public async Task GetCryptoPriceAsync(string symbol) { @@ -67,14 +75,22 @@ public class MarketDataService : IMarketDataService { _logger.LogInformation($"Requesting crypto price for symbol: {symbol}"); - var request = new LatestMarketDataRequest(symbol); - var latestTrade = await _dataClient.GetLatestTradeAsync(request); + // Tiingo 加密货币最新价格端点 + var url = $"https://api.tiingo.com/tiingo/crypto/prices?tickers={symbol}&token={_tiingoApiKey}"; + var response = await _httpClient.GetFromJsonAsync>(url); + if (response == null || response.Count == 0 || response[0].priceData == null || response[0].priceData.Count == 0) + { + throw new Exception($"No data found for {symbol}"); + } + + var latest = response[0].priceData[0]; return new MarketPriceResponse { Symbol = symbol, - Price = latestTrade.Price, - Timestamp = latestTrade.TimestampUtc, + Price = latest.close ?? 0, + PreviousClose = 0, // Tiingo crypto 端点没有 prevClose,暂时用 0 + Timestamp = latest.date ?? DateTime.UtcNow, AssetType = "Crypto" }; } @@ -98,32 +114,37 @@ public class MarketDataService : IMarketDataService { _logger.LogInformation($"Requesting stock historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}"); - var barTimeFrame = GetBarTimeFrame(timeframe); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); + var resampleFreq = GetTiingoResampleFreq(timeframe); - var request = new HistoricalBarsRequest(symbol, startDate, endDate, barTimeFrame); - var barsPage = await _dataClient.GetHistoricalBarsAsync(request); + // Tiingo 历史数据端点 + var url = $"https://api.tiingo.com/tiingo/daily/{symbol}/prices?startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&resampleFreq={resampleFreq}&token={_tiingoApiKey}"; + var response = await _httpClient.GetFromJsonAsync>(url); - var result = new List(); - foreach (var kvp in barsPage.Items) + if (response == null) { - foreach (var bar in kvp.Value) - { - result.Add(new MarketDataResponse - { - Symbol = symbol, - Open = bar.Open, - High = bar.High, - Low = bar.Low, - Close = bar.Close, - Volume = bar.Volume, - Timestamp = bar.TimeUtc, - AssetType = "Stock" - }); - } + 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) @@ -146,32 +167,37 @@ public class MarketDataService : IMarketDataService { _logger.LogInformation($"Requesting crypto historical data for symbol: {symbol}, timeframe: {timeframe}, limit: {limit}"); - var barTimeFrame = GetBarTimeFrame(timeframe); var endDate = DateTime.UtcNow; var startDate = CalculateStartDate(endDate, timeframe, limit); + var resampleFreq = GetTiingoResampleFreq(timeframe); - var request = new HistoricalBarsRequest(symbol, startDate, endDate, barTimeFrame); - var barsPage = await _dataClient.GetHistoricalBarsAsync(request); + // Tiingo 加密货币历史数据端点 + var url = $"https://api.tiingo.com/tiingo/crypto/prices?tickers={symbol}&startDate={startDate:yyyy-MM-dd}&endDate={endDate:yyyy-MM-dd}&resampleFreq={resampleFreq}&token={_tiingoApiKey}"; + var response = await _httpClient.GetFromJsonAsync>(url); - var result = new List(); - foreach (var kvp in barsPage.Items) + if (response == null || response.Count == 0 || response[0].priceData == null) { - foreach (var bar in kvp.Value) - { - result.Add(new MarketDataResponse - { - Symbol = symbol, - Open = bar.Open, - High = bar.High, - Low = bar.Low, - Close = bar.Close, - Volume = bar.Volume, - Timestamp = bar.TimeUtc, - AssetType = "Crypto" - }); - } + return new List(); } + // 取最近 limit 条 + var result = response[0].priceData! + .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 = "Crypto" + }) + .ToList(); + return result; } catch (Exception ex) @@ -182,32 +208,26 @@ public class MarketDataService : IMarketDataService } /// - /// 转换时间周期 + /// 转换为 Tiingo 的 resampleFreq /// - /// 时间周期字符串 - /// BarTimeFrame 对象 - private BarTimeFrame GetBarTimeFrame(string timeframe) + private string GetTiingoResampleFreq(string timeframe) { return timeframe.ToLower() switch { - "1min" => BarTimeFrame.Minute, - "5min" => BarTimeFrame.Minute, - "15min" => BarTimeFrame.Minute, - "1h" => BarTimeFrame.Hour, - "1d" => BarTimeFrame.Day, - "1w" => BarTimeFrame.Week, - "1m" => BarTimeFrame.Month, - _ => BarTimeFrame.Day + "1min" => "1min", + "5min" => "5min", + "15min" => "15min", + "1h" => "1hour", + "1d" => "daily", + "1w" => "weekly", + "1m" => "monthly", + _ => "daily" }; } /// /// 计算开始日期 /// - /// 结束日期 - /// 时间周期 - /// 数据点数量 - /// 开始日期 private DateTime CalculateStartDate(DateTime endDate, string timeframe, int limit) { return timeframe.ToLower() switch @@ -222,4 +242,38 @@ public class MarketDataService : IMarketDataService _ => endDate.AddDays(-limit) }; } -} \ No newline at end of file +} + +// 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; } +}