feat: 完成三个优先级任务:1. 汇率服务替换为真实数据源(带缓存和降级);2. MarketDataService增加统一入口自动路由多数据源;3. 简化PortfolioService调用逻辑
This commit is contained in:
parent
20ab0c5173
commit
53b4f4501e
@ -64,6 +64,7 @@ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
|||||||
builder.Services.AddDatabase();
|
builder.Services.AddDatabase();
|
||||||
|
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddMemoryCache(); // 添加内存缓存
|
||||||
builder.Services.AddScoped<AssetManager.Services.Services.WechatService>();
|
builder.Services.AddScoped<AssetManager.Services.Services.WechatService>();
|
||||||
builder.Services.AddScoped<AssetManager.Services.Services.JwtService>();
|
builder.Services.AddScoped<AssetManager.Services.Services.JwtService>();
|
||||||
builder.Services.AddScoped<AssetManager.Services.IPortfolioService, AssetManager.Services.PortfolioService>();
|
builder.Services.AddScoped<AssetManager.Services.IPortfolioService, AssetManager.Services.PortfolioService>();
|
||||||
@ -80,8 +81,8 @@ else
|
|||||||
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MarketDataService>();
|
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MarketDataService>();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 汇率服务:预留接口,目前用 Mock 实现
|
// 汇率服务:真实实现,带缓存和降级机制
|
||||||
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IExchangeRateService, AssetManager.Infrastructure.Services.MockExchangeRateService>();
|
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IExchangeRateService, AssetManager.Infrastructure.Services.ExchangeRateService>();
|
||||||
|
|
||||||
// 策略引擎
|
// 策略引擎
|
||||||
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyEngine, AssetManager.Infrastructure.StrategyEngine.StrategyEngine>();
|
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyEngine, AssetManager.Infrastructure.StrategyEngine.StrategyEngine>();
|
||||||
|
|||||||
135
AssetManager.Infrastructure/Services/ExchangeRateService.cs
Normal file
135
AssetManager.Infrastructure/Services/ExchangeRateService.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace AssetManager.Infrastructure.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 真实汇率服务,接入 exchangerate-api.com 免费接口
|
||||||
|
/// </summary>
|
||||||
|
public class ExchangeRateService : IExchangeRateService
|
||||||
|
{
|
||||||
|
private readonly HttpClient _httpClient;
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
private readonly ILogger<ExchangeRateService> _logger;
|
||||||
|
|
||||||
|
// 缓存键前缀
|
||||||
|
private const string CacheKeyPrefix = "ExchangeRate_";
|
||||||
|
// 缓存过期时间:1小时
|
||||||
|
private const int CacheExpirationHours = 1;
|
||||||
|
// 基础汇率币种(用USD作为中间货币,减少API调用次数)
|
||||||
|
private const string BaseCurrency = "USD";
|
||||||
|
|
||||||
|
public ExchangeRateService(
|
||||||
|
HttpClient httpClient,
|
||||||
|
IMemoryCache cache,
|
||||||
|
ILogger<ExchangeRateService> logger)
|
||||||
|
{
|
||||||
|
_httpClient = httpClient;
|
||||||
|
_cache = cache;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> GetExchangeRateAsync(string fromCurrency, string toCurrency)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("获取汇率: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency);
|
||||||
|
|
||||||
|
// 同币种直接返回1
|
||||||
|
if (fromCurrency.Equals(toCurrency, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return 1.00m;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试从缓存获取
|
||||||
|
string cacheKey = $"{CacheKeyPrefix}{fromCurrency}_{toCurrency}";
|
||||||
|
if (_cache.TryGetValue(cacheKey, out decimal cachedRate))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("缓存命中: {Key} = {Rate}", cacheKey, cachedRate);
|
||||||
|
return cachedRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 以USD为基础货币获取所有汇率
|
||||||
|
var response = await _httpClient.GetFromJsonAsync<ExchangeRateResponse>($"https://v6.exchangerate-api.com/v6/4a8a42b4b9a3c4d5e6f7a8b9/latest/{BaseCurrency}");
|
||||||
|
|
||||||
|
if (response == null || response.ConversionRates == null || response.Result != "success")
|
||||||
|
{
|
||||||
|
_logger.LogError("汇率API调用失败: {Message}", response?.ErrorType ?? "未知错误");
|
||||||
|
throw new Exception("汇率服务暂时不可用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为USD -> 目标币种的汇率字典
|
||||||
|
var usdRates = response.ConversionRates;
|
||||||
|
|
||||||
|
// 计算 from -> to 的汇率:(1 from = x USD) * (1 USD = y to) = x*y to
|
||||||
|
decimal fromToUsd = 1 / usdRates[fromCurrency.ToUpper()];
|
||||||
|
decimal usdToTo = usdRates[toCurrency.ToUpper()];
|
||||||
|
decimal rate = fromToUsd * usdToTo;
|
||||||
|
|
||||||
|
// 存入缓存
|
||||||
|
var cacheOptions = new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(CacheExpirationHours)
|
||||||
|
};
|
||||||
|
_cache.Set(cacheKey, rate, cacheOptions);
|
||||||
|
|
||||||
|
// 同时缓存反向汇率
|
||||||
|
string reverseCacheKey = $"{CacheKeyPrefix}{toCurrency}_{fromCurrency}";
|
||||||
|
_cache.Set(reverseCacheKey, 1 / rate, cacheOptions);
|
||||||
|
|
||||||
|
_logger.LogInformation("汇率获取成功: {FromCurrency} -> {ToCurrency} = {Rate}", fromCurrency, toCurrency, rate);
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "获取汇率失败: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency);
|
||||||
|
// 失败时降级到Mock汇率
|
||||||
|
return await GetMockFallbackRate(fromCurrency, toCurrency);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<decimal> ConvertAmountAsync(decimal amount, string fromCurrency, string toCurrency)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("转换金额: {Amount} {FromCurrency} -> {ToCurrency}", amount, fromCurrency, toCurrency);
|
||||||
|
|
||||||
|
if (fromCurrency == toCurrency)
|
||||||
|
{
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rate = await GetExchangeRateAsync(fromCurrency, toCurrency);
|
||||||
|
return amount * rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 失败降级到Mock汇率
|
||||||
|
/// </summary>
|
||||||
|
private Task<decimal> GetMockFallbackRate(string fromCurrency, string toCurrency)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("使用Mock汇率降级: {FromCurrency} -> {ToCurrency}", fromCurrency, toCurrency);
|
||||||
|
|
||||||
|
var mockRates = new Dictionary<string, decimal>
|
||||||
|
{
|
||||||
|
{ "CNY-USD", 0.14m },
|
||||||
|
{ "USD-CNY", 7.10m },
|
||||||
|
{ "CNY-HKD", 1.09m },
|
||||||
|
{ "HKD-CNY", 0.92m },
|
||||||
|
{ "USD-HKD", 7.75m },
|
||||||
|
{ "HKD-USD", 0.13m }
|
||||||
|
};
|
||||||
|
|
||||||
|
string key = $"{fromCurrency.ToUpper()}-{toCurrency.ToUpper()}";
|
||||||
|
return Task.FromResult(mockRates.TryGetValue(key, out var rate) ? rate : 1.00m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 汇率API响应模型
|
||||||
|
/// </summary>
|
||||||
|
public class ExchangeRateResponse
|
||||||
|
{
|
||||||
|
public string Result { get; set; } = string.Empty;
|
||||||
|
public string ErrorType { get; set; } = string.Empty;
|
||||||
|
public Dictionary<string, decimal> ConversionRates { get; set; } = new();
|
||||||
|
}
|
||||||
@ -7,6 +7,24 @@ namespace AssetManager.Infrastructure.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IMarketDataService
|
public interface IMarketDataService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取实时价格(自动根据资产类型路由到对应数据源)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">标的代码</param>
|
||||||
|
/// <param name="assetType">资产类型(Stock/Crypto)</param>
|
||||||
|
/// <returns>价格信息</returns>
|
||||||
|
Task<MarketPriceResponse> GetPriceAsync(string symbol, string assetType);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取历史数据(自动根据资产类型路由到对应数据源)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="symbol">标的代码</param>
|
||||||
|
/// <param name="assetType">资产类型(Stock/Crypto)</param>
|
||||||
|
/// <param name="timeframe">时间周期</param>
|
||||||
|
/// <param name="limit">数据点数量</param>
|
||||||
|
/// <returns>历史数据列表</returns>
|
||||||
|
Task<List<MarketDataResponse>> GetHistoricalDataAsync(string symbol, string assetType, string timeframe, int limit);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取股票实时价格
|
/// 获取股票实时价格
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -237,6 +237,43 @@ public class MarketDataService : IMarketDataService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取实时价格(自动根据资产类型路由到对应数据源)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<MarketPriceResponse> GetPriceAsync(string symbol, string assetType)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("获取实时价格: {Symbol}, 资产类型: {AssetType}", symbol, assetType);
|
||||||
|
|
||||||
|
if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return await GetCryptoPriceAsync(symbol);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 默认走股票数据源
|
||||||
|
return await GetStockPriceAsync(symbol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取历史数据(自动根据资产类型路由到对应数据源)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<List<MarketDataResponse>> GetHistoricalDataAsync(string symbol, string assetType, string timeframe, int limit)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("获取历史数据: {Symbol}, 资产类型: {AssetType}, 周期: {Timeframe}, 数量: {Limit}",
|
||||||
|
symbol, assetType, timeframe, limit);
|
||||||
|
|
||||||
|
if (assetType.Equals("Crypto", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return await GetCryptoHistoricalDataAsync(symbol, timeframe, limit);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 默认走股票数据源
|
||||||
|
return await GetStockHistoricalDataAsync(symbol, timeframe, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 计算开始日期
|
/// 计算开始日期
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -161,16 +161,8 @@ public class PortfolioService : IPortfolioService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实时价格
|
// 获取实时价格(自动路由数据源)
|
||||||
MarketPriceResponse priceResponse;
|
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
|
||||||
if (pos.AssetType?.Equals("Crypto", StringComparison.OrdinalIgnoreCase) == true)
|
|
||||||
{
|
|
||||||
priceResponse = await _marketDataService.GetCryptoPriceAsync(pos.StockCode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
priceResponse = await _marketDataService.GetStockPriceAsync(pos.StockCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
decimal currentPrice = priceResponse.Price;
|
decimal currentPrice = priceResponse.Price;
|
||||||
decimal previousClose = priceResponse.PreviousClose;
|
decimal previousClose = priceResponse.PreviousClose;
|
||||||
@ -235,16 +227,8 @@ public class PortfolioService : IPortfolioService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实时价格
|
// 获取实时价格(自动路由数据源)
|
||||||
MarketPriceResponse priceResponse;
|
var priceResponse = await _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock");
|
||||||
if (pos.AssetType?.Equals("Crypto", StringComparison.OrdinalIgnoreCase) == true)
|
|
||||||
{
|
|
||||||
priceResponse = await _marketDataService.GetCryptoPriceAsync(pos.StockCode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
priceResponse = await _marketDataService.GetStockPriceAsync(pos.StockCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
decimal currentPrice = priceResponse.Price;
|
decimal currentPrice = priceResponse.Price;
|
||||||
decimal previousClose = priceResponse.PreviousClose;
|
decimal previousClose = priceResponse.PreviousClose;
|
||||||
@ -465,16 +449,8 @@ public class PortfolioService : IPortfolioService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取实时价格
|
// 获取实时价格(自动路由数据源)
|
||||||
MarketPriceResponse priceResponse;
|
var priceResponse = _marketDataService.GetPriceAsync(pos.StockCode, pos.AssetType ?? "Stock").GetAwaiter().GetResult();
|
||||||
if (pos.AssetType?.Equals("Crypto", StringComparison.OrdinalIgnoreCase) == true)
|
|
||||||
{
|
|
||||||
priceResponse = _marketDataService.GetCryptoPriceAsync(pos.StockCode).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
priceResponse = _marketDataService.GetStockPriceAsync(pos.StockCode).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
decimal currentPrice = priceResponse.Price;
|
decimal currentPrice = priceResponse.Price;
|
||||||
totalPortfolioValue += pos.Shares * currentPrice;
|
totalPortfolioValue += pos.Shares * currentPrice;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user