using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// RAIN: Rainbow Moving Average
///
///
/// Ten cascaded SMA layers with weighted composite. Layers 1–4 receive
/// weights 5, 4, 3, 2 and layers 5–10 each receive weight 1 (divisor = 20).
/// Each SMA layer uses O(1) running-sum via RingBuffer.
///
/// Calculation: RAIN = (5·SMA₁ + 4·SMA₂ + 3·SMA₃ + 2·SMA₄ + SMA₅ + SMA₆ + SMA₇ + SMA₈ + SMA₉ + SMA₁₀) / 20
///
/// Detailed documentation
[SkipLocalsInit]
public sealed class Rain : AbstractBase
{
private const int LayerCount = 10;
private const double InvTotalWeight = 1.0 / 20.0; // 5+4+3+2+1+1+1+1+1+1 = 20
private readonly int _period;
private readonly Sma[] _layers;
private readonly TValuePublishedHandler _handler;
private ITValuePublisher? _publisher;
private bool _disposed;
// Weights: layers 1-4 get 5,4,3,2; layers 5-10 get 1 each
private static ReadOnlySpan Weights =>
[
5.0, 4.0, 3.0, 2.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0
];
///
/// Creates RAIN with specified period for each SMA layer.
///
/// Lookback window for each SMA layer (must be > 0)
public Rain(int period)
{
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
_period = period;
_layers = new Sma[LayerCount];
for (int i = 0; i < LayerCount; i++)
{
_layers[i] = new Sma(period);
}
Name = $"Rain({period})";
// All 10 layers share the same period; the cascade needs 10*period bars
// for full convergence, but the first layer is "hot" after 'period' bars.
// We define WarmupPeriod as the point where ALL layers are hot.
WarmupPeriod = period * LayerCount;
_handler = Handle;
}
public Rain(ITValuePublisher source, int period) : this(period)
{
_publisher = source;
source.Pub += _handler;
}
public Rain(TSeries source, int period) : this(period)
{
Prime(source.Values);
if (source.Count > 0)
{
Last = new TValue(source.LastTime, Last.Value);
}
_publisher = source;
source.Pub += _handler;
}
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
public override bool IsHot
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get
{
// Hot when all 10 layers have full buffers
for (int i = 0; i < LayerCount; i++)
{
if (!_layers[i].IsHot)
{
return false;
}
}
return true;
}
}
public override void Prime(ReadOnlySpan source, TimeSpan? step = null)
{
if (source.Length == 0)
{
return;
}
// Reset all layers
for (int i = 0; i < LayerCount; i++)
{
_layers[i].Reset();
}
// Prime layer 0 directly
_layers[0].Prime(source);
// For each subsequent layer, compute intermediate series then prime
double[] tempA = ArrayPool.Shared.Rent(source.Length);
double[] tempB = ArrayPool.Shared.Rent(source.Length);
try
{
Span current = tempA.AsSpan(0, source.Length);
Span next = tempB.AsSpan(0, source.Length);
// Compute layer 0 output
Sma.Batch(source, current, _period);
for (int layer = 1; layer < LayerCount; layer++)
{
// Prime this layer from the previous layer's output
_layers[layer].Prime(current);
if (layer < LayerCount - 1)
{
// Compute this layer's output for the next layer
Sma.Batch(current, next, _period);
// Swap buffers
var tmp = current;
current = next;
next = tmp;
}
}
}
finally
{
ArrayPool.Shared.Return(tempA);
ArrayPool.Shared.Return(tempB);
}
// Compute the final RAIN value from all layers' Last values
double result = ComputeWeightedAverage();
Last = new TValue(DateTime.MinValue, result);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
// Cascade through all 10 SMA layers
TValue current = input;
for (int i = 0; i < LayerCount; i++)
{
current = _layers[i].Update(current, isNew);
}
// Weighted average of all 10 layer outputs
double result = ComputeWeightedAverage();
Last = new TValue(input.Time, result);
PubEvent(Last, isNew);
return Last;
}
public override TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List(len);
var v = new List(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
Prime(source.Values);
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public static TSeries Batch(TSeries source, int period)
{
var rain = new Rain(period);
return rain.Update(source);
}
///
/// Calculates RAIN in-place using 10 cascaded SMA passes with weighted average.
/// Uses double-buffered ArrayPool for intermediate layers.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Batch(ReadOnlySpan source, Span output, int period)
{
if (source.Length != output.Length)
{
throw new ArgumentException("Source and output must have the same length", nameof(output));
}
if (period <= 0)
{
throw new ArgumentException("Period must be greater than 0", nameof(period));
}
int len = source.Length;
if (len == 0)
{
return;
}
// We need to store all 10 layer outputs for the weighted average.
// Layers are computed sequentially: layer[i] = SMA(layer[i-1], period).
// We accumulate the weighted sum as we go to minimize memory.
//
// Strategy: keep two temp buffers + accumulate weighted sum into output.
double[] rentA = ArrayPool.Shared.Rent(len);
double[] rentB = ArrayPool.Shared.Rent(len);
try
{
Span current = rentA.AsSpan(0, len);
Span next = rentB.AsSpan(0, len);
ReadOnlySpan weights = Weights;
// Layer 0: SMA(source, period)
Sma.Batch(source, current, period);
// Initialize output with weight[0] * layer[0]
double w0 = weights[0]; // 5.0
for (int i = 0; i < len; i++)
{
output[i] = w0 * current[i];
}
// Layers 1..9
for (int layer = 1; layer < LayerCount; layer++)
{
Sma.Batch(current, next, period);
double w = weights[layer];
// Accumulate weighted contribution
// skipcq: CS-R1140 - FMA accumulation is correct inline; splitting fragments the pipeline
for (int i = 0; i < len; i++)
{
output[i] = Math.FusedMultiplyAdd(w, next[i], output[i]);
}
// Swap for next iteration
var tmp = current;
current = next;
next = tmp;
}
// Final division by total weight (20)
for (int i = 0; i < len; i++)
{
output[i] *= InvTotalWeight;
}
}
finally
{
ArrayPool.Shared.Return(rentA);
ArrayPool.Shared.Return(rentB);
}
}
public static (TSeries Results, Rain Indicator) Calculate(TSeries source, int period)
{
var indicator = new Rain(period);
TSeries results = indicator.Update(source);
return (results, indicator);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double ComputeWeightedAverage()
{
ReadOnlySpan weights = Weights;
// Unrolled weighted sum for 10 layers
double sum = weights[0] * _layers[0].Last.Value;
sum = Math.FusedMultiplyAdd(weights[1], _layers[1].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[2], _layers[2].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[3], _layers[3].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[4], _layers[4].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[5], _layers[5].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[6], _layers[6].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[7], _layers[7].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[8], _layers[8].Last.Value, sum);
sum = Math.FusedMultiplyAdd(weights[9], _layers[9].Last.Value, sum);
return sum * InvTotalWeight;
}
public override void Reset()
{
for (int i = 0; i < LayerCount; i++)
{
_layers[i].Reset();
}
Last = default;
}
protected override void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing && _publisher != null)
{
_publisher.Pub -= _handler;
_publisher = null;
}
_disposed = true;
}
base.Dispose(disposing);
}
}