mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 20:18:05 +00:00
SIMD Refactor: Merge simd-dev into dev (#55)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat> Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
co-authored by
Claude Opus 4.5
aider
Warp
parent
5bcdf8d614
commit
86fe32a682
@@ -0,0 +1,153 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ReluIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ReluIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RELU - Rectified Linear Unit", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_MinHistoryDepths_IsOne()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
Assert.Equal(1, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
Assert.Equal("RELU", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("ReLU", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Close = -5 (negative value should become 0)
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -10, -5);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// ReLU of -5 is 0
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ProcessUpdate_PositiveValue_PassesThrough()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Close = 10 (positive value should pass through)
|
||||
indicator.HistoricalData.AddBar(now, 0, 15, 5, 10);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// ReLU of 10 is 10
|
||||
Assert.Equal(10.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, -2);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 0, 5, 0, 3);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
// ReLU of 3 is 3
|
||||
Assert.Equal(3.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_ProcessUpdate_ZeroValue_ReturnsZero()
|
||||
{
|
||||
var indicator = new ReluIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// ReLU of 0 is 0
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReluIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[]
|
||||
{
|
||||
SourceType.Open,
|
||||
SourceType.High,
|
||||
SourceType.Low,
|
||||
SourceType.Close,
|
||||
SourceType.HL2,
|
||||
SourceType.HLC3,
|
||||
};
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new ReluIndicator { Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 1, 2, 0, 1);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RELU (Rectified Linear Unit) Quantower indicator.
|
||||
/// Applies max(0, x) transformation to input values.
|
||||
/// </summary>
|
||||
public class ReluIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Relu? _relu;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => 1;
|
||||
public override string ShortName => "RELU";
|
||||
|
||||
public ReluIndicator()
|
||||
{
|
||||
Name = "RELU - Rectified Linear Unit";
|
||||
Description = "Applies max(0, x) transformation to input values";
|
||||
SeparateWindow = true;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_relu = new Relu();
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("ReLU", Color.Green, 2, LineStyle.Solid));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_relu == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_relu.Update(input, isNew);
|
||||
|
||||
bool isHot = _relu.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_relu.Last.Value, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ReluTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
Assert.Equal("ReLU", indicator.Name);
|
||||
Assert.Equal(0, indicator.WarmupPeriod);
|
||||
Assert.True(indicator.IsHot); // Always hot (no warmup)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsRelu()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 5.0));
|
||||
Assert.Equal(5.0, indicator.Last.Value, Tolerance); // max(0, 5) = 5
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), -3.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance); // max(0, -3) = 0
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 0.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance); // max(0, 0) = 0
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(3), 100.5));
|
||||
Assert.Equal(100.5, indicator.Last.Value, Tolerance); // max(0, 100.5) = 100.5
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValues()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Positive values pass through
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
Assert.Equal(10.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Negative values become zero
|
||||
indicator.Update(new TValue(time.AddMinutes(1), -10.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Zero stays zero
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 0.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Small positive value
|
||||
indicator.Update(new TValue(time.AddMinutes(3), 0.001));
|
||||
Assert.Equal(0.001, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_CorrectsPreviousValue()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 5.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), -2.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Correct last value
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 3.0), isNew: false);
|
||||
Assert.Equal(3.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrection_RestoresState()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
double[] values = { 5.0, -3.0, 2.5, -1.0, 0.0, 8.0, -5.0 };
|
||||
|
||||
// Process all values
|
||||
foreach (var v in values)
|
||||
{
|
||||
indicator.Update(new TValue(time, v));
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
double finalResult = indicator.Last.Value;
|
||||
|
||||
// Reset and process with corrections
|
||||
indicator.Reset();
|
||||
time = DateTime.UtcNow;
|
||||
foreach (var v in values)
|
||||
{
|
||||
// Submit wrong value first
|
||||
indicator.Update(new TValue(time, 999.0));
|
||||
// Correct it
|
||||
indicator.Update(new TValue(time, v), isNew: false);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
|
||||
Assert.Equal(finalResult, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 7.0));
|
||||
double beforeNaN = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.NaN));
|
||||
Assert.Equal(beforeNaN, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 3.5));
|
||||
double beforeInf = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.Equal(beforeInf, indicator.Last.Value, Tolerance);
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(2), double.NegativeInfinity));
|
||||
Assert.Equal(beforeInf, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), i - 5));
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
indicator.Reset();
|
||||
Assert.True(indicator.IsHot); // Still hot (no warmup)
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
int eventCount = 0;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 5.0));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_Constructor_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Relu(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 5.0), true);
|
||||
Assert.Equal(5.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(1), -3.0), true);
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_MatchesStreaming()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
// Use returns (which can be negative) for meaningful ReLU test
|
||||
var source = Change.Calculate(bars.Close);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Relu();
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming.Update(source[i]);
|
||||
streamingResults.Add(streaming.Last.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batch = Relu.Calculate(source);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batch[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MatchesTSeries()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = Change.Calculate(bars.Close);
|
||||
|
||||
// TSeries batch
|
||||
var batchResult = Relu.Calculate(source);
|
||||
|
||||
// Span calculation
|
||||
var values = source.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Relu.Calculate(values, output);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesArguments()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
Span<double> output = stackalloc double[10];
|
||||
Relu.Calculate(ReadOnlySpan<double>.Empty, output);
|
||||
});
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
ReadOnlySpan<double> source = stackalloc double[10];
|
||||
Span<double> output = stackalloc double[5];
|
||||
Relu.Calculate(source, output);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_PositivePassthrough()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All positive values should pass through unchanged
|
||||
for (double v = 0.1; v <= 100.0; v += 10.0)
|
||||
{
|
||||
indicator.Update(new TValue(time, v));
|
||||
Assert.Equal(v, indicator.Last.Value, Tolerance);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_NegativeBecomesZero()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// All negative values should become zero
|
||||
for (double v = -0.1; v >= -100.0; v -= 10.0)
|
||||
{
|
||||
indicator.Update(new TValue(time, v));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_AlwaysNonNegative()
|
||||
{
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// ReLU output should always be >= 0
|
||||
for (int i = -50; i <= 50; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i + 50), i));
|
||||
Assert.True(indicator.Last.Value >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// ReLU validation tests - validates against known mathematical properties
|
||||
/// since no external library implementations exist for this activation function.
|
||||
/// </summary>
|
||||
public class ReluValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Relu_MathematicalDefinition_Streaming()
|
||||
{
|
||||
// ReLU: f(x) = max(0, x)
|
||||
var indicator = new Relu();
|
||||
var time = DateTime.UtcNow;
|
||||
double[] testValues = { -10.0, -5.0, -1.0, -0.5, 0.0, 0.5, 1.0, 5.0, 10.0 };
|
||||
|
||||
foreach (var x in testValues)
|
||||
{
|
||||
indicator.Update(new TValue(time, x));
|
||||
double expected = Math.Max(0.0, x);
|
||||
Assert.Equal(expected, indicator.Last.Value, Tolerance);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_MathematicalDefinition_Batch()
|
||||
{
|
||||
double[] testValues = { -10.0, -5.0, -1.0, -0.5, 0.0, 0.5, 1.0, 5.0, 10.0 };
|
||||
var source = new TSeries();
|
||||
var time = DateTime.UtcNow;
|
||||
foreach (var v in testValues)
|
||||
{
|
||||
source.Add(new TValue(time, v), true);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
|
||||
var result = Relu.Calculate(source);
|
||||
|
||||
for (int i = 0; i < testValues.Length; i++)
|
||||
{
|
||||
double expected = Math.Max(0.0, testValues[i]);
|
||||
Assert.Equal(expected, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_MathematicalDefinition_Span()
|
||||
{
|
||||
double[] testValues = { -10.0, -5.0, -1.0, -0.5, 0.0, 0.5, 1.0, 5.0, 10.0 };
|
||||
double[] output = new double[testValues.Length];
|
||||
|
||||
Relu.Calculate(testValues, output);
|
||||
|
||||
for (int i = 0; i < testValues.Length; i++)
|
||||
{
|
||||
double expected = Math.Max(0.0, testValues[i]);
|
||||
Assert.Equal(expected, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_Property_NonNegative()
|
||||
{
|
||||
// Property: ReLU output is always >= 0
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.0, sigma: 0.5, seed: 43000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = Change.Calculate(bars.Close);
|
||||
|
||||
var result = Relu.Calculate(source);
|
||||
|
||||
for (int i = 0; i < result.Count; i++)
|
||||
{
|
||||
Assert.True(result[i].Value >= 0, $"ReLU output at index {i} should be non-negative");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_Property_PositivePassthrough()
|
||||
{
|
||||
// Property: For x > 0, ReLU(x) = x
|
||||
double[] positiveValues = { 0.001, 0.1, 1.0, 10.0, 100.0, 1000.0 };
|
||||
double[] output = new double[positiveValues.Length];
|
||||
|
||||
Relu.Calculate(positiveValues, output);
|
||||
|
||||
for (int i = 0; i < positiveValues.Length; i++)
|
||||
{
|
||||
Assert.Equal(positiveValues[i], output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_Property_NegativeZero()
|
||||
{
|
||||
// Property: For x < 0, ReLU(x) = 0
|
||||
double[] negativeValues = { -0.001, -0.1, -1.0, -10.0, -100.0, -1000.0 };
|
||||
double[] output = new double[negativeValues.Length];
|
||||
|
||||
Relu.Calculate(negativeValues, output);
|
||||
|
||||
for (int i = 0; i < negativeValues.Length; i++)
|
||||
{
|
||||
Assert.Equal(0.0, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_Property_ZeroAtZero()
|
||||
{
|
||||
// Property: ReLU(0) = 0
|
||||
var indicator = new Relu();
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 0.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Relu_StreamingVsBatch_Consistency()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.3, seed: 43001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = Change.Calculate(bars.Close);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Relu();
|
||||
var streamingResults = new double[source.Count];
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming.Update(source[i]);
|
||||
streamingResults[i] = streaming.Last.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batch = Relu.Calculate(source);
|
||||
|
||||
// Span
|
||||
var spanOutput = new double[source.Count];
|
||||
Relu.Calculate(source.Values.ToArray(), spanOutput);
|
||||
|
||||
// All three should match
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batch[i].Value, Tolerance);
|
||||
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
// RELU: Rectified Linear Unit
|
||||
// Activation function that returns max(0, x)
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RELU: Rectified Linear Unit
|
||||
/// Applies max(0, x) transformation to input values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Zero for negative inputs, passthrough for positive
|
||||
/// - Commonly used as activation function in neural networks
|
||||
/// - Computationally efficient: simple comparison
|
||||
/// - Non-linear, allowing networks to learn complex patterns
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Relu : AbstractBase
|
||||
{
|
||||
private record struct State(double LastValid);
|
||||
private State _state, _p_state;
|
||||
private readonly ITValuePublisher? _source;
|
||||
private readonly TValuePublishedHandler? _handler;
|
||||
|
||||
public override bool IsHot => true; // No warmup needed
|
||||
|
||||
public Relu()
|
||||
{
|
||||
Name = "ReLU";
|
||||
WarmupPeriod = 0;
|
||||
}
|
||||
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
public Relu(ITValuePublisher source) : this()
|
||||
{
|
||||
_source = source;
|
||||
_handler = HandleUpdate;
|
||||
_source.Pub += _handler;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _source != null && _handler != null)
|
||||
{
|
||||
_source.Pub -= _handler;
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
_p_state = _state;
|
||||
else
|
||||
_state = _p_state;
|
||||
|
||||
double value = input.Value;
|
||||
double result;
|
||||
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
result = Math.Max(0.0, value);
|
||||
_state = new State(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _state.LastValid;
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return new TSeries([], []);
|
||||
|
||||
int len = source.Count;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
System.Runtime.InteropServices.CollectionsMarshal.SetCount(t, len);
|
||||
System.Runtime.InteropServices.CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(v);
|
||||
|
||||
// Use vectorized Calculate for batch processing
|
||||
Calculate(source.Values, vSpan);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state from last value
|
||||
if (len > 0 && double.IsFinite(vSpan[len - 1]))
|
||||
{
|
||||
_state = new State(vSpan[len - 1]);
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(time, source[i]), true);
|
||||
time += interval;
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source)
|
||||
{
|
||||
var indicator = new Relu();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates ReLU over a span of values with SIMD optimization.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
throw new ArgumentException("Source cannot be empty", nameof(source));
|
||||
if (output.Length < source.Length)
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
|
||||
double lastValid = 0.0;
|
||||
int i = 0;
|
||||
|
||||
// SIMD path for AVX2
|
||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
||||
{
|
||||
Vector256<double> zero = Vector256<double>.Zero;
|
||||
int simdLength = source.Length - (source.Length % Vector256<double>.Count);
|
||||
|
||||
for (; i < simdLength; i += Vector256<double>.Count)
|
||||
{
|
||||
Vector256<double> vec = Vector256.LoadUnsafe(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(source.Slice(i)));
|
||||
|
||||
// Create mask for finite values (NaN and Infinity comparisons return false)
|
||||
// A value is finite if it equals itself AND is not +/- infinity
|
||||
Vector256<double> isFiniteMask = Avx.And(
|
||||
Avx.Compare(vec, vec, FloatComparisonMode.OrderedEqualNonSignaling),
|
||||
Avx.And(
|
||||
Avx.Compare(vec, Vector256.Create(double.PositiveInfinity), FloatComparisonMode.OrderedNotEqualNonSignaling),
|
||||
Avx.Compare(vec, Vector256.Create(double.NegativeInfinity), FloatComparisonMode.OrderedNotEqualNonSignaling)
|
||||
)
|
||||
);
|
||||
|
||||
// ReLU: max(0, x) for finite values
|
||||
Vector256<double> relu = Avx.Max(zero, vec);
|
||||
|
||||
// Blend: finite lanes get relu result, non-finite lanes get lastValid
|
||||
Vector256<double> lastValidVec = Vector256.Create(lastValid);
|
||||
Vector256<double> result = Avx.BlendVariable(lastValidVec, relu, isFiniteMask);
|
||||
|
||||
result.StoreUnsafe(ref System.Runtime.InteropServices.MemoryMarshal.GetReference(output.Slice(i)));
|
||||
|
||||
// Update lastValid from the last finite element in this vector
|
||||
for (int j = Vector256<double>.Count - 1; j >= 0; j--)
|
||||
{
|
||||
double elem = vec.GetElement(j);
|
||||
if (double.IsFinite(elem))
|
||||
{
|
||||
lastValid = result.GetElement(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
for (; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
double result = Math.Max(0.0, val);
|
||||
lastValid = result;
|
||||
output[i] = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = lastValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
# RELU: Rectified Linear Unit
|
||||
|
||||
> "The simplest non-linearity that works—ReLU's computational efficiency and gradient-friendly properties made deep learning practical."
|
||||
|
||||
The Rectified Linear Unit (ReLU) activation function applies `max(0, x)` to each value, passing positive inputs unchanged while zeroing negative ones. Its simplicity belies its importance: ReLU enabled the training of deep neural networks by mitigating vanishing gradients, and its computational efficiency makes it the default activation for most architectures.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Core Formula
|
||||
|
||||
$$
|
||||
\text{ReLU}(x) = \max(0, x) = \begin{cases} x & \text{if } x > 0 \\ 0 & \text{if } x \leq 0 \end{cases}
|
||||
$$
|
||||
|
||||
### Key Properties
|
||||
|
||||
| Property | Formula | Description |
|
||||
|:---------|:--------|:------------|
|
||||
| **Non-negativity** | $\text{ReLU}(x) \geq 0$ | Output always ≥ 0 |
|
||||
| **Identity for Positives** | $\text{ReLU}(x) = x$ for $x > 0$ | Passthrough for positive values |
|
||||
| **Sparsity Inducing** | $\text{ReLU}(x) = 0$ for $x \leq 0$ | Creates sparse activations |
|
||||
| **Derivative** | $\frac{d}{dx}\text{ReLU}(x) = \mathbf{1}_{x>0}$ | 1 for positive, 0 for negative |
|
||||
| **Scale Equivariance** | $\text{ReLU}(\alpha x) = \alpha \cdot \text{ReLU}(x)$ for $\alpha > 0$ | Positive scaling preserved |
|
||||
|
||||
### Domain and Range
|
||||
|
||||
| | Value |
|
||||
|:--|:--|
|
||||
| **Domain** | $(-\infty, +\infty)$ |
|
||||
| **Range** | $[0, +\infty)$ |
|
||||
|
||||
## Financial Applications
|
||||
|
||||
### Threshold-Based Signals
|
||||
|
||||
Zero out values below a threshold (e.g., only consider positive returns):
|
||||
|
||||
$$
|
||||
\text{PositiveReturns}_t = \text{ReLU}(r_t)
|
||||
$$
|
||||
|
||||
### Asymmetric Risk Metrics
|
||||
|
||||
Compute downside deviation using ReLU on negated returns:
|
||||
|
||||
$$
|
||||
\text{Downside}_t = \text{ReLU}(-r_t)
|
||||
$$
|
||||
|
||||
### Clamping Negative Values
|
||||
|
||||
Ensure non-negative inputs to subsequent calculations:
|
||||
|
||||
$$
|
||||
\text{Volume}_{\text{clamped}} = \text{ReLU}(\text{Volume} - \text{Threshold})
|
||||
$$
|
||||
|
||||
### Neural Network Features
|
||||
|
||||
Pre-processing layer for ML-based trading models where ReLU activation is standard.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### SIMD Optimization
|
||||
|
||||
The implementation uses AVX2 vectorization when available:
|
||||
- Processes 4 doubles per instruction using `Avx.Max`
|
||||
- Falls back to scalar `Math.Max` for remaining elements
|
||||
- Achieves ~4× throughput improvement on compatible hardware
|
||||
|
||||
### NaN Handling
|
||||
|
||||
Non-finite inputs (NaN, ±Infinity) are replaced with the last valid output value, maintaining series continuity.
|
||||
|
||||
### Streaming Characteristics
|
||||
|
||||
| Metric | Value |
|
||||
|:-------|:------|
|
||||
| **Warmup Period** | 0 |
|
||||
| **Memory** | O(1) |
|
||||
| **Complexity** | O(1) per update |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Scalar)
|
||||
|
||||
| Operation | Count | Notes |
|
||||
|:----------|:-----:|:------|
|
||||
| CMP | 1 | Comparison with zero |
|
||||
| MOV | 1 | Conditional move |
|
||||
| **Total** | ~2-3 cycles | Branch-free with CMOV |
|
||||
|
||||
### SIMD Performance (AVX2)
|
||||
|
||||
| Mode | Throughput | Notes |
|
||||
|:-----|:-----------|:------|
|
||||
| Scalar | 1 value/cycle | Single comparison |
|
||||
| AVX2 | 4 values/cycle | `vpmaxpd` instruction |
|
||||
| **Speedup** | ~4× | For aligned batch operations |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
|:-------|:-----:|:------|
|
||||
| **Accuracy** | 10/10 | Exact computation |
|
||||
| **Timeliness** | 10/10 | Zero lag |
|
||||
| **Smoothness** | 7/10 | Discontinuous derivative at origin |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```csharp
|
||||
var relu = new Relu();
|
||||
var input = new TValue(DateTime.UtcNow, -5.0);
|
||||
var result = relu.Update(input); // Returns 0.0
|
||||
|
||||
input = new TValue(DateTime.UtcNow, 3.5);
|
||||
result = relu.Update(input); // Returns 3.5
|
||||
```
|
||||
|
||||
### Filtering Negative Returns
|
||||
|
||||
```csharp
|
||||
var returns = new TSeries();
|
||||
// ... populate with return values
|
||||
|
||||
var relu = new Relu();
|
||||
var positiveReturns = relu.Update(returns);
|
||||
// All negative returns become 0
|
||||
```
|
||||
|
||||
### Batch Processing with SIMD
|
||||
|
||||
```csharp
|
||||
double[] source = { -2.0, -1.0, 0.0, 1.0, 2.0, 3.0 };
|
||||
double[] output = new double[source.Length];
|
||||
|
||||
Relu.Calculate(source.AsSpan(), output.AsSpan());
|
||||
// output: { 0, 0, 0, 1, 2, 3 }
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Dead Neurons**: In neural network contexts, neurons with ReLU can "die" if they receive consistently negative inputs during training—they output zero and have zero gradient.
|
||||
|
||||
2. **Unbounded Output**: Unlike sigmoid, ReLU has no upper bound. Large positive inputs pass through unchanged, potentially causing numerical issues downstream.
|
||||
|
||||
3. **Non-differentiable at Origin**: The derivative is technically undefined at x=0. In practice, implementations choose either 0 or 1; this rarely matters for gradient descent.
|
||||
|
||||
4. **Loss of Negative Information**: ReLU discards all information from negative values. If negative values carry meaningful signals, consider alternatives like LeakyReLU or using the raw values.
|
||||
|
||||
5. **Not Zero-Centered**: ReLU outputs are always non-negative, which can slow convergence in some optimization scenarios.
|
||||
|
||||
## Validation
|
||||
|
||||
| Test | Status |
|
||||
|:-----|:------:|
|
||||
| **Math.Max(0, x) Parity** | ✅ |
|
||||
| **Zero Passthrough** | ✅ |
|
||||
| **Negative → Zero** | ✅ |
|
||||
| **Positive Passthrough** | ✅ |
|
||||
| **SIMD/Scalar Consistency** | ✅ |
|
||||
| **NaN Handling** | ✅ |
|
||||
|
||||
## References
|
||||
|
||||
- Nair, V. & Hinton, G. (2010). "Rectified Linear Units Improve Restricted Boltzmann Machines." *ICML*.
|
||||
- Glorot, X., Bordes, A., & Bengio, Y. (2011). "Deep Sparse Rectifier Neural Networks." *AISTATS*.
|
||||
- Goodfellow, I., Bengio, Y., & Courville, A. (2016). *Deep Learning*. MIT Press.
|
||||
@@ -0,0 +1,25 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Rectified Linear Unit (ReLU)", "ReLU", overlay=false, precision=6)
|
||||
|
||||
//@function Applies the Rectified Linear Unit (ReLU) activation function to a series.
|
||||
// ReLU returns the input directly if it is positive, otherwise, it returns zero.
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/relu.md
|
||||
//@param src The source series.
|
||||
//@returns The ReLU transformed series.
|
||||
relu(series float src) =>
|
||||
math.max(0, src)
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation of relu on SMA(source)-source
|
||||
reluDn = -relu(ta.sma(i_source,20)-i_source)
|
||||
reluUp = relu(i_source-ta.sma(i_source,20))
|
||||
|
||||
// Plot
|
||||
plot(reluUp, "ReLU", color=color.green, linewidth=2)
|
||||
plot(reluDn, "ReLU", color=color.red, linewidth=2)
|
||||
Reference in New Issue
Block a user