feat: 替换市场数据源为 Tiingo(移除 Alpaca)

This commit is contained in:
虾球 2026-03-06 08:31:55 +00:00
parent b5499ef7fe
commit 14b51e636a
2 changed files with 129 additions and 75 deletions

View File

@ -6,8 +6,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Alpaca.Markets" Version="7.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>

View File

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