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

146 lines
5.5 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 AssetManager.Data;
using AssetManager.Infrastructure.Services;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
/// <summary>
/// 吊灯止损策略计算器
/// </summary>
public class ChandelierExitCalculator : IStrategyCalculator
{
private readonly ILogger<ChandelierExitCalculator> _logger;
private readonly IMarketDataService _marketDataService;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.ChandelierExit;
public ChandelierExitCalculator(
ILogger<ChandelierExitCalculator> logger,
IMarketDataService marketDataService)
{
_logger = logger;
_marketDataService = marketDataService;
}
public async Task<StrategySignal> CalculateAsync(
string configJson,
List<Position> positions,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("计算吊灯止损策略信号");
var config = JsonSerializer.Deserialize<ChandelierExitConfig>(configJson, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
}) ?? new ChandelierExitConfig();
if (positions.Count == 0)
{
return new StrategySignal
{
StrategyType = StrategyType,
Signal = "HOLD",
Reason = "无持仓",
GeneratedAt = DateTime.UtcNow
};
}
var positionSignals = new List<PositionSignal>();
bool shouldSell = false;
var reasons = new List<string>();
foreach (var position in positions)
{
cancellationToken.ThrowIfCancellationRequested();
if (position.StockCode == null)
{
continue;
}
try
{
// 获取历史 K 线数据(需要 period + 1 根来计算 ATR
var historicalData = position.AssetType?.ToLower() == "crypto"
? await _marketDataService.GetCryptoHistoricalDataAsync(position.StockCode, "1d", config.Period + 1)
: await _marketDataService.GetStockHistoricalDataAsync(position.StockCode, "1d", config.Period + 1);
if (historicalData.Count < config.Period)
{
positionSignals.Add(new PositionSignal
{
Symbol = position.StockCode,
Signal = "HOLD",
Reason = $"历史数据不足(需要 {config.Period} 根,实际 {historicalData.Count} 根)"
});
continue;
}
// 按时间排序(确保从旧到新)
var sortedData = historicalData.OrderBy(d => d.Timestamp).ToList();
var highs = sortedData.Select(d => d.High).ToList();
var lows = sortedData.Select(d => d.Low).ToList();
var closes = sortedData.Select(d => d.Close).ToList();
// 计算 ATR
var atrList = TechnicalIndicators.CalculateATR(highs, lows, closes, config.Period);
var latestAtr = atrList.Last();
// 计算最高价(最近 period 根)
var priceSeries = config.UseClose ? closes : highs;
var highestHighList = TechnicalIndicators.CalculateHighestHigh(priceSeries, config.Period);
var latestHighestHigh = highestHighList.Last();
if (!latestAtr.HasValue || !latestHighestHigh.HasValue)
{
positionSignals.Add(new PositionSignal
{
Symbol = position.StockCode,
Signal = "HOLD",
Reason = "无法计算指标(数据不足)"
});
continue;
}
// 计算吊灯止损价
decimal stopLossPrice = latestHighestHigh.Value - latestAtr.Value * config.Multiplier;
decimal currentPrice = closes.Last();
// 判断信号:如果当前价格跌破止损价,卖出
string signal = currentPrice < stopLossPrice ? "SELL" : "HOLD";
if (signal == "SELL") shouldSell = true;
reasons.Add($"{position.StockCode}: 止损价 {stopLossPrice:F2}, 当前价 {currentPrice:F2}, 信号 {signal}");
positionSignals.Add(new PositionSignal
{
Symbol = position.StockCode,
Signal = signal,
Reason = $"止损价: {stopLossPrice:F2}, 当前价: {currentPrice:F2}, ATR({config.Period}): {latestAtr.Value:F2}",
SuggestedQuantity = signal == "SELL" ? position.Shares : null
});
}
catch (Exception ex)
{
_logger.LogError(ex, "计算 {Symbol} 的吊灯止损信号失败", position.StockCode);
positionSignals.Add(new PositionSignal
{
Symbol = position.StockCode,
Signal = "HOLD",
Reason = $"计算失败: {ex.Message}"
});
}
}
return new StrategySignal
{
StrategyType = StrategyType,
Signal = shouldSell ? "SELL" : "HOLD",
PositionSignals = positionSignals,
Reason = string.Join("; ", reasons),
GeneratedAt = DateTime.UtcNow
};
}
}