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:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
@@ -0,0 +1,165 @@
using Xunit;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib.Tests;
public class SigmoidIndicatorTests
{
[Fact]
public void SigmoidIndicator_Constructor_SetsDefaults()
{
var indicator = new SigmoidIndicator();
Assert.Equal(SourceType.Close, indicator.Source);
Assert.Equal(1.0, indicator.Steepness);
Assert.Equal(0.0, indicator.Midpoint);
Assert.True(indicator.ShowColdValues);
Assert.Equal("SIGMOID - Logistic Function", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void SigmoidIndicator_MinHistoryDepths_IsOne()
{
var indicator = new SigmoidIndicator();
Assert.Equal(1, indicator.MinHistoryDepths);
}
[Fact]
public void SigmoidIndicator_ShortName_IncludesParameters()
{
var indicator = new SigmoidIndicator { Steepness = 2.0, Midpoint = 50.0 };
Assert.Equal("SIGMOID(2.00,50.00)", indicator.ShortName);
}
[Fact]
public void SigmoidIndicator_Initialize_CreatesLineSeries()
{
var indicator = new SigmoidIndicator();
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
Assert.Equal("Sigmoid", indicator.LinesSeries[0].Name);
}
[Fact]
public void SigmoidIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new SigmoidIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
// Sigmoid of 0 with default params is 0.5
Assert.Equal(0.5, indicator.LinesSeries[0].GetValue(0), 1e-10);
}
[Fact]
public void SigmoidIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new SigmoidIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
indicator.HistoricalData.AddBar(now.AddMinutes(1), 0, 2, -1, 1);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
// Sigmoid of 1 is about 0.731
double expected = 1.0 / (1.0 + Math.Exp(-1.0));
Assert.Equal(expected, indicator.LinesSeries[0].GetValue(0), 1e-10);
}
[Fact]
public void SigmoidIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
{
var indicator = new SigmoidIndicator();
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 SigmoidIndicator_CustomParameters_AreApplied()
{
var indicator = new SigmoidIndicator
{
Steepness = 2.0,
Midpoint = 50.0
};
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 50, 51, 49, 50);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Sigmoid at midpoint should be 0.5
Assert.Equal(0.5, indicator.LinesSeries[0].GetValue(0), 1e-10);
}
[Fact]
public void SigmoidIndicator_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 SigmoidIndicator { 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);
double val = indicator.LinesSeries[0].GetValue(0);
// All outputs should be in (0, 1)
Assert.True(val > 0 && val < 1);
}
}
[Fact]
public void SigmoidIndicator_OutputAlwaysInRange()
{
var indicator = new SigmoidIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// Test with large positive and negative values
double[] testValues = { -1000, -100, -10, -1, 0, 1, 10, 100, 1000 };
foreach (var val in testValues)
{
indicator.HistoricalData.AddBar(now, val, val + 1, val - 1, val);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
double output = indicator.LinesSeries[0].GetValue(0);
Assert.True(output >= 0 && output <= 1, $"Sigmoid({val}) = {output} should be in [0,1]");
now = now.AddMinutes(1);
}
}
}
+62
View File
@@ -0,0 +1,62 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
using static QuanTAlib.IndicatorExtensions;
namespace QuanTAlib;
/// <summary>
/// SIGMOID (Logistic Function) Quantower indicator.
/// Maps any real-valued input to the range (0, 1) using the logistic function.
/// </summary>
public class SigmoidIndicator : Indicator, IWatchlistIndicator
{
[DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Steepness (k)", sortIndex: 10, minimum: 0.01, maximum: 100, increment: 0.1, decimalPlaces: 2)]
public double Steepness { get; set; } = 1.0;
[InputParameter("Midpoint (x0)", sortIndex: 20, minimum: -10000, maximum: 10000, increment: 1, decimalPlaces: 2)]
public double Midpoint { get; set; } = 0.0;
[InputParameter("Show Cold Values", sortIndex: 100)]
public bool ShowColdValues { get; set; } = true;
private Sigmoid? _sigmoid;
private Func<IHistoryItem, double>? _selector;
public int MinHistoryDepths => 1;
public override string ShortName => $"SIGMOID({Steepness:F2},{Midpoint:F2})";
public SigmoidIndicator()
{
Name = "SIGMOID - Logistic Function";
Description = "Maps any real-valued input to the range (0, 1) using the logistic function";
SeparateWindow = true;
OnBackGround = true;
}
protected override void OnInit()
{
_sigmoid = new Sigmoid(Steepness, Midpoint);
_selector = Source.GetPriceSelector();
AddLineSeries(new LineSeries("Sigmoid", Color.Orange, 2, LineStyle.Solid));
}
protected override void OnUpdate(UpdateArgs args)
{
if (_sigmoid == null || _selector == null) return;
var item = HistoricalData[0, SeekOriginHistory.End];
double value = _selector(item);
bool isNew = args.IsNewBar();
TValue input = new(item.TimeLeft, value);
_sigmoid.Update(input, isNew);
bool isHot = _sigmoid.IsHot;
LinesSeries[0].SetValue(_sigmoid.Last.Value, isHot, ShowColdValues);
}
}
+354
View File
@@ -0,0 +1,354 @@
using Xunit;
namespace QuanTAlib.Tests;
public class SigmoidTests
{
private readonly GBM _gbm = new(1000, 0.05, 0.2, seed: 100);
private const double Epsilon = 1e-10;
// ═══════════════════════════════════════════════════════════════════════════════
// Constructor Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Constructor_WithDefaultParameters_SetsCorrectName()
{
var sigmoid = new Sigmoid();
Assert.Equal("Sigmoid(1.00,0.00)", sigmoid.Name);
}
[Fact]
public void Constructor_WithCustomParameters_SetsCorrectName()
{
var sigmoid = new Sigmoid(k: 0.5, x0: 100.0);
Assert.Equal("Sigmoid(0.50,100.00)", sigmoid.Name);
}
[Fact]
public void Constructor_WithZeroK_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Sigmoid(k: 0));
}
[Fact]
public void Constructor_WithNegativeK_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Sigmoid(k: -1));
}
[Fact]
public void Constructor_WarmupPeriod_IsZero()
{
var sigmoid = new Sigmoid();
Assert.Equal(0, sigmoid.WarmupPeriod);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Basic Update Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Update_ReturnsTValue()
{
var sigmoid = new Sigmoid();
var input = new TValue(DateTime.UtcNow, 0.0);
var result = sigmoid.Update(input);
Assert.IsType<TValue>(result);
}
[Fact]
public void Update_AtMidpoint_ReturnsHalf()
{
var sigmoid = new Sigmoid(k: 1.0, x0: 0.0);
var input = new TValue(DateTime.UtcNow, 0.0);
var result = sigmoid.Update(input);
Assert.Equal(0.5, result.Value, Epsilon);
}
[Fact]
public void Update_AtMidpointWithOffset_ReturnsHalf()
{
var sigmoid = new Sigmoid(k: 1.0, x0: 100.0);
var input = new TValue(DateTime.UtcNow, 100.0);
var result = sigmoid.Update(input);
Assert.Equal(0.5, result.Value, Epsilon);
}
[Fact]
public void Update_Last_IsUpdated()
{
var sigmoid = new Sigmoid();
var input = new TValue(DateTime.UtcNow, 1.0);
sigmoid.Update(input);
Assert.Equal(input.Time, sigmoid.Last.Time);
}
[Fact]
public void Update_IsHot_IsAlwaysTrue()
{
var sigmoid = new Sigmoid();
Assert.True(sigmoid.IsHot);
sigmoid.Update(new TValue(DateTime.UtcNow, 0.0));
Assert.True(sigmoid.IsHot);
}
// ═══════════════════════════════════════════════════════════════════════════════
// isNew State Management Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Update_WithIsNewTrue_AdvancesState()
{
var sigmoid = new Sigmoid();
var input1 = new TValue(DateTime.UtcNow, 1.0);
var input2 = new TValue(DateTime.UtcNow.AddSeconds(1), 2.0);
var result1 = sigmoid.Update(input1, isNew: true);
var result2 = sigmoid.Update(input2, isNew: true);
Assert.NotEqual(result1.Value, result2.Value);
}
[Fact]
public void Update_WithIsNewFalse_ReplacesCurrentBar()
{
var sigmoid = new Sigmoid();
var input1 = new TValue(DateTime.UtcNow, 1.0);
var input2 = new TValue(DateTime.UtcNow, 2.0);
sigmoid.Update(input1, isNew: true);
var result = sigmoid.Update(input2, isNew: false);
Assert.Equal(sigmoid.Last.Value, result.Value);
}
[Fact]
public void Update_IterativeCorrections_RestoreState()
{
var sigmoid = new Sigmoid();
// Initial value
sigmoid.Update(new TValue(DateTime.UtcNow, 1.0), isNew: true);
double afterFirst = sigmoid.Last.Value;
// Multiple corrections (isNew = false)
sigmoid.Update(new TValue(DateTime.UtcNow, 2.0), isNew: false);
sigmoid.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
sigmoid.Update(new TValue(DateTime.UtcNow, 1.0), isNew: false);
// Should restore to same state as after first update with same input
Assert.Equal(afterFirst, sigmoid.Last.Value, Epsilon);
}
// ═══════════════════════════════════════════════════════════════════════════════
// NaN/Infinity Handling Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Update_NaN_UsesLastValidValue()
{
var sigmoid = new Sigmoid();
sigmoid.Update(new TValue(DateTime.UtcNow, 1.0), isNew: true);
double lastValid = sigmoid.Last.Value;
var nanResult = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NaN), isNew: true);
Assert.Equal(lastValid, nanResult.Value);
}
[Fact]
public void Update_PositiveInfinity_UsesLastValidValue()
{
var sigmoid = new Sigmoid();
sigmoid.Update(new TValue(DateTime.UtcNow, 0.0), isNew: true);
double lastValid = sigmoid.Last.Value;
var infResult = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.PositiveInfinity), isNew: true);
Assert.Equal(lastValid, infResult.Value);
}
[Fact]
public void Update_NegativeInfinity_UsesLastValidValue()
{
var sigmoid = new Sigmoid();
sigmoid.Update(new TValue(DateTime.UtcNow, 0.0), isNew: true);
double lastValid = sigmoid.Last.Value;
var infResult = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(1), double.NegativeInfinity), isNew: true);
Assert.Equal(lastValid, infResult.Value);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Reset Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Reset_ClearsState()
{
var sigmoid = new Sigmoid();
sigmoid.Update(new TValue(DateTime.UtcNow, 1.0), isNew: true);
sigmoid.Reset();
Assert.Equal(default, sigmoid.Last);
}
// ═══════════════════════════════════════════════════════════════════════════════
// TSeries Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Update_TSeries_ReturnsCorrectLength()
{
var sigmoid = new Sigmoid();
var series = new TSeries();
for (int i = 0; i < 100; i++)
series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true);
var result = sigmoid.Update(series);
Assert.Equal(series.Count, result.Count);
}
[Fact]
public void Calculate_TSeries_ReturnsCorrectLength()
{
var series = new TSeries();
for (int i = 0; i < 100; i++)
series.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i - 50), isNew: true);
var result = Sigmoid.Calculate(series);
Assert.Equal(series.Count, result.Count);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Span API Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Calculate_Span_EmptySource_ThrowsArgumentException()
{
double[] output = new double[10];
Assert.Throws<ArgumentException>(() => Sigmoid.Calculate(ReadOnlySpan<double>.Empty, output.AsSpan()));
}
[Fact]
public void Calculate_Span_OutputTooSmall_ThrowsArgumentException()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[3];
Assert.Throws<ArgumentException>(() => Sigmoid.Calculate(source.AsSpan(), output.AsSpan()));
}
[Fact]
public void Calculate_Span_InvalidK_ThrowsArgumentException()
{
double[] source = [1, 2, 3, 4, 5];
double[] output = new double[5];
Assert.Throws<ArgumentException>(() => Sigmoid.Calculate(source.AsSpan(), output.AsSpan(), k: 0));
}
[Fact]
public void Calculate_Span_MatchesStreaming()
{
double[] source = new double[100];
var rng = new Random(42);
for (int i = 0; i < source.Length; i++)
source[i] = rng.NextDouble() * 200 - 100;
double[] spanOutput = new double[source.Length];
Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan());
var sigmoid = new Sigmoid();
double[] streamOutput = new double[source.Length];
for (int i = 0; i < source.Length; i++)
streamOutput[i] = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(i), source[i]), true).Value;
for (int i = 0; i < source.Length; i++)
Assert.Equal(streamOutput[i], spanOutput[i], Epsilon);
}
[Fact]
public void Calculate_Span_HandlesNaN()
{
double[] source = [1.0, double.NaN, 2.0];
double[] output = new double[3];
Sigmoid.Calculate(source.AsSpan(), output.AsSpan());
Assert.True(double.IsFinite(output[0]));
Assert.True(double.IsFinite(output[1])); // NaN replaced with last valid
Assert.True(double.IsFinite(output[2]));
}
// ═══════════════════════════════════════════════════════════════════════════════
// Chaining Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Chaining_PublishesEvents()
{
var source = new TSeries();
var sigmoid = new Sigmoid(source);
int eventCount = 0;
sigmoid.Pub += (_, in _) => eventCount++;
for (int i = 0; i < 10; i++)
source.Add(new TValue(DateTime.UtcNow.AddSeconds(i), i), isNew: true);
Assert.Equal(10, eventCount);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Steepness Parameter Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Update_HigherK_CreatesSteeperTransition()
{
var sigmoidLow = new Sigmoid(k: 0.5);
var sigmoidHigh = new Sigmoid(k: 5.0);
// At x=1, higher k should give value closer to 1
var resultLow = sigmoidLow.Update(new TValue(DateTime.UtcNow, 1.0));
var resultHigh = sigmoidHigh.Update(new TValue(DateTime.UtcNow, 1.0));
Assert.True(resultHigh.Value > resultLow.Value);
}
[Fact]
public void Update_DifferentX0_ShiftsMidpoint()
{
var sigmoid0 = new Sigmoid(k: 1.0, x0: 0.0);
var sigmoid100 = new Sigmoid(k: 1.0, x0: 100.0);
// At x=0, sigmoid with x0=0 should be 0.5
var result0 = sigmoid0.Update(new TValue(DateTime.UtcNow, 0.0));
// At x=100, sigmoid with x0=100 should be 0.5
var result100 = sigmoid100.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.Equal(0.5, result0.Value, Epsilon);
Assert.Equal(0.5, result100.Value, Epsilon);
}
}
@@ -0,0 +1,239 @@
using Xunit;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation tests for Sigmoid indicator against mathematical properties.
/// Sigmoid has no direct external library equivalents, so we validate against
/// the mathematical definition: S(x) = 1 / (1 + exp(-k * (x - x0)))
/// </summary>
public class SigmoidValidationTests
{
private const double Epsilon = 1e-10;
// ═══════════════════════════════════════════════════════════════════════════════
// Mathematical Definition Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Theory]
[InlineData(0.0, 1.0, 0.0)] // S(0) with k=1, x0=0
[InlineData(1.0, 1.0, 0.0)] // S(1) with k=1, x0=0
[InlineData(-1.0, 1.0, 0.0)] // S(-1) with k=1, x0=0
[InlineData(5.0, 1.0, 0.0)] // S(5) with k=1, x0=0
[InlineData(-5.0, 1.0, 0.0)] // S(-5) with k=1, x0=0
[InlineData(0.0, 2.0, 0.0)] // Different steepness
[InlineData(100.0, 1.0, 100.0)] // Shifted midpoint
public void Sigmoid_MatchesMathematicalDefinition(double x, double k, double x0)
{
var sigmoid = new Sigmoid(k, x0);
var result = sigmoid.Update(new TValue(DateTime.UtcNow, x));
// Mathematical definition: S(x) = 1 / (1 + exp(-k * (x - x0)))
double expected = 1.0 / (1.0 + Math.Exp(-k * (x - x0)));
Assert.Equal(expected, result.Value, Epsilon);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Symmetry Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Theory]
[InlineData(1.0)]
[InlineData(2.0)]
[InlineData(5.0)]
[InlineData(10.0)]
public void Sigmoid_Symmetry_AroundMidpoint(double offset)
{
// Property: S(x0 + d) + S(x0 - d) = 1
var sigmoid = new Sigmoid(k: 1.0, x0: 0.0);
var resultPlus = sigmoid.Update(new TValue(DateTime.UtcNow, offset));
sigmoid.Reset();
var resultMinus = sigmoid.Update(new TValue(DateTime.UtcNow, -offset));
Assert.Equal(1.0, resultPlus.Value + resultMinus.Value, Epsilon);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Midpoint Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Theory]
[InlineData(0.0)]
[InlineData(50.0)]
[InlineData(-50.0)]
[InlineData(100.0)]
public void Sigmoid_AtMidpoint_ReturnsHalf(double x0)
{
// Property: S(x0) = 0.5 for any x0
var sigmoid = new Sigmoid(k: 1.0, x0: x0);
var result = sigmoid.Update(new TValue(DateTime.UtcNow, x0));
Assert.Equal(0.5, result.Value, Epsilon);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Range Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_OutputAlwaysBetweenZeroAndOne()
{
var sigmoid = new Sigmoid();
var rng = new Random(42);
for (int i = 0; i < 1000; i++)
{
double x = rng.NextDouble() * 2000 - 1000; // Range [-1000, 1000]
var result = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(i), x), true);
Assert.True(result.Value >= 0.0, $"Output {result.Value} should be >= 0 for input {x}");
Assert.True(result.Value <= 1.0, $"Output {result.Value} should be <= 1 for input {x}");
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Monotonicity Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_IsStrictlyIncreasing()
{
// Property: if x1 < x2 then S(x1) < S(x2)
var sigmoid = new Sigmoid();
double prevValue = double.NegativeInfinity;
for (double x = -10; x <= 10; x += 0.5)
{
sigmoid.Reset();
var result = sigmoid.Update(new TValue(DateTime.UtcNow, x));
Assert.True(result.Value > prevValue, $"S({x}) = {result.Value} should be > {prevValue}");
prevValue = result.Value;
}
}
// ═══════════════════════════════════════════════════════════════════════════════
// Steepness Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_HigherK_SteeperTransition()
{
// At x = x0 + 1, higher k should produce values closer to 1
double x = 1.0;
var sigmoidK1 = new Sigmoid(k: 1.0);
var sigmoidK5 = new Sigmoid(k: 5.0);
var sigmoidK10 = new Sigmoid(k: 10.0);
var result1 = sigmoidK1.Update(new TValue(DateTime.UtcNow, x));
var result5 = sigmoidK5.Update(new TValue(DateTime.UtcNow, x));
var result10 = sigmoidK10.Update(new TValue(DateTime.UtcNow, x));
Assert.True(result10.Value > result5.Value);
Assert.True(result5.Value > result1.Value);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Derivative Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_DerivativeMaximumAtMidpoint()
{
// Property: The derivative of sigmoid is maximum at x0
// S'(x) = k * S(x) * (1 - S(x))
// At x0, S(x0) = 0.5, so S'(x0) = k * 0.5 * 0.5 = k/4
double k = 2.0;
var sigmoid = new Sigmoid(k: k, x0: 0.0);
// Numerical derivative using central difference
double h = 0.0001;
sigmoid.Reset();
double sPlus = sigmoid.Update(new TValue(DateTime.UtcNow, h)).Value;
sigmoid.Reset();
double sMinus = sigmoid.Update(new TValue(DateTime.UtcNow, -h)).Value;
double numericalDerivative = (sPlus - sMinus) / (2 * h);
double expectedDerivative = k / 4.0;
Assert.Equal(expectedDerivative, numericalDerivative, 1e-4);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Limit Property Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_ApproachesOneForLargePositive()
{
// lim(x→∞) S(x) = 1
var sigmoid = new Sigmoid();
var result = sigmoid.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.True(result.Value > 0.99999);
}
[Fact]
public void Sigmoid_ApproachesZeroForLargeNegative()
{
// lim(x→-∞) S(x) = 0
var sigmoid = new Sigmoid();
var result = sigmoid.Update(new TValue(DateTime.UtcNow, -100.0));
Assert.True(result.Value < 0.00001);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Inverse Relationship Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Theory]
[InlineData(0.1)]
[InlineData(0.25)]
[InlineData(0.5)]
[InlineData(0.75)]
[InlineData(0.9)]
public void Sigmoid_InverseIsLogit(double y)
{
// Logit(y) = ln(y / (1-y)) = x (inverse of sigmoid with k=1, x0=0)
var sigmoid = new Sigmoid(k: 1.0, x0: 0.0);
// Calculate x from y using logit
double x = Math.Log(y / (1 - y));
// Sigmoid of x should give y
var result = sigmoid.Update(new TValue(DateTime.UtcNow, x));
Assert.Equal(y, result.Value, Epsilon);
}
// ═══════════════════════════════════════════════════════════════════════════════
// Span vs Streaming Consistency Tests
// ═══════════════════════════════════════════════════════════════════════════════
[Fact]
public void Sigmoid_SpanAndStreaming_ProduceSameResults()
{
double k = 0.5;
double x0 = 50.0;
double[] source = new double[500];
var rng = new Random(42);
for (int i = 0; i < source.Length; i++)
source[i] = rng.NextDouble() * 200 - 50; // Range [-50, 150]
// Span calculation
double[] spanOutput = new double[source.Length];
Sigmoid.Calculate(source.AsSpan(), spanOutput.AsSpan(), k, x0);
// Streaming calculation
var sigmoid = new Sigmoid(k, x0);
for (int i = 0; i < source.Length; i++)
{
var result = sigmoid.Update(new TValue(DateTime.UtcNow.AddSeconds(i), source[i]), true);
Assert.Equal(spanOutput[i], result.Value, Epsilon);
}
}
}
+171
View File
@@ -0,0 +1,171 @@
// SIGMOID: Logistic Function
// Activation function that maps any real value to (0, 1)
// Formula: S(x) = 1 / (1 + exp(-k * (x - x0)))
using System.Runtime.CompilerServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
namespace QuanTAlib;
/// <summary>
/// SIGMOID: Logistic Function
/// Maps any real-valued input to the range (0, 1) using the logistic function.
/// </summary>
/// <remarks>
/// Key properties:
/// - Output always between 0 and 1 (exclusive)
/// - S-shaped curve centered at x0
/// - Steepness controlled by parameter k
/// - Commonly used for probability-like outputs and neural networks
/// </remarks>
[SkipLocalsInit]
public sealed class Sigmoid : AbstractBase
{
private readonly double _k;
private readonly double _x0;
private record struct State(double LastValid);
private State _state, _p_state;
public override bool IsHot => true; // No warmup needed
/// <param name="k">Steepness factor (default 1.0). Higher values create steeper transitions.</param>
/// <param name="x0">Midpoint value where output equals 0.5 (default 0.0).</param>
public Sigmoid(double k = 1.0, double x0 = 0.0)
{
if (k <= 0)
throw new ArgumentException("Steepness (k) must be positive", nameof(k));
_k = k;
_x0 = x0;
Name = $"Sigmoid({k:F2},{x0:F2})";
WarmupPeriod = 0;
}
/// <param name="source">Source indicator for chaining</param>
/// <param name="k">Steepness factor (default 1.0)</param>
/// <param name="x0">Midpoint value (default 0.0)</param>
public Sigmoid(ITValuePublisher source, double k = 1.0, double x0 = 0.0) : this(k, x0)
{
source.Pub += HandleUpdate;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void HandleUpdate(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeSigmoid(double x, double k, double x0)
{
double exponent = -k * (x - x0);
// Guard against overflow: exp(>709) overflows, exp(<-709) underflows to 0
if (exponent > 700) return 0.0; // exp(-700) ≈ 0
if (exponent < -700) return 1.0; // 1/(1+0) = 1
return 1.0 / (1.0 + Math.Exp(exponent));
}
[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 = ComputeSigmoid(value, _k, _x0);
_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)
{
var result = new TSeries(source.Count);
ReadOnlySpan<double> values = source.Values;
ReadOnlySpan<long> times = source.Times;
for (int i = 0; i < source.Count; i++)
{
var tv = Update(new TValue(new DateTime(times[i], DateTimeKind.Utc), values[i]), true);
result.Add(tv, true);
}
return result;
}
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, double k = 1.0, double x0 = 0.0)
{
var indicator = new Sigmoid(k, x0);
return indicator.Update(source);
}
/// <summary>
/// Calculates Sigmoid over a span of values.
/// </summary>
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, double k = 1.0, double x0 = 0.0)
{
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));
if (k <= 0)
throw new ArgumentException("Steepness (k) must be positive", nameof(k));
double lastValid = 0.5; // Sigmoid(x0) = 0.5
int i = 0;
// SIMD path for AVX2 - sigmoid requires exp(), so vectorization is limited
// Using scalar computation with potential for future SVML support
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
{
// For now, process in scalar due to exp() dependency
// Future: could use Intel SVML or approximate methods
}
// Scalar path
for (; i < source.Length; i++)
{
double val = source[i];
if (double.IsFinite(val))
{
double result = ComputeSigmoid(val, k, x0);
lastValid = result;
output[i] = result;
}
else
{
output[i] = lastValid;
}
}
}
public override void Reset()
{
_state = default;
_p_state = default;
Last = default;
}
}
+214
View File
@@ -0,0 +1,214 @@
# SIGMOID: Logistic Function
> "The sigmoid function is the S-curve that turns messy reality into neat probabilities—a mathematical diplomat that insists every answer must be between 0 and 1."
The Sigmoid (Logistic) transformer maps any real-valued input to the bounded range (0, 1) using the standard logistic function. Its characteristic S-shaped curve makes it indispensable for probability estimation, neural network activations, and any scenario requiring bounded outputs from unbounded inputs.
## Mathematical Foundation
### Core Formula
$$
S(x) = \frac{1}{1 + e^{-k(x - x_0)}}
$$
where:
- $x$ is the input value
- $k$ is the steepness factor (default 1.0)
- $x_0$ is the midpoint where $S(x_0) = 0.5$ (default 0.0)
- $e \approx 2.71828...$ is Euler's number
### Key Properties
| Property | Formula | Description |
|:---------|:--------|:------------|
| **Midpoint** | $S(x_0) = 0.5$ | Centered at $x_0$ |
| **Symmetry** | $S(x_0 + d) + S(x_0 - d) = 1$ | Point symmetry about $(x_0, 0.5)$ |
| **Limits** | $\lim_{x \to -\infty} S(x) = 0$, $\lim_{x \to +\infty} S(x) = 1$ | Asymptotic bounds |
| **Derivative** | $S'(x) = k \cdot S(x) \cdot (1 - S(x))$ | Self-referential gradient |
| **Monotonicity** | $S'(x) > 0$ for all $x$ | Strictly increasing |
| **Steepness** | Higher $k$ → steeper transition | Controls sensitivity |
### Domain and Range
| | Value |
|:--|:--|
| **Domain** | $(-\infty, +\infty)$ |
| **Range** | $(0, 1)$ exclusive |
The sigmoid accepts any real number and always produces outputs strictly between 0 and 1 (never exactly 0 or 1).
## Financial Applications
### Probability-like Outputs
Convert any signal to a pseudo-probability:
$$
P_{signal} = S(z\text{-score})
$$
where large positive z-scores approach 1, negative approach 0.
### Bounded Confidence Indicators
Transform unbounded oscillators to fixed ranges:
$$
\text{BoundedRSI} = S(k \cdot (\text{RSI} - 50))
$$
### Regime Classification
Soft classification between bullish (1) and bearish (0) regimes:
$$
\text{Regime} = S(k \cdot \text{TrendStrength})
$$
### Position Sizing
Map conviction signals to allocation weights:
$$
\text{Weight} = S(\text{ConvictionScore})
$$
## Parameter Guide
### Steepness ($k$)
| $k$ Value | Behavior | Use Case |
|:----------|:---------|:---------|
| 0.1 | Very gradual | Smooth transitions, noise reduction |
| 0.5 | Gentle | Conservative probability mapping |
| 1.0 | Standard | General purpose (default) |
| 2.0 | Steep | Quick regime detection |
| 5.0+ | Very steep | Near binary classification |
### Midpoint ($x_0$)
| $x_0$ Value | Behavior |
|:------------|:---------|
| 0.0 | Standard (default), symmetric about origin |
| Mean | Centers output around data average |
| Threshold | Custom decision boundary |
## Implementation Details
### Overflow Handling
For extreme inputs, the exponential can overflow:
- When $-k(x - x_0) > 700$: return 0.0 (avoid exp overflow)
- When $-k(x - x_0) < -700$: return 1.0 (exp underflows to 0)
### Precision Considerations
| Input Range | Output Precision |
|:------------|:-----------------|
| $|k(x-x_0)| < 20$ | Full 15-16 digits |
| $|k(x-x_0)| > 36$ | Saturates to 0 or 1 within double precision |
### Streaming Characteristics
| Metric | Value |
|:-------|:------|
| **Warmup Period** | 0 |
| **Memory** | O(1) |
| **Complexity** | O(1) per update |
## Performance Profile
### Operation Count (Scalar)
| Operation | Count | Notes |
|:----------|:-----:|:------|
| SUB | 1 | $x - x_0$ |
| MUL | 1 | $k \times (x - x_0)$ |
| NEG | 1 | Negate for exp |
| EXP | 1 | Hardware instruction |
| ADD | 1 | $1 + \exp(...)$ |
| DIV | 1 | Final division |
| **Total** | ~25-30 cycles | Dominated by EXP |
### Quality Metrics
| Metric | Score | Notes |
|:-------|:-----:|:------|
| **Accuracy** | 10/10 | IEEE 754 compliant |
| **Timeliness** | 10/10 | Zero lag |
| **Smoothness** | 10/10 | Infinitely differentiable |
| **Boundedness** | 10/10 | Guaranteed (0, 1) output |
## Usage Examples
### Basic Usage
```csharp
// Create Sigmoid with default parameters
var sigmoid = new Sigmoid();
// Transform z-score to probability-like value
var zscore = new TValue(DateTime.UtcNow, 2.0);
var probability = sigmoid.Update(zscore); // ≈ 0.881
```
### Custom Steepness
```csharp
// Steep sigmoid for quick transitions
var steepSigmoid = new Sigmoid(k: 3.0);
var x = new TValue(DateTime.UtcNow, 1.0);
var result = steepSigmoid.Update(x); // ≈ 0.953 (steeper than default 0.731)
```
### Custom Midpoint
```csharp
// Center sigmoid at RSI neutral level (50)
var rsiSigmoid = new Sigmoid(k: 0.1, x0: 50);
var rsiValue = new TValue(DateTime.UtcNow, 70);
var bullishProbability = rsiSigmoid.Update(rsiValue); // ≈ 0.881
```
### Span API for Batch Processing
```csharp
double[] inputs = { -2, -1, 0, 1, 2 };
double[] outputs = new double[inputs.Length];
Sigmoid.Calculate(inputs, outputs, k: 1.0, x0: 0.0);
// outputs ≈ { 0.119, 0.269, 0.500, 0.731, 0.881 }
```
## Common Pitfalls
1. **Not Exactly 0 or 1**: Sigmoid asymptotically approaches but never reaches 0 or 1. If you need exact binary outputs, apply a threshold post-sigmoid.
2. **Vanishing Gradients**: For very large or small inputs, $S'(x) \approx 0$. This is a feature for boundedness but can cause issues if the sigmoid is part of a learning system.
3. **Scale Sensitivity**: The default $k=1$ assumes inputs are roughly in the range $[-5, 5]$. For inputs with different scales, adjust $k$ or normalize inputs first.
4. **Midpoint Confusion**: Remember $x_0$ shifts where 0.5 occurs, not where 0 occurs. Sigmoid never outputs exactly 0.
5. **Symmetry Assumption**: Sigmoid imposes symmetric transition behavior. For asymmetric responses, consider other activation functions.
## Validation
| Test | Status |
|:-----|:------:|
| **Midpoint S(x₀) = 0.5** | ✅ |
| **Symmetry Property** | ✅ |
| **Range (0, 1)** | ✅ |
| **Monotonicity** | ✅ |
| **Steepness Effect** | ✅ |
| **Limit Behavior** | ✅ |
| **Overflow Guards** | ✅ |
## References
- Verhulst, P.-F. (1838). "Notice sur la loi que la population suit dans son accroissement." *Correspondance Mathématique et Physique*.
- Rumelhart, D., Hinton, G., & Williams, R. (1986). "Learning representations by back-propagating errors." *Nature*.
- Bishop, C. (2006). *Pattern Recognition and Machine Learning*. Springer.
+28
View File
@@ -0,0 +1,28 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Logistic Function (SIGMOID)", "SIGMOID", overlay=false, precision=6)
//@function Applies the logistic (sigmoid) function to a source series.
// Formula: S(x) = 1 / (1 + exp(-k * (x - x0)))
// Maps any real-valued input to the range (0, 1).
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/sigmoid.md
//@param src The source series.
//@param k The steepness factor of the sigmoid curve. Higher k means a steeper curve.
//@param x0 The x-value of the sigmoid's midpoint (where the output is 0.5).
//@returns The sigmoid transformed series, values between 0 and 1.
sigmoid(series float src, simple float k, float x0) =>
1 / (1 + math.exp(-k * (src - x0)))
// ---------- Main loop ----------
// Inputs
i_source = input.source(close, "Source")
i_steepness_k = input.float(0.5, "Steepness (k)", minval = 0.000001, step = 0.1)
// Calculation
sigmoidValue = sigmoid(i_source, i_steepness_k, ta.sma(i_source,200))
// Plot
plot(sigmoidValue, "Sigmoid", color=color.yellow, linewidth=2)