- GetNavHistoryAsync现在会自动检查是否有历史数据 - 无历史数据时自动调用BackfillNavHistoryInternalAsync - 拆分内部回填方法,避免重复验证权限
163 lines
6.2 KiB
C#
Executable File
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
|
|
};
|
|
}
|
|
} |