using AssetManager.Data;
using AssetManager.Infrastructure.Services;
using AssetManager.Models.DTOs;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
///
/// 吊灯止损策略计算器
///
public class ChandelierExitCalculator : IStrategyCalculator
{
private readonly ILogger _logger;
private readonly IMarketDataService _marketDataService;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.ChandelierExit;
public ChandelierExitCalculator(
ILogger logger,
IMarketDataService marketDataService)
{
_logger = logger;
_marketDataService = marketDataService;
}
public async Task CalculateAsync(
string configJson,
List positions,
CancellationToken cancellationToken = default)
{
_logger.LogInformation("计算吊灯止损策略信号");
var config = JsonSerializer.Deserialize(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();
bool shouldSell = false;
var reasons = new List();
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
};
}
}