AssetManager.API/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs
OpenClaw Agent 1977dd609d fix: 请求收益曲线时自动回填历史数据
- GetNavHistoryAsync现在会自动检查是否有历史数据
- 无历史数据时自动调用BackfillNavHistoryInternalAsync
- 拆分内部回填方法,避免重复验证权限
2026-03-13 16:21:31 +00:00

257 lines
8.7 KiB
C#
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.Json;
using AssetManager.Data;
using AssetManager.Infrastructure.Services;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
/// <summary>
/// 风险平价策略计算器
/// </summary>
public class RiskParityCalculator : IStrategyCalculator
{
private readonly IMarketDataService _marketDataService;
private readonly ILogger<RiskParityCalculator> _logger;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.RiskParity;
public RiskParityCalculator(IMarketDataService marketDataService, ILogger<RiskParityCalculator> logger)
{
_marketDataService = marketDataService;
_logger = logger;
}
public async Task<StrategySignal> CalculateAsync(
string configJson,
List<Position> positions,
CancellationToken cancellationToken = default)
{
var config = JsonSerializer.Deserialize<RiskParityConfig>(configJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new RiskParityConfig();
// 确定目标资产列表
var targetAssets = new List<string>();
var userDefinedWeights = new Dictionary<string, decimal>();
if (config.Assets?.Count >0)
{
targetAssets = config.Assets.Where(a => a.Symbol != null).Select(a => a.Symbol!).ToList();
userDefinedWeights = config.Assets.Where(a => a.Symbol != null).ToDictionary(a => a.Symbol!, a => a.TargetWeight);
}
else
{
targetAssets = positions.Where(p => p.StockCode != null).Select(p => p.StockCode!).ToList();
}
if (targetAssets.Count == 0)
{
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "无目标资产",
GeneratedAt = DateTime.UtcNow
};
}
// 计算资产的波动率和目标权重
var assetVolatilities = new Dictionary<string, decimal>();
var positionSignals = new List<PositionSignal>();
foreach (var symbol in targetAssets)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
var volatility = await CalculateVolatilityAsync(symbol, config.LookbackPeriod, cancellationToken);
assetVolatilities[symbol] = volatility;
}
catch (Exception ex)
{
_logger.LogError(ex, "计算 {Symbol} 的波动率失败", symbol);
assetVolatilities[symbol] = 0.2m; // 默认波动率20%
}
}
// 计算风险平价的目标权重,若无则等权初始化
var targetWeights = CalculateRiskParityWeights(assetVolatilities);
// 如果用户定义了权重,则使用用户定义的权重
foreach (var kvp in userDefinedWeights)
{
targetWeights[kvp.Key] = kvp.Value;
}
// 计算当前权重(使用实时市值)
var currentWeights = await CalculateCurrentWeightsAsync(positions, targetAssets, cancellationToken);
// 根据偏差决定是否再平衡
var maxDeviation = 0m;
var needRebalance = false;
var reasons = new List<string>();
foreach (var symbol in targetAssets)
{
var targetWeight = targetWeights.GetValueOrDefault(symbol, 0m);
var currentWeight = currentWeights.GetValueOrDefault(symbol, 0m);
var deviation = Math.Abs(targetWeight - currentWeight);
if (deviation > config.RebalanceThreshold)
{
needRebalance = true;
}
maxDeviation = Math.Max(maxDeviation, deviation);
var positionSignal = new PositionSignal
{
Symbol = symbol,
TargetWeight = targetWeight,
Signal = deviation > config.RebalanceThreshold
? (targetWeight > currentWeight ? "BUY" : "SELL")
: "HOLD",
Reason = $"目标权重: {targetWeight:P2}, 当前权重: {currentWeight:P2}, 偏差: {deviation:P2}"
};
positionSignals.Add(positionSignal);
if (deviation > config.RebalanceThreshold)
{
reasons.Add($"{symbol}: 调整 {(targetWeight > currentWeight ? "+" : "")}{(targetWeight - currentWeight):P2}");
}
}
return new StrategySignal
{
StrategyType = StrategyType,
Signal = needRebalance ? "REBALANCE" : "HOLD",
PositionSignals = positionSignals,
Reason = needRebalance
? $"最大偏差 {maxDeviation:P2} 超过阈值 {config.RebalanceThreshold:P2}。{string.Join("; ", reasons)}"
: $"资产偏差在阈值范围内。(最大偏差: {maxDeviation:P2})",
GeneratedAt = DateTime.UtcNow
};
}
/// <summary>
/// 计算资产波动率(年化标准差)
/// </summary>
private async Task<decimal> CalculateVolatilityAsync(
string symbol,
int lookbackPeriod,
CancellationToken cancellationToken)
{
var historicalData = await _marketDataService.GetStockHistoricalDataAsync(
symbol, "1d", lookbackPeriod + 1);
if (historicalData.Count < 2)
{
throw new InvalidOperationException($"历史数据不足以计算波动率");
}
// 按时间排序并计算日收益率
var sortedData = historicalData.OrderBy(d => d.Timestamp).ToList();
var returns = new List<decimal>();
for (int i = 1; i < sortedData.Count; i++)
{
var dailyReturn = (sortedData[i].Close - sortedData[i - 1].Close) / sortedData[i - 1].Close;
returns.Add(dailyReturn);
}
// 计算标准差
var avgReturn = returns.Average();
var variance = returns.Sum(r => (r - avgReturn) * (r - avgReturn)) / returns.Count;
var dailyStdDev = (decimal)Math.Sqrt((double)variance);
// 年化波动率假设252个交易日
return dailyStdDev * (decimal)Math.Sqrt(252);
}
/// <summary>
/// 计算风险平价权重,若无则等权初始化
/// </summary>
private Dictionary<string, decimal> CalculateRiskParityWeights(Dictionary<string, decimal> volatilities)
{
var weights = new Dictionary<string, decimal>();
// 计算倒数波动率之和
var inverseVolSum = volatilities.Values.Where(v => v > 0).Sum(v => 1m / v);
if (inverseVolSum == 0)
{
// 如果无法计算,采用等权分配
var equalWeight = 1m / volatilities.Count;
foreach (var symbol in volatilities.Keys)
{
weights[symbol] = equalWeight;
}
}
else
{
foreach (var kvp in volatilities)
{
weights[kvp.Key] = kvp.Value > 0 ? (1m / kvp.Value) / inverseVolSum : 0m;
}
}
return weights;
}
/// <summary>
/// 计算当前持仓权重(使用实时市值)
/// </summary>
private async Task<Dictionary<string, decimal>> CalculateCurrentWeightsAsync(
List<Position> positions,
List<string> targetAssets,
CancellationToken cancellationToken)
{
var weights = new Dictionary<string, decimal>();
var marketValues = new Dictionary<string, decimal>();
decimal totalValue = 0;
// 获取每个持仓的实时价格并计算市值
foreach (var position in positions)
{
cancellationToken.ThrowIfCancellationRequested();
if (position.StockCode == null)
{
continue;
}
MarketPriceResponse priceResponse;
if (position.AssetType?.Equals("Crypto", StringComparison.OrdinalIgnoreCase) == true)
{
priceResponse = await _marketDataService.GetCryptoPriceAsync(position.StockCode);
}
else
{
priceResponse = await _marketDataService.GetStockPriceAsync(position.StockCode);
}
decimal marketValue = position.Shares * priceResponse.Price;
marketValues[position.StockCode] = marketValue;
totalValue += marketValue;
}
// 计算权重
foreach (var symbol in targetAssets)
{
if (marketValues.TryGetValue(symbol, out var marketValue) && totalValue > 0)
{
weights[symbol] = marketValue / totalValue;
}
else
{
weights[symbol] = 0m;
}
}
return weights;
}
}