feat: 替换市场数据源为 Tiingo(移除 Alpaca)
This commit is contained in:
parent
b5499ef7fe
commit
14b51e636a
@ -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>
|
||||||
|
|||||||
@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user