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 MaTrendCalculator : IStrategyCalculator { private readonly ILogger _logger; private readonly IMarketDataService _marketDataService; public string StrategyType => AssetManager.Models.DTOs.StrategyType.MaTrend; public MaTrendCalculator( 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 MaTrendConfig(); if (positions.Count == 0) { return new StrategySignal { StrategyType = StrategyType, Signal = "HOLD", Reason = "无持仓", GeneratedAt = DateTime.UtcNow }; } var positionSignals = new List(); string overallSignal = "HOLD"; var reasons = new List(); foreach (var position in positions) { cancellationToken.ThrowIfCancellationRequested(); if (position.StockCode == null) { continue; } try { // 获取历史 K 线数据(需要 longPeriod + 1 根) var historicalData = position.AssetType?.ToLower() == "crypto" ? await _marketDataService.GetCryptoHistoricalDataAsync(position.StockCode, "1d", config.LongPeriod + 1) : await _marketDataService.GetStockHistoricalDataAsync(position.StockCode, "1d", config.LongPeriod + 1); if (historicalData.Count < config.LongPeriod) { positionSignals.Add(new PositionSignal { Symbol = position.StockCode, Signal = "HOLD", Reason = $"历史数据不足(需要 {config.LongPeriod} 根,实际 {historicalData.Count} 根)" }); continue; } // 按时间排序 var sortedData = historicalData.OrderBy(d => d.Timestamp).ToList(); var closes = sortedData.Select(d => d.Close).ToList(); // 计算均线 List shortMaList, longMaList; if (config.MaType?.ToUpper() == "EMA") { shortMaList = TechnicalIndicators.CalculateEMA(closes, config.ShortPeriod); longMaList = TechnicalIndicators.CalculateEMA(closes, config.LongPeriod); } else { shortMaList = TechnicalIndicators.CalculateSMA(closes, config.ShortPeriod); longMaList = TechnicalIndicators.CalculateSMA(closes, config.LongPeriod); } // 获取最新和前一个值 var latestShortMa = shortMaList.Last(); var latestLongMa = longMaList.Last(); var prevShortMa = shortMaList.Count >= 2 ? shortMaList[^2] : null; var prevLongMa = longMaList.Count >= 2 ? longMaList[^2] : null; if (!latestShortMa.HasValue || !latestLongMa.HasValue || !prevShortMa.HasValue || !prevLongMa.HasValue) { positionSignals.Add(new PositionSignal { Symbol = position.StockCode, Signal = "HOLD", Reason = "无法计算均线(数据不足)" }); continue; } // 判断金叉/死叉 string signal = "HOLD"; // 金叉:短期均线上穿长期均线(前一个短期 < 长期,现在短期 > 长期) bool goldenCross = prevShortMa < prevLongMa && latestShortMa > latestLongMa; // 死叉:短期均线下穿长期均线(前一个短期 > 长期,现在短期 < 长期) bool deathCross = prevShortMa > prevLongMa && latestShortMa < latestLongMa; if (goldenCross) { signal = "BUY"; overallSignal = "BUY"; } else if (deathCross) { signal = "SELL"; if (overallSignal != "BUY") overallSignal = "SELL"; } reasons.Add($"{position.StockCode}: 短均线({config.ShortPeriod}) {latestShortMa.Value:F2}, 长均线({config.LongPeriod}) {latestLongMa.Value:F2}, 信号 {signal}"); positionSignals.Add(new PositionSignal { Symbol = position.StockCode, Signal = signal, Reason = $"{config.MaType?.ToUpper() ?? "SMA"}({config.ShortPeriod}): {latestShortMa.Value:F2}, {config.MaType?.ToUpper() ?? "SMA"}({config.LongPeriod}): {latestLongMa.Value:F2}" }); } 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 = overallSignal, PositionSignals = positionSignals, Reason = string.Join("; ", reasons), GeneratedAt = DateTime.UtcNow }; } }