实现策略引擎核心功能,包括三种策略计算器和相关DTO定义: 1. 添加双均线策略(ma_trend)计算器 2. 添加吊灯止损策略(chandelier_exit)计算器 3. 添加风险平价策略(risk_parity)计算器 4. 定义策略类型常量类和策略配置DTO 5. 实现策略引擎服务接口和扩展方法 6. 更新项目引用和README文档
230 lines
7.7 KiB
C#
230 lines
7.7 KiB
C#
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;
|
||
}
|
||
} |