- GetNavHistoryAsync现在会自动检查是否有历史数据 - 无历史数据时自动调用BackfillNavHistoryInternalAsync - 拆分内部回填方法,避免重复验证权限
146 lines
5.5 KiB
C#
Executable File
146 lines
5.5 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 ChandelierExitCalculator : IStrategyCalculator
|
||
{
|
||
private readonly ILogger<ChandelierExitCalculator> _logger;
|
||
private readonly IMarketDataService _marketDataService;
|
||
|
||
public string StrategyType => AssetManager.Models.DTOs.StrategyType.ChandelierExit;
|
||
|
||
public ChandelierExitCalculator(
|
||
ILogger<ChandelierExitCalculator> 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<ChandelierExitConfig>(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<PositionSignal>();
|
||
bool shouldSell = false;
|
||
var reasons = new List<string>();
|
||
|
||
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
|
||
};
|
||
}
|
||
} |