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 }; } }