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

163 lines
6.2 KiB
C#
Executable File

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 MaTrendCalculator : IStrategyCalculator
{
private readonly ILogger<MaTrendCalculator> _logger;
private readonly IMarketDataService _marketDataService;
public string StrategyType => AssetManager.Models.DTOs.StrategyType.MaTrend;
public MaTrendCalculator(
ILogger<MaTrendCalculator> 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<MaTrendConfig>(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<PositionSignal>();
string overallSignal = "HOLD";
var reasons = new List<string>();
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<decimal?> 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
};
}
}