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; }
+}