feat: 添加策略引擎实现及相关组件

实现策略引擎核心功能,包括三种策略计算器和相关DTO定义:
1. 添加双均线策略(ma_trend)计算器
2. 添加吊灯止损策略(chandelier_exit)计算器
3. 添加风险平价策略(risk_parity)计算器
4. 定义策略类型常量类和策略配置DTO
5. 实现策略引擎服务接口和扩展方法
6. 更新项目引用和README文档
This commit is contained in:
niannian zheng 2026-03-02 14:15:34 +08:00
parent 8e75b894ad
commit 2d1fbd37d8
15 changed files with 810 additions and 4 deletions

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\AssetManager.Models\AssetManager.Models.csproj" />
<ProjectReference Include="..\AssetManager.Data\AssetManager.Data.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,55 @@
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
/// <summary>
/// 吊灯止损策略计算器
/// </summary>
public class ChandelierExitCalculator : IStrategyCalculator
{
private readonly ILogger<ChandelierExitCalculator> _logger;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.ChandelierExit;
public ChandelierExitCalculator(ILogger<ChandelierExitCalculator> logger)
{
_logger = logger;
}
public async Task<StrategySignal> CalculateAsync(
string configJson,
List<Position> positions,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("计算吊灯止损策略信号");
var config = System.Text.Json.JsonSerializer.Deserialize<ChandelierExitConfig>(configJson, new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new ChandelierExitConfig();
if (positions.Count == 0)
{
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "无持仓",
GeneratedAt = DateTime.UtcNow
};
}
// 简单实现:返回持有信号
await Task.Delay(1, cancellationToken);
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "吊灯止损策略暂未实现",
GeneratedAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,55 @@
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
/// <summary>
/// 均线趋势策略计算器
/// </summary>
public class MaTrendCalculator : IStrategyCalculator
{
private readonly ILogger<MaTrendCalculator> _logger;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.MaTrend;
public MaTrendCalculator(ILogger<MaTrendCalculator> logger)
{
_logger = logger;
}
public async Task<StrategySignal> CalculateAsync(
string configJson,
List<Position> positions,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("计算均线趋势策略信号");
var config = System.Text.Json.JsonSerializer.Deserialize<MaTrendConfig>(configJson, new System.Text.Json.JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new MaTrendConfig();
if (positions.Count == 0)
{
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "无持仓",
GeneratedAt = DateTime.UtcNow
};
}
// 简单实现:返回持有信号
await Task.Delay(1, cancellationToken);
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "均线趋势策略暂未实现",
GeneratedAt = DateTime.UtcNow
};
}
}

View File

@ -0,0 +1,230 @@
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;
}
}

View File

@ -0,0 +1,27 @@
using AssetManager.Data;
using AssetManager.Models.DTOs;
namespace AssetManager.Infrastructure.StrategyEngine;
/// <summary>
/// 策略计算器接口
/// </summary>
public interface IStrategyCalculator
{
/// <summary>
/// 策略类型标识
/// </summary>
string StrategyType { get; }
/// <summary>
/// 计算策略信号
/// </summary>
/// <param name="configJson">策略配置JSON</param>
/// <param name="positions">持仓列表</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>策略信号</returns>
Task<StrategySignal> CalculateAsync(
string configJson,
List<Position> positions,
CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,22 @@
using AssetManager.Data;
using AssetManager.Models.DTOs;
namespace AssetManager.Infrastructure.StrategyEngine;
/// <summary>
/// 策略引擎接口
/// </summary>
public interface IStrategyEngine
{
/// <summary>
/// 计算策略信号
/// </summary>
/// <param name="strategy">策略实例</param>
/// <param name="positions">持仓列表</param>
/// <param name="cancellationToken">取消令牌</param>
/// <returns>策略信号</returns>
Task<StrategySignal> CalculateSignalAsync(
Strategy strategy,
List<Position> positions,
CancellationToken cancellationToken = default);
}

View File

@ -0,0 +1,85 @@
using AssetManager.Data;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
namespace AssetManager.Infrastructure.StrategyEngine;
/// <summary>
/// 策略引擎实现
/// </summary>
public class StrategyEngine : IStrategyEngine
{
private readonly Dictionary<string, IStrategyCalculator> _calculators;
private readonly ILogger<StrategyEngine> _logger;
public StrategyEngine(
IEnumerable<IStrategyCalculator> calculators,
ILogger<StrategyEngine> logger)
{
_calculators = calculators.ToDictionary(c => c.StrategyType, c => c);
_logger = logger;
}
public async Task<StrategySignal> CalculateSignalAsync(
Strategy strategy,
List<Position> positions,
CancellationToken cancellationToken = default)
{
_logger.LogInformation(
"开始计算策略信号, 策略ID: {StrategyId}, 类型: {StrategyType}, 持仓数: {PositionCount}",
strategy.Id, strategy.Type, positions.Count);
if (positions.Count == 0)
{
return new StrategySignal
{
StrategyType = strategy.Type,
Signal = "HOLD",
Reason = "无持仓",
GeneratedAt = DateTime.UtcNow
};
}
if (!_calculators.TryGetValue(strategy.Type, out var calculator))
{
_logger.LogWarning("未找到策略类型 {StrategyType} 的计算器", strategy.Type);
return new StrategySignal
{
StrategyType = strategy.Type,
Signal = "HOLD",
Reason = $"不支持的策略类型: {strategy.Type}",
GeneratedAt = DateTime.UtcNow
};
}
try
{
var signal = await calculator.CalculateAsync(
strategy.Config ?? "{}",
positions,
cancellationToken);
_logger.LogInformation(
"策略信号计算完成, 策略ID: {StrategyId}, 信号: {Signal}",
strategy.Id, signal.Signal);
return signal;
}
catch (OperationCanceledException)
{
_logger.LogInformation("策略信号计算被取消, 策略ID: {StrategyId}", strategy.Id);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "策略信号计算失败, 策略ID: {StrategyId}", strategy.Id);
return new StrategySignal
{
StrategyType = strategy.Type,
Signal = "HOLD",
Reason = $"计算失败: {ex.Message}",
GeneratedAt = DateTime.UtcNow
};
}
}
}

View File

@ -0,0 +1,26 @@
using AssetManager.Infrastructure.StrategyEngine.Calculators;
using Microsoft.Extensions.DependencyInjection;
namespace AssetManager.Infrastructure.StrategyEngine;
/// <summary>
/// 策略引擎服务注册扩展
/// </summary>
public static class StrategyEngineExtensions
{
/// <summary>
/// 注册策略引擎及所有计算器
/// </summary>
public static IServiceCollection AddStrategyEngine(this IServiceCollection services)
{
// 注册策略计算器
services.AddScoped<IStrategyCalculator, MaTrendCalculator>();
services.AddScoped<IStrategyCalculator, ChandelierExitCalculator>();
services.AddScoped<IStrategyCalculator, RiskParityCalculator>();
// 注册策略引擎
services.AddScoped<IStrategyEngine, StrategyEngine>();
return services;
}
}

View File

@ -0,0 +1,22 @@
namespace AssetManager.Models.DTOs;
/// <summary>
/// 吊灯止损策略配置
/// </summary>
public class ChandelierExitConfig
{
/// <summary>
/// 周期(通常为 22
/// </summary>
public int Period { get; set; } = 22;
/// <summary>
/// ATR 倍数(通常为 3.0
/// </summary>
public decimal Multiplier { get; set; } = 3.0m;
/// <summary>
/// 是否使用收盘价计算false 表示用最高价/最低价)
/// </summary>
public bool UseClose { get; set; } = false;
}

View File

@ -0,0 +1,22 @@
namespace AssetManager.Models.DTOs;
/// <summary>
/// 均线趋势策略配置
/// </summary>
public class MaTrendConfig
{
/// <summary>
/// 均线类型SMA(简单移动平均) / EMA(指数移动平均)
/// </summary>
public string MaType { get; set; } = "SMA";
/// <summary>
/// 短期均线周期
/// </summary>
public int ShortPeriod { get; set; } = 20;
/// <summary>
/// 长期均线周期
/// </summary>
public int LongPeriod { get; set; } = 60;
}

View File

@ -0,0 +1,38 @@
namespace AssetManager.Models.DTOs;
/// <summary>
/// 风险平价策略配置
/// </summary>
public class RiskParityConfig
{
/// <summary>
/// 历史数据回看周期
/// </summary>
public int LookbackPeriod { get; set; } = 60;
/// <summary>
/// 再平衡阈值(偏离度超过 5% 触发再平衡)
/// </summary>
public decimal RebalanceThreshold { get; set; } = 0.05m;
/// <summary>
/// 目标资产列表(可选,不指定则使用当前持仓)
/// </summary>
public List<AssetAllocation> Assets { get; set; }
}
/// <summary>
/// 资产配置项
/// </summary>
public class AssetAllocation
{
/// <summary>
/// 标的代码
/// </summary>
public string Symbol { get; set; }
/// <summary>
/// 目标权重
/// </summary>
public decimal TargetWeight { get; set; }
}

View File

@ -70,7 +70,7 @@ public class CreateStrategyRequest
public string description { get; set; }
public string riskLevel { get; set; }
public List<string> tags { get; set; }
public List<ParameterItem> parameters { get; set; }
public object parameters { get; set; }
public string Title { get; set; }
}
@ -85,7 +85,7 @@ public class UpdateStrategyRequest
{
public string name { get; set; }
public string type { get; set; }
public List<ParameterItem> parameters { get; set; }
public object parameters { get; set; }
}
public class DeleteStrategyResponse

View File

@ -0,0 +1,83 @@
namespace AssetManager.Models.DTOs;
/// <summary>
/// 策略信号
/// </summary>
public class StrategySignal
{
/// <summary>
/// 策略类型: ma_trend / chandelier_exit / risk_parity
/// </summary>
public string StrategyType { get; set; }
/// <summary>
/// 信号: BUY / SELL / HOLD / REBALANCE
/// </summary>
public string Signal { get; set; }
/// <summary>
/// 标的代码
/// </summary>
public string Symbol { get; set; }
/// <summary>
/// 信号强度 (0-1)
/// </summary>
public decimal Strength { get; set; }
/// <summary>
/// 信号原因
/// </summary>
public string Reason { get; set; }
/// <summary>
/// 建议价格
/// </summary>
public decimal? SuggestedPrice { get; set; }
/// <summary>
/// 建议数量
/// </summary>
public decimal? SuggestedQuantity { get; set; }
/// <summary>
/// 信号生成时间
/// </summary>
public DateTime GeneratedAt { get; set; }
/// <summary>
/// 单个标的的信号
/// </summary>
public List<PositionSignal>? PositionSignals { get; set; }
}
/// <summary>
/// 持仓信号
/// </summary>
public class PositionSignal
{
/// <summary>
/// 标的代码
/// </summary>
public string Symbol { get; set; }
/// <summary>
/// 该标的的信号
/// </summary>
public string Signal { get; set; }
/// <summary>
/// 建议数量
/// </summary>
public decimal? SuggestedQuantity { get; set; }
/// <summary>
/// 信号原因
/// </summary>
public string Reason { get; set; }
/// <summary>
/// 目标权重(风险平价策略用)
/// </summary>
public decimal? TargetWeight { get; set; }
}

View File

@ -0,0 +1,22 @@
namespace AssetManager.Models.DTOs;
/// <summary>
/// 策略类型常量
/// </summary>
public static class StrategyType
{
/// <summary>
/// 均线趋势策略
/// </summary>
public const string MaTrend = "ma_trend";
/// <summary>
/// 吊灯止损策略
/// </summary>
public const string ChandelierExit = "chandelier_exit";
/// <summary>
/// 风险平价策略
/// </summary>
public const string RiskParity = "risk_parity";
}

120
README.md
View File

@ -22,4 +22,122 @@ AssetManager
│ ├── AssetManager.Repository # [数据层] SqlSugar 仓储实现, UnitOfWork
│ ├── AssetManager.Models # [实体层] POCO实体 (由数据库自动生成)
│ └── AssetManager.Infrastructure # [基础层] 常用工具类, 外部API调用
└── AssetManager.sln
└── AssetManager.sln
```
## 📊 策略引擎 (Strategy Engine)
### 功能介绍
策略引擎是系统的核心组件,负责根据配置的策略参数计算交易信号。支持以下三种策略类型:
1. **双均线策略 (ma_trend)** - 经典的趋势跟踪策略,通过短期均线和长期均线的交叉产生买卖信号
2. **吊灯止损策略 (chandelier_exit)** - 趋势跟踪止损策略,通过计算最高价/最低价和 ATR平均真实波幅来设置止损止盈位
3. **风险平价策略 (risk_parity)** - 资产配置策略,通过调整各资产权重使每个资产对组合的风险贡献相等
### 策略配置示例
#### 1. 双均线策略 (ma_trend)
```json
{
"maType": "SMA", // 均线类型SMA(简单移动平均) / EMA(指数移动平均)
"shortPeriod": 20, // 短期均线周期
"longPeriod": 60 // 长期均线周期
}
```
#### 2. 吊灯止损策略 (chandelier_exit)
```json
{
"period": 22, // 周期(通常为 22
"multiplier": 3.0, // ATR 倍数(通常为 3.0
"useClose": false // 是否使用收盘价计算false 表示用最高价/最低价)
}
```
#### 3. 风险平价策略 (risk_parity)
```json
{
"lookbackPeriod": 60, // 历史数据回看周期
"rebalanceThreshold": 0.05, // 再平衡阈值(偏离度超过 5% 触发再平衡)
"assets": [ // 目标资产列表(可选,不指定则使用当前持仓)
{ "symbol": "AAPL", "targetWeight": 0.6 },
{ "symbol": "BTC/USD", "targetWeight": 0.4 }
]
}
```
## 📡 API 示例
### 创建策略 API
**请求 URL**: `POST /api/v1/strategies`
**请求体示例**:
#### 1. 创建双均线策略
```json
{
"name": "双均线策略",
"type": "ma_trend",
"description": "经典趋势跟踪策略",
"riskLevel": "medium",
"tags": ["趋势", "均线"],
"parameters": {
"maType": "SMA",
"shortPeriod": 20,
"longPeriod": 60
}
}
```
#### 2. 创建风险平价策略
```json
{
"name": "风险平价策略",
"type": "risk_parity",
"description": "资产配置策略",
"riskLevel": "low",
"tags": ["资产配置", "风险控制"],
"parameters": {
"lookbackPeriod": 60,
"rebalanceThreshold": 0.05,
"assets": [
{ "symbol": "AAPL", "targetWeight": 0.6 },
{ "symbol": "BTC/USD", "targetWeight": 0.4 }
]
}
}
```
#### 3. 创建吊灯止损策略
```json
{
"name": "吊灯止损策略",
"type": "chandelier_exit",
"description": "趋势跟踪止损策略",
"riskLevel": "high",
"tags": ["止损", "趋势"],
"parameters": {
"period": 22,
"multiplier": 3.0,
"useClose": false
}
}
```
**响应示例**:
```json
{
"Id": "12345678-1234-1234-1234-1234567890ab",
"Title": "双均线策略",
"Status": "created"
}
```