mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 12:38:06 +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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user