AssetManager.API/AssetManager.Infrastructure/StrategyEngine/Calculators/RiskParityCalculator.cs
niannian zheng 2d1fbd37d8 feat: 添加策略引擎实现及相关组件
实现策略引擎核心功能,包括三种策略计算器和相关DTO定义:
1. 添加双均线策略(ma_trend)计算器
2. 添加吊灯止损策略(chandelier_exit)计算器
3. 添加风险平价策略(risk_parity)计算器
4. 定义策略类型常量类和策略配置DTO
5. 实现策略引擎服务接口和扩展方法
6. 更新项目引用和README文档
2026-03-02 14:15:34 +08:00

230 lines
7.7 KiB
C#
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.Select(a => a.Symbol).ToList();
userDefinedWeights = config.Assets.ToDictionary(a => a.Symbol, a => a.TargetWeight);
}
else
{
targetAssets = positions.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 = CalculateCurrentWeights(positions, targetAssets);
// 根据偏差决定是否再平衡
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 Dictionary<string, decimal> CalculateCurrentWeights(List<Position> positions, List<string> targetAssets)
{
var weights = new Dictionary<string, decimal>();
// 计算总价值
var totalValue = positions.Sum(p => p.Shares * p.AvgPrice);
foreach (var symbol in targetAssets)
{
var position = positions.FirstOrDefault(p => p.StockCode == symbol);
if (position != null && totalValue > 0)
{
weights[symbol] = (position.Shares * position.AvgPrice) / totalValue;
}
else
{
weights[symbol] = 0m;
}
}
return weights;
}
}