feat(策略引擎): 实现技术指标库、Mock市场数据、吊灯止损/双均线策略、策略信号API
This commit is contained in:
parent
fe781db417
commit
f442f0cd1b
@ -1,3 +1,5 @@
|
||||
using AssetManager.Data;
|
||||
using AssetManager.Infrastructure.StrategyEngine;
|
||||
using AssetManager.Models.DTOs;
|
||||
using AssetManager.Models;
|
||||
using AssetManager.Services;
|
||||
@ -15,11 +17,22 @@ public class PortfolioController : ControllerBase
|
||||
{
|
||||
private readonly ILogger<PortfolioController> _logger;
|
||||
private readonly IPortfolioService _portfolioService;
|
||||
private readonly IStrategyEngine _strategyEngine;
|
||||
private readonly IStrategyService _strategyService;
|
||||
private readonly DatabaseService _databaseService;
|
||||
|
||||
public PortfolioController(ILogger<PortfolioController> logger, IPortfolioService portfolioService)
|
||||
public PortfolioController(
|
||||
ILogger<PortfolioController> logger,
|
||||
IPortfolioService portfolioService,
|
||||
IStrategyEngine strategyEngine,
|
||||
IStrategyService strategyService,
|
||||
DatabaseService databaseService)
|
||||
{
|
||||
_logger = logger;
|
||||
_portfolioService = portfolioService;
|
||||
_strategyEngine = strategyEngine;
|
||||
_strategyService = strategyService;
|
||||
_databaseService = databaseService;
|
||||
}
|
||||
|
||||
|
||||
@ -279,4 +292,79 @@ public class PortfolioController : ControllerBase
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取投资组合策略信号
|
||||
/// </summary>
|
||||
/// <param name="id">投资组合ID</param>
|
||||
/// <returns>策略信号</returns>
|
||||
[HttpGet("{id}/signal")]
|
||||
public async Task<ActionResult<ApiResponse<StrategySignal>>> GetPortfolioSignal(string id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return Unauthorized(new ApiResponse<StrategySignal>
|
||||
{
|
||||
code = AssetManager.Models.StatusCodes.Unauthorized,
|
||||
data = null,
|
||||
message = "用户未授权"
|
||||
});
|
||||
}
|
||||
|
||||
_logger.LogInformation("Request to get portfolio signal: {PortfolioId}", id);
|
||||
|
||||
// 1. 获取投资组合
|
||||
var portfolio = _databaseService.GetPortfolioById(id, userId);
|
||||
if (portfolio == null)
|
||||
{
|
||||
return NotFound(new ApiResponse<StrategySignal>
|
||||
{
|
||||
code = AssetManager.Models.StatusCodes.NotFound,
|
||||
data = null,
|
||||
message = "投资组合不存在"
|
||||
});
|
||||
}
|
||||
|
||||
// 2. 获取策略
|
||||
var strategy = _strategyService.GetStrategyById(portfolio.StrategyId, userId);
|
||||
if (strategy == null)
|
||||
{
|
||||
return NotFound(new ApiResponse<StrategySignal>
|
||||
{
|
||||
code = AssetManager.Models.StatusCodes.NotFound,
|
||||
data = null,
|
||||
message = "策略不存在"
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 获取持仓
|
||||
var positions = _databaseService.GetPositionsByPortfolioId(id);
|
||||
|
||||
// 4. 计算策略信号
|
||||
var signal = await _strategyEngine.CalculateSignalAsync(strategy, positions);
|
||||
|
||||
_logger.LogInformation("Portfolio signal retrieved successfully: {PortfolioId}", id);
|
||||
|
||||
return Ok(new ApiResponse<StrategySignal>
|
||||
{
|
||||
code = AssetManager.Models.StatusCodes.Success,
|
||||
data = signal,
|
||||
message = "success"
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting portfolio signal: {PortfolioId}", id);
|
||||
|
||||
return StatusCode(AssetManager.Models.StatusCodes.InternalServerError, new ApiResponse<StrategySignal>
|
||||
{
|
||||
code = AssetManager.Models.StatusCodes.InternalServerError,
|
||||
data = null,
|
||||
message = ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,7 +68,24 @@ builder.Services.AddScoped<AssetManager.Services.Services.WechatService>();
|
||||
builder.Services.AddScoped<AssetManager.Services.Services.JwtService>();
|
||||
builder.Services.AddScoped<AssetManager.Services.IPortfolioService, AssetManager.Services.PortfolioService>();
|
||||
builder.Services.AddScoped<AssetManager.Services.IStrategyService, AssetManager.Services.StrategyService>();
|
||||
|
||||
// 市场数据服务:开发环境用 Mock,生产环境用 Alpaca
|
||||
if (builder.Environment.IsDevelopment())
|
||||
{
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MockMarketDataService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MarketDataService>();
|
||||
}
|
||||
|
||||
// 策略引擎
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyEngine, AssetManager.Infrastructure.StrategyEngine.StrategyEngine>();
|
||||
|
||||
// 策略计算器
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyCalculator, AssetManager.Infrastructure.StrategyEngine.Calculators.ChandelierExitCalculator>();
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyCalculator, AssetManager.Infrastructure.StrategyEngine.Calculators.MaTrendCalculator>();
|
||||
builder.Services.AddScoped<AssetManager.Infrastructure.StrategyEngine.IStrategyCalculator, AssetManager.Infrastructure.StrategyEngine.Calculators.RiskParityCalculator>();
|
||||
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.AddConsole();
|
||||
|
||||
@ -26,4 +26,23 @@ public class DatabaseService
|
||||
{
|
||||
return _db;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据ID获取投资组合(校验用户ID)
|
||||
/// </summary>
|
||||
public Portfolio? GetPortfolioById(string id, string userId)
|
||||
{
|
||||
return _db.Queryable<Portfolio>()
|
||||
.First(p => p.Id == id && p.UserId == userId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据投资组合ID获取持仓列表
|
||||
/// </summary>
|
||||
public List<Position> GetPositionsByPortfolioId(string portfolioId)
|
||||
{
|
||||
return _db.Queryable<Position>()
|
||||
.Where(p => p.PortfolioId == portfolioId)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
115
AssetManager.Infrastructure/Services/MockMarketDataService.cs
Normal file
115
AssetManager.Infrastructure/Services/MockMarketDataService.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using AssetManager.Models.DTOs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace AssetManager.Infrastructure.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Mock 市场数据服务(用于开发测试)
|
||||
/// </summary>
|
||||
public class MockMarketDataService : IMarketDataService
|
||||
{
|
||||
private readonly ILogger<MockMarketDataService> _logger;
|
||||
|
||||
public MockMarketDataService(ILogger<MockMarketDataService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public Task<MarketPriceResponse> GetStockPriceAsync(string symbol)
|
||||
{
|
||||
_logger.LogInformation("Mock 获取股票价格: {Symbol}", symbol);
|
||||
|
||||
// Mock 价格:基于标的代码生成一个稳定的价格
|
||||
decimal basePrice = symbol.GetHashCode() % 1000 + 50;
|
||||
return Task.FromResult(new MarketPriceResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Price = basePrice,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
AssetType = "Stock"
|
||||
});
|
||||
}
|
||||
|
||||
public Task<MarketPriceResponse> GetCryptoPriceAsync(string symbol)
|
||||
{
|
||||
_logger.LogInformation("Mock 获取加密货币价格: {Symbol}", symbol);
|
||||
|
||||
decimal basePrice = symbol.GetHashCode() % 50000 + 10000;
|
||||
return Task.FromResult(new MarketPriceResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Price = basePrice,
|
||||
Timestamp = DateTime.UtcNow,
|
||||
AssetType = "Crypto"
|
||||
});
|
||||
}
|
||||
|
||||
public Task<List<MarketDataResponse>> GetStockHistoricalDataAsync(string symbol, string timeframe, int limit)
|
||||
{
|
||||
_logger.LogInformation("Mock 获取股票历史数据: {Symbol}, {Timeframe}, {Limit}", symbol, timeframe, limit);
|
||||
return Task.FromResult(GenerateMockData(symbol, "Stock", timeframe, limit));
|
||||
}
|
||||
|
||||
public Task<List<MarketDataResponse>> GetCryptoHistoricalDataAsync(string symbol, string timeframe, int limit)
|
||||
{
|
||||
_logger.LogInformation("Mock 获取加密货币历史数据: {Symbol}, {Timeframe}, {Limit}", symbol, timeframe, limit);
|
||||
return Task.FromResult(GenerateMockData(symbol, "Crypto", timeframe, limit));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成 Mock K 线数据(模拟一个上升趋势 + 随机波动)
|
||||
/// </summary>
|
||||
private List<MarketDataResponse> GenerateMockData(string symbol, string assetType, string timeframe, int limit)
|
||||
{
|
||||
var data = new List<MarketDataResponse>();
|
||||
var random = new Random(symbol.GetHashCode()); // 基于 symbol 的稳定随机数
|
||||
|
||||
// 基础价格:基于 symbol 生成
|
||||
decimal basePrice = symbol.GetHashCode() % 500 + 100;
|
||||
DateTime currentTime = DateTime.UtcNow;
|
||||
|
||||
// 根据 timeframe 转换为时间增量
|
||||
TimeSpan increment = timeframe.ToLower() switch
|
||||
{
|
||||
"1min" => TimeSpan.FromMinutes(1),
|
||||
"5min" => TimeSpan.FromMinutes(5),
|
||||
"15min" => TimeSpan.FromMinutes(15),
|
||||
"1h" => TimeSpan.FromHours(1),
|
||||
"1d" => TimeSpan.FromDays(1),
|
||||
"1w" => TimeSpan.FromDays(7),
|
||||
"1m" => TimeSpan.FromDays(30),
|
||||
_ => TimeSpan.FromDays(1)
|
||||
};
|
||||
|
||||
// 倒序生成(从过去到现在)
|
||||
for (int i = limit - 1; i >= 0; i--)
|
||||
{
|
||||
DateTime timestamp = currentTime - increment * i;
|
||||
|
||||
// 模拟上升趋势 + 随机波动
|
||||
decimal trend = (limit - i) * 0.1m; // 上升趋势
|
||||
decimal noise = (decimal)(random.NextDouble() - 0.5) * 2; // ±1 的随机波动
|
||||
decimal close = basePrice + trend + noise;
|
||||
|
||||
// 生成 OHLC
|
||||
decimal open = close + (decimal)(random.NextDouble() - 0.5) * 1;
|
||||
decimal high = Math.Max(open, close) + (decimal)random.NextDouble() * 0.5m;
|
||||
decimal low = Math.Min(open, close) - (decimal)random.NextDouble() * 0.5m;
|
||||
decimal volume = (decimal)(random.NextDouble() * 1000000 + 100000);
|
||||
|
||||
data.Add(new MarketDataResponse
|
||||
{
|
||||
Symbol = symbol,
|
||||
Timestamp = timestamp,
|
||||
Open = open,
|
||||
High = high,
|
||||
Low = low,
|
||||
Close = close,
|
||||
Volume = volume,
|
||||
AssetType = assetType
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,8 @@
|
||||
using AssetManager.Data;
|
||||
using AssetManager.Infrastructure.Services;
|
||||
using AssetManager.Models.DTOs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
|
||||
|
||||
@ -10,12 +12,16 @@ namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
|
||||
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)
|
||||
public ChandelierExitCalculator(
|
||||
ILogger<ChandelierExitCalculator> logger,
|
||||
IMarketDataService marketDataService)
|
||||
{
|
||||
_logger = logger;
|
||||
_marketDataService = marketDataService;
|
||||
}
|
||||
|
||||
public async Task<StrategySignal> CalculateAsync(
|
||||
@ -25,7 +31,7 @@ public class ChandelierExitCalculator : IStrategyCalculator
|
||||
{
|
||||
_logger.LogInformation("计算吊灯止损策略信号");
|
||||
|
||||
var config = System.Text.Json.JsonSerializer.Deserialize<ChandelierExitConfig>(configJson, new System.Text.Json.JsonSerializerOptions
|
||||
var config = JsonSerializer.Deserialize<ChandelierExitConfig>(configJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new ChandelierExitConfig();
|
||||
@ -41,14 +47,94 @@ public class ChandelierExitCalculator : IStrategyCalculator
|
||||
};
|
||||
}
|
||||
|
||||
// 简单实现:返回持有信号
|
||||
await Task.Delay(1, cancellationToken);
|
||||
var positionSignals = new List<PositionSignal>();
|
||||
bool shouldSell = false;
|
||||
var reasons = new List<string>();
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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 = "HOLD",
|
||||
Reason = "吊灯止损策略暂未实现",
|
||||
Signal = shouldSell ? "SELL" : "HOLD",
|
||||
PositionSignals = positionSignals,
|
||||
Reason = string.Join("; ", reasons),
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
using AssetManager.Data;
|
||||
using AssetManager.Infrastructure.Services;
|
||||
using AssetManager.Models.DTOs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
|
||||
|
||||
@ -10,12 +12,16 @@ namespace AssetManager.Infrastructure.StrategyEngine.Calculators;
|
||||
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)
|
||||
public MaTrendCalculator(
|
||||
ILogger<MaTrendCalculator> logger,
|
||||
IMarketDataService marketDataService)
|
||||
{
|
||||
_logger = logger;
|
||||
_marketDataService = marketDataService;
|
||||
}
|
||||
|
||||
public async Task<StrategySignal> CalculateAsync(
|
||||
@ -25,7 +31,7 @@ public class MaTrendCalculator : IStrategyCalculator
|
||||
{
|
||||
_logger.LogInformation("计算均线趋势策略信号");
|
||||
|
||||
var config = System.Text.Json.JsonSerializer.Deserialize<MaTrendConfig>(configJson, new System.Text.Json.JsonSerializerOptions
|
||||
var config = JsonSerializer.Deserialize<MaTrendConfig>(configJson, new JsonSerializerOptions
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
}) ?? new MaTrendConfig();
|
||||
@ -41,14 +47,111 @@ public class MaTrendCalculator : IStrategyCalculator
|
||||
};
|
||||
}
|
||||
|
||||
// 简单实现:返回持有信号
|
||||
await Task.Delay(1, cancellationToken);
|
||||
var positionSignals = new List<PositionSignal>();
|
||||
string overallSignal = "HOLD";
|
||||
var reasons = new List<string>();
|
||||
|
||||
foreach (var position in positions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
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 = "HOLD",
|
||||
Reason = "均线趋势策略暂未实现",
|
||||
Signal = overallSignal,
|
||||
PositionSignals = positionSignals,
|
||||
Reason = string.Join("; ", reasons),
|
||||
GeneratedAt = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,232 @@
|
||||
namespace AssetManager.Infrastructure.StrategyEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 技术指标计算库
|
||||
/// </summary>
|
||||
public static class TechnicalIndicators
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算简单移动平均 (SMA)
|
||||
/// </summary>
|
||||
/// <param name="values">值列表</param>
|
||||
/// <param name="period">周期</param>
|
||||
/// <returns>SMA 列表(长度与输入一致,前 period-1 个为 null)</returns>
|
||||
public static List<decimal?> CalculateSMA(List<decimal> values, int period)
|
||||
{
|
||||
var result = new List<decimal?>();
|
||||
if (values.Count < period)
|
||||
{
|
||||
result.AddRange(values.Select(_ => (decimal?)null));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 前 period-1 个为 null
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
result.Add(null);
|
||||
}
|
||||
|
||||
// 计算第一个 SMA
|
||||
decimal sum = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
sum += values[i];
|
||||
}
|
||||
result.Add(sum / period);
|
||||
|
||||
// 滑动窗口计算后续 SMA
|
||||
for (int i = period; i < values.Count; i++)
|
||||
{
|
||||
sum = sum - values[i - period] + values[i];
|
||||
result.Add(sum / period);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算指数移动平均 (EMA)
|
||||
/// </summary>
|
||||
/// <param name="values">值列表</param>
|
||||
/// <param name="period">周期</param>
|
||||
/// <returns>EMA 列表(长度与输入一致,前 period-1 个为 null)</returns>
|
||||
public static List<decimal?> CalculateEMA(List<decimal> values, int period)
|
||||
{
|
||||
var result = new List<decimal?>();
|
||||
if (values.Count < period)
|
||||
{
|
||||
result.AddRange(values.Select(_ => (decimal?)null));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 前 period-1 个为 null
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
result.Add(null);
|
||||
}
|
||||
|
||||
// 第一个 EMA 使用 SMA
|
||||
decimal sma = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
sma += values[i];
|
||||
}
|
||||
sma /= period;
|
||||
result.Add(sma);
|
||||
|
||||
// 计算后续 EMA:EMA = (当前值 - 前一个 EMA) * 乘数 + 前一个 EMA
|
||||
decimal multiplier = 2.0m / (period + 1);
|
||||
decimal prevEma = sma;
|
||||
|
||||
for (int i = period; i < values.Count; i++)
|
||||
{
|
||||
decimal ema = (values[i] - prevEma) * multiplier + prevEma;
|
||||
result.Add(ema);
|
||||
prevEma = ema;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算真实波幅 (TR)
|
||||
/// </summary>
|
||||
/// <param name="highs">最高价列表</param>
|
||||
/// <param name="lows">最低价列表</param>
|
||||
/// <param name="closes">收盘价列表</param>
|
||||
/// <returns>TR 列表</returns>
|
||||
public static List<decimal> CalculateTR(List<decimal> highs, List<decimal> lows, List<decimal> closes)
|
||||
{
|
||||
var trList = new List<decimal>();
|
||||
if (highs.Count == 0) return trList;
|
||||
|
||||
// 第一个 TR:High - Low
|
||||
trList.Add(highs[0] - lows[0]);
|
||||
|
||||
// 后续 TR:Max(High-Low, High-PrevClose, PrevClose-Low)
|
||||
for (int i = 1; i < highs.Count; i++)
|
||||
{
|
||||
decimal prevClose = closes[i - 1];
|
||||
decimal tr1 = highs[i] - lows[i];
|
||||
decimal tr2 = Math.Abs(highs[i] - prevClose);
|
||||
decimal tr3 = Math.Abs(prevClose - lows[i]);
|
||||
trList.Add(Math.Max(Math.Max(tr1, tr2), tr3));
|
||||
}
|
||||
|
||||
return trList;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算平均真实波幅 (ATR)
|
||||
/// </summary>
|
||||
/// <param name="highs">最高价列表</param>
|
||||
/// <param name="lows">最低价列表</param>
|
||||
/// <param name="closes">收盘价列表</param>
|
||||
/// <param name="period">周期(默认 14)</param>
|
||||
/// <returns>ATR 列表(长度与输入一致,前 period-1 个为 null)</returns>
|
||||
public static List<decimal?> CalculateATR(List<decimal> highs, List<decimal> lows, List<decimal> closes, int period = 14)
|
||||
{
|
||||
var result = new List<decimal?>();
|
||||
var trList = CalculateTR(highs, lows, closes);
|
||||
|
||||
if (trList.Count < period)
|
||||
{
|
||||
result.AddRange(trList.Select(_ => (decimal?)null));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 前 period-1 个为 null
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
result.Add(null);
|
||||
}
|
||||
|
||||
// 第一个 ATR:TR 的简单平均
|
||||
decimal sum = 0;
|
||||
for (int i = 0; i < period; i++)
|
||||
{
|
||||
sum += trList[i];
|
||||
}
|
||||
decimal atr = sum / period;
|
||||
result.Add(atr);
|
||||
|
||||
// 后续 ATR:(前一个 ATR * (period-1) + 当前 TR) / period
|
||||
for (int i = period; i < trList.Count; i++)
|
||||
{
|
||||
atr = (atr * (period - 1) + trList[i]) / period;
|
||||
result.Add(atr);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算滚动窗口最高价
|
||||
/// </summary>
|
||||
/// <param name="values">值列表</param>
|
||||
/// <param name="period">周期</param>
|
||||
/// <returns>滚动最高价列表(长度与输入一致,前 period-1 个为 null)</returns>
|
||||
public static List<decimal?> CalculateHighestHigh(List<decimal> values, int period)
|
||||
{
|
||||
var result = new List<decimal?>();
|
||||
if (values.Count < period)
|
||||
{
|
||||
result.AddRange(values.Select(_ => (decimal?)null));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 前 period-1 个为 null
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
result.Add(null);
|
||||
}
|
||||
|
||||
// 滑动窗口计算
|
||||
for (int i = period - 1; i < values.Count; i++)
|
||||
{
|
||||
decimal max = decimal.MinValue;
|
||||
for (int j = i - period + 1; j <= i; j++)
|
||||
{
|
||||
if (values[j] > max) max = values[j];
|
||||
}
|
||||
result.Add(max);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算滚动窗口最低价
|
||||
/// </summary>
|
||||
/// <param name="values">值列表</param>
|
||||
/// <param name="period">周期</param>
|
||||
/// <returns>滚动最低价列表(长度与输入一致,前 period-1 个为 null)</returns>
|
||||
public static List<decimal?> CalculateLowestLow(List<decimal> values, int period)
|
||||
{
|
||||
var result = new List<decimal?>();
|
||||
if (values.Count < period)
|
||||
{
|
||||
result.AddRange(values.Select(_ => (decimal?)null));
|
||||
return result;
|
||||
}
|
||||
|
||||
// 前 period-1 个为 null
|
||||
for (int i = 0; i < period - 1; i++)
|
||||
{
|
||||
result.Add(null);
|
||||
}
|
||||
|
||||
// 滑动窗口计算
|
||||
for (int i = period - 1; i < values.Count; i++)
|
||||
{
|
||||
decimal min = decimal.MaxValue;
|
||||
for (int j = i - period + 1; j <= i; j++)
|
||||
{
|
||||
if (values[j] < min) min = values[j];
|
||||
}
|
||||
result.Add(min);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user