mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 19:48: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,119 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ExptransIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ExptransIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("EXPTRANS - Exponential Function", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_MinHistoryDepths_IsOne()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
Assert.Equal(1, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
Assert.Equal("Exptrans", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Exptrans", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Exp of 0 is 1.0
|
||||
Assert.Equal(1.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 0, 1, -1, 1);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 0, 1, -1, 1);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
// Exp of 1 is e (~2.718)
|
||||
Assert.Equal(Math.E, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExptransIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new ExptransIndicator();
|
||||
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 ExptransIndicator_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 ExptransIndicator { 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>
|
||||
/// EXPTRANS (Exponential Function) Quantower indicator.
|
||||
/// Transforms values using the natural exponential function e^x.
|
||||
/// </summary>
|
||||
public class ExptransIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Exptrans? _exptrans;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => 1;
|
||||
public override string ShortName => "Exptrans";
|
||||
|
||||
public ExptransIndicator()
|
||||
{
|
||||
Name = "EXPTRANS - Exponential Function";
|
||||
Description = "Transforms values using the natural exponential function e^x";
|
||||
SeparateWindow = true;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_exptrans = new Exptrans();
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("Exptrans", Color.Green, 2, LineStyle.Solid));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_exptrans == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_exptrans.Update(input, isNew);
|
||||
|
||||
bool isHot = _exptrans.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_exptrans.Last.Value, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ExptransTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Constructor_SetsProperties()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
Assert.Equal("Exptrans", indicator.Name);
|
||||
Assert.Equal(0, indicator.WarmupPeriod);
|
||||
Assert.True(indicator.IsHot); // Always hot (no warmup)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Update_ReturnsExponential()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 0.0));
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance); // exp(0) = 1
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 1.0));
|
||||
Assert.Equal(Math.E, indicator.Last.Value, Tolerance); // exp(1) = e
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(2), 2.0));
|
||||
Assert.Equal(Math.E * Math.E, indicator.Last.Value, Tolerance); // exp(2) = e^2
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(3), -1.0));
|
||||
Assert.Equal(1.0 / Math.E, indicator.Last.Value, Tolerance); // exp(-1) = 1/e
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Update_KnownValues()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// exp(0) = 1
|
||||
indicator.Update(new TValue(time, 0.0));
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// exp(ln(10)) = 10
|
||||
indicator.Update(new TValue(time.AddMinutes(1), Math.Log(10.0)));
|
||||
Assert.Equal(10.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// exp(ln(0.5)) = 0.5
|
||||
indicator.Update(new TValue(time.AddMinutes(2), Math.Log(0.5)));
|
||||
Assert.Equal(0.5, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Update_IsNewFalse_CorrectsPreviousValue()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 1.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 2.0));
|
||||
Assert.Equal(Math.Exp(2.0), indicator.Last.Value, Tolerance);
|
||||
|
||||
// Correct last value
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 3.0), isNew: false);
|
||||
Assert.Equal(Math.Exp(3.0), indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Update_IterativeCorrection_RestoresState()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
double[] values = { 0.5, 1.0, 0.8, 1.2, 0.7, 1.5, 1.1 };
|
||||
|
||||
// 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, 0.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 Exptrans_Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 2.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 Exptrans_Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 1.5));
|
||||
double beforeInf = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.Equal(beforeInf, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Update_LargeInput_HandlesOverflow()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 5.0));
|
||||
double validResult = indicator.Last.Value;
|
||||
|
||||
// exp(1000) overflows to infinity
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 1000.0));
|
||||
// Should use last valid value
|
||||
Assert.Equal(validResult, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), i * 0.1));
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
indicator.Reset();
|
||||
Assert.True(indicator.IsHot); // Still hot (no warmup)
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Pub_EventFires()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
int eventCount = 0;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Chaining_Constructor_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Exptrans(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 0.0), true);
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance); // exp(0) = 1
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(1), 1.0), true);
|
||||
Assert.Equal(Math.E, indicator.Last.Value, Tolerance); // exp(1) = e
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Calculate_TSeries_MatchesStreaming()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 40000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
// Use log of close prices to stay in reasonable exp range
|
||||
var logSource = Logtrans.Calculate(bars.Close);
|
||||
|
||||
// Streaming
|
||||
var streaming = new Exptrans();
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < logSource.Count; i++)
|
||||
{
|
||||
streaming.Update(logSource[i]);
|
||||
streamingResults.Add(streaming.Last.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batch = Exptrans.Calculate(logSource);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < logSource.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batch[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Calculate_Span_MatchesTSeries()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 40001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var logSource = Logtrans.Calculate(bars.Close);
|
||||
|
||||
// TSeries batch
|
||||
var batchResult = Exptrans.Calculate(logSource);
|
||||
|
||||
// Span calculation
|
||||
var values = logSource.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Exptrans.Calculate(values, output);
|
||||
|
||||
for (int i = 0; i < logSource.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Calculate_Span_ValidatesArguments()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
Span<double> output = stackalloc double[10];
|
||||
Exptrans.Calculate(ReadOnlySpan<double>.Empty, output);
|
||||
});
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
ReadOnlySpan<double> source = stackalloc double[10];
|
||||
Span<double> output = stackalloc double[5];
|
||||
Exptrans.Calculate(source, output);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_LogInverse_ReturnsOriginal()
|
||||
{
|
||||
var exp = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
double logValue = 3.5;
|
||||
|
||||
exp.Update(new TValue(time, logValue));
|
||||
double expResult = exp.Last.Value;
|
||||
|
||||
// log(exp(x)) should equal x
|
||||
Assert.Equal(logValue, Math.Log(expResult), Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Negative_ReturnsPositive()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// exp(x) is always positive for any finite x
|
||||
for (int i = -10; i <= 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i + 10), i));
|
||||
Assert.True(indicator.Last.Value > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// EXPTRANS validation tests - validates against Math.Exp (standard library)
|
||||
/// </summary>
|
||||
public class ExptransValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-14;
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Batch_MatchesMathExp()
|
||||
{
|
||||
int count = 100;
|
||||
// Use log-transformed prices to keep exp in reasonable range
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var logSource = Logtrans.Calculate(bars.Close);
|
||||
|
||||
var result = Exptrans.Calculate(logSource);
|
||||
|
||||
for (int i = 0; i < logSource.Count; i++)
|
||||
{
|
||||
double expected = Math.Exp(logSource[i].Value);
|
||||
Assert.Equal(expected, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Streaming_MatchesMathExp()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var logSource = Logtrans.Calculate(bars.Close);
|
||||
|
||||
var indicator = new Exptrans();
|
||||
|
||||
for (int i = 0; i < logSource.Count; i++)
|
||||
{
|
||||
indicator.Update(logSource[i]);
|
||||
double expected = Math.Exp(logSource[i].Value);
|
||||
Assert.Equal(expected, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_Span_MatchesMathExp()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50002);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var logSource = Logtrans.Calculate(bars.Close);
|
||||
|
||||
var values = logSource.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Exptrans.Calculate(values, output);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double expected = Math.Exp(values[i]);
|
||||
Assert.Equal(expected, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_KnownIdentities()
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// exp(0) = 1
|
||||
indicator.Update(new TValue(time, 0.0));
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// exp(1) = e
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 1.0));
|
||||
Assert.Equal(Math.E, indicator.Last.Value, Tolerance);
|
||||
|
||||
// exp(n) = e^n
|
||||
for (int n = 2; n <= 5; n++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(n), n));
|
||||
Assert.Equal(Math.Exp(n), indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_InverseOfLog()
|
||||
{
|
||||
// exp(ln(x)) = x for all x > 0
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 50003);
|
||||
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var logResult = Logtrans.Calculate(source);
|
||||
var expResult = Exptrans.Calculate(logResult);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(source[i].Value, expResult[i].Value, 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_ProductRule()
|
||||
{
|
||||
// exp(a + b) = exp(a) * exp(b)
|
||||
double a = 1.5;
|
||||
double b = 2.3;
|
||||
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double expA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, b));
|
||||
double expB = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, a + b));
|
||||
double expAB = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(expA * expB, expAB, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_QuotientRule()
|
||||
{
|
||||
// exp(a - b) = exp(a) / exp(b)
|
||||
double a = 3.0;
|
||||
double b = 1.5;
|
||||
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double expA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, b));
|
||||
double expB = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, a - b));
|
||||
double expAMinusB = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(expA / expB, expAMinusB, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Exptrans_PowerRule()
|
||||
{
|
||||
// exp(n * a) = exp(a)^n
|
||||
double a = 1.2;
|
||||
int n = 3;
|
||||
|
||||
var indicator = new Exptrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double expA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, n * a));
|
||||
double expNA = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(Math.Pow(expA, n), expNA, 1e-12);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// EXPTRANS: Exponential Transformer
|
||||
// Transforms values using the exponential function e^x
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// EXPTRANS: Exponential Transformer
|
||||
/// Applies e^x transformation to input values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Inverse of natural logarithm: exp(ln(x)) = x
|
||||
/// - Maps additive relationships to multiplicative
|
||||
/// - Always positive output for any finite input
|
||||
/// - Useful for converting log returns to price ratios
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Exptrans : AbstractBase
|
||||
{
|
||||
private record struct State(double LastValid = 1.0); // exp(0) = 1
|
||||
private State _state = new(1.0), _p_state = new(1.0);
|
||||
|
||||
public override bool IsHot => true; // No warmup needed
|
||||
|
||||
public Exptrans()
|
||||
{
|
||||
Name = "Exptrans";
|
||||
WarmupPeriod = 0;
|
||||
}
|
||||
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
public Exptrans(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += HandleUpdate;
|
||||
}
|
||||
|
||||
[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.Exp(value);
|
||||
// Check for overflow (exp can produce infinity for large inputs)
|
||||
if (double.IsFinite(result))
|
||||
{
|
||||
_state = new State(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _state.LastValid;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
var indicator = new Exptrans();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates exponential over a span of values.
|
||||
/// </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 = 1.0; // exp(0) = 1
|
||||
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
double result = Math.Exp(val);
|
||||
if (double.IsFinite(result))
|
||||
{
|
||||
lastValid = result;
|
||||
output[i] = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = lastValid;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = lastValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = new(1.0);
|
||||
_p_state = new(1.0);
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
# EXPTRANS: Exponential Function
|
||||
|
||||
> "The exponential function is the only function that is its own derivative—a mathematical curiosity that makes it indispensable for modeling growth, decay, and everything compounding."
|
||||
|
||||
The Exponential (EXP) transformer applies the natural exponential function $e^x$ to each value in a time series. As the inverse of the natural logarithm, it converts additive relationships back to multiplicative ones, making it essential for reconstructing price levels from log-returns and implementing models that assume log-normal distributions.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Core Formula
|
||||
|
||||
$$
|
||||
\text{EXP}_t = e^{x_t}
|
||||
$$
|
||||
|
||||
where:
|
||||
- $x_t$ is the input value at time $t$
|
||||
- $e \approx 2.71828...$ is Euler's number
|
||||
|
||||
### Key Properties
|
||||
|
||||
| Property | Formula | Description |
|
||||
|:---------|:--------|:------------|
|
||||
| **Inverse of Log** | $e^{\ln(x)} = x$ | Undoes natural logarithm |
|
||||
| **Product Rule** | $e^{a+b} = e^a \cdot e^b$ | Additive inputs → multiplicative outputs |
|
||||
| **Quotient Rule** | $e^{a-b} = e^a / e^b$ | Differences → ratios |
|
||||
| **Power Rule** | $e^{n \cdot x} = (e^x)^n$ | Scaling in exponent → power |
|
||||
| **Identity** | $e^0 = 1$ | Zero maps to unity |
|
||||
| **Base Value** | $e^1 = e \approx 2.71828$ | Unit exponent gives $e$ |
|
||||
|
||||
### Domain and Range
|
||||
|
||||
| | Value |
|
||||
|:--|:--|
|
||||
| **Domain** | $(-\infty, +\infty)$ |
|
||||
| **Range** | $(0, +\infty)$ |
|
||||
|
||||
The exponential function accepts any real number but always produces strictly positive outputs.
|
||||
|
||||
## Financial Applications
|
||||
|
||||
### Log-Return to Price Reconstruction
|
||||
|
||||
Given cumulative log-returns, reconstruct price levels:
|
||||
|
||||
$$
|
||||
P_t = P_0 \cdot e^{\sum_{i=1}^{t} r_i}
|
||||
$$
|
||||
|
||||
where $r_i$ are log-returns.
|
||||
|
||||
### Volatility Scaling
|
||||
|
||||
Convert log-volatility to multiplicative factors:
|
||||
|
||||
$$
|
||||
\text{VolFactor} = e^{\sigma \sqrt{T}}
|
||||
$$
|
||||
|
||||
### Compound Growth
|
||||
|
||||
Model continuous compounding:
|
||||
|
||||
$$
|
||||
A = P \cdot e^{rt}
|
||||
$$
|
||||
|
||||
where $r$ is the continuous rate and $t$ is time.
|
||||
|
||||
### Option Pricing
|
||||
|
||||
The exponential appears throughout Black-Scholes:
|
||||
|
||||
$$
|
||||
C = S \cdot N(d_1) - K \cdot e^{-rT} \cdot N(d_2)
|
||||
$$
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Overflow Handling
|
||||
|
||||
For large positive inputs, $e^x$ can overflow to infinity:
|
||||
- $e^{709}$ ≈ $8.2 \times 10^{307}$ (near double max)
|
||||
- $e^{710}$ → overflow
|
||||
|
||||
The implementation substitutes the last valid value when overflow occurs.
|
||||
|
||||
### Precision Considerations
|
||||
|
||||
| Input Range | Relative Precision |
|
||||
|:------------|:-------------------|
|
||||
| $|x| < 1$ | Full 15-16 digits |
|
||||
| $|x| < 20$ | Full precision |
|
||||
| $|x| > 700$ | Overflow risk |
|
||||
|
||||
### Streaming Characteristics
|
||||
|
||||
| Metric | Value |
|
||||
|:-------|:------|
|
||||
| **Warmup Period** | 0 |
|
||||
| **Memory** | O(1) |
|
||||
| **Complexity** | O(1) per update |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Scalar)
|
||||
|
||||
| Operation | Count | Notes |
|
||||
|:----------|:-----:|:------|
|
||||
| EXP | 1 | Hardware instruction |
|
||||
| **Total** | ~20 cycles | Platform dependent |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
|:-------|:-----:|:------|
|
||||
| **Accuracy** | 10/10 | IEEE 754 compliant |
|
||||
| **Timeliness** | 10/10 | Zero lag |
|
||||
| **Smoothness** | N/A | Transform preserves input characteristics |
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```csharp
|
||||
// Create EXP transformer
|
||||
var exp = new Exptrans();
|
||||
|
||||
// Transform log-returns back to growth factors
|
||||
var logReturn = new TValue(DateTime.UtcNow, 0.05);
|
||||
var growthFactor = exp.Update(logReturn); // ≈ 1.0513
|
||||
```
|
||||
|
||||
### Reconstructing Prices from Log-Returns
|
||||
|
||||
```csharp
|
||||
var logReturns = new TSeries();
|
||||
// ... populate with cumulative log-returns
|
||||
|
||||
var cumulativeExp = new Exptrans();
|
||||
var priceRatios = cumulativeExp.Update(logReturns);
|
||||
|
||||
// Multiply by initial price to get price levels
|
||||
var initialPrice = 100.0;
|
||||
var prices = priceRatios.Select(v => v * initialPrice);
|
||||
```
|
||||
|
||||
### Undoing Log Transform
|
||||
|
||||
```csharp
|
||||
var log = new Logtrans();
|
||||
var exp = new Exptrans();
|
||||
|
||||
// Round-trip: price → log → exp → price
|
||||
var price = new TValue(DateTime.UtcNow, 150.0);
|
||||
var logPrice = log.Update(price); // ≈ 5.0106
|
||||
var recovered = exp.Update(logPrice); // ≈ 150.0
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Overflow Risk**: Input values above ~709 cause overflow. Monitor input ranges when working with cumulative sums.
|
||||
|
||||
2. **Magnitude Explosion**: Small additive changes in the exponent create large multiplicative changes in output. A change of 1.0 in the exponent multiplies the output by $e$ ≈ 2.72.
|
||||
|
||||
3. **Inverse Relationship**: EXP undoes LOG, but only if the original values were positive. Negative prices cannot be recovered through log-exp round-trip.
|
||||
|
||||
4. **Scale Sensitivity**: Unlike LOG which compresses ranges, EXP expands them dramatically. Ensure downstream consumers can handle the output magnitudes.
|
||||
|
||||
## Validation
|
||||
|
||||
| Test | Status |
|
||||
|:-----|:------:|
|
||||
| **Math.Exp Parity** | ✅ |
|
||||
| **Known Values (e⁰=1, e¹=e)** | ✅ |
|
||||
| **Inverse of Log** | ✅ |
|
||||
| **Product Rule** | ✅ |
|
||||
| **Quotient Rule** | ✅ |
|
||||
| **Power Rule** | ✅ |
|
||||
|
||||
## References
|
||||
|
||||
- Euler, L. (1748). *Introductio in analysin infinitorum*.
|
||||
- Maor, E. (1994). *e: The Story of a Number*. Princeton University Press.
|
||||
- Hull, J. (2018). *Options, Futures, and Other Derivatives*. Pearson. (Black-Scholes applications)
|
||||
@@ -0,0 +1,25 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Exponential Transformation (EXP)", "Exptrans", overlay=false)
|
||||
|
||||
//@function Applies an exponential transformation (y = e^x) to the input series.
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/exp.md
|
||||
//@param source series float The input series to transform.
|
||||
//@returns series float The exponentially transformed series.
|
||||
//@optimized for performance and dirty data
|
||||
expT(series float source) =>
|
||||
if na(source)
|
||||
runtime.error("Parameter 'source' cannot be na.")
|
||||
math.exp(source)
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input(close, "Source")
|
||||
|
||||
// Calculation
|
||||
transformedSource = expT(i_source)
|
||||
|
||||
// Plot
|
||||
plot(transformedSource, "Exponential Transformation", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user