feat(策略引擎): 实现技术指标库、Mock市场数据、吊灯止损/双均线策略、策略信号API

This commit is contained in:
fanfpy 2026-03-05 09:46:33 +00:00
parent fe781db417
commit f442f0cd1b
7 changed files with 674 additions and 14 deletions

View File

@ -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
});
}
}
}

View File

@ -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>();
builder.Services.AddScoped<AssetManager.Infrastructure.Services.IMarketDataService, AssetManager.Infrastructure.Services.MarketDataService>();
// 市场数据服务:开发环境用 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();

View File

@ -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();
}
}

View 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;
}
}

View File

@ -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
};
}

View File

@ -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
};
}

View File

@ -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);
// 计算后续 EMAEMA = (当前值 - 前一个 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;
// 第一个 TRHigh - Low
trList.Add(highs[0] - lows[0]);
// 后续 TRMax(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);
}
// 第一个 ATRTR 的简单平均
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;
}
}