mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-22 04:28:04 +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,120 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class LogtransIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void LogtransIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("LOGTRANS - Natural Logarithm", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_MinHistoryDepths_IsOne()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
Assert.Equal(1, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_ShortName_IsCorrect()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
Assert.Equal("Logtrans", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
Assert.Equal("Logtrans", indicator.LinesSeries[0].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Log of 100 is approximately 4.605
|
||||
double value = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(value > 4.0 && value < 5.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, Math.E);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, Math.E);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
// Log of e is 1.0
|
||||
Assert.Equal(1.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new LogtransIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransIndicator_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 LogtransIndicator { Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
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>
|
||||
/// LOGTRANS (Natural Logarithm) Quantower indicator.
|
||||
/// Transforms values using natural logarithm ln(x).
|
||||
/// </summary>
|
||||
public class LogtransIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Logtrans? _logtrans;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => 1;
|
||||
public override string ShortName => "Logtrans";
|
||||
|
||||
public LogtransIndicator()
|
||||
{
|
||||
Name = "LOGTRANS - Natural Logarithm";
|
||||
Description = "Transforms values using natural logarithm ln(x)";
|
||||
SeparateWindow = true;
|
||||
OnBackGround = true;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_logtrans = new Logtrans();
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("Logtrans", Color.Orange, 2, LineStyle.Solid));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_logtrans == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_logtrans.Update(input, isNew);
|
||||
|
||||
bool isHot = _logtrans.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_logtrans.Last.Value, isHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class LogtransTests
|
||||
{
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
[Fact]
|
||||
public void Constructor_SetsProperties()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
Assert.Equal("Logtrans", indicator.Name);
|
||||
Assert.Equal(0, indicator.WarmupPeriod);
|
||||
Assert.True(indicator.IsHot); // Always hot (no warmup)
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ReturnsNaturalLog()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 1.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance); // ln(1) = 0
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), Math.E));
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance); // ln(e) = 1
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(2), Math.E * Math.E));
|
||||
Assert.Equal(2.0, indicator.Last.Value, Tolerance); // ln(e^2) = 2
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(3), 10.0));
|
||||
Assert.Equal(Math.Log(10.0), indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_KnownValues()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// ln(100) ≈ 4.605
|
||||
indicator.Update(new TValue(time, 100.0));
|
||||
Assert.Equal(Math.Log(100.0), indicator.Last.Value, Tolerance);
|
||||
|
||||
// ln(0.5) ≈ -0.693
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 0.5));
|
||||
Assert.Equal(Math.Log(0.5), indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IsNewFalse_CorrectsPreviousValue()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 20.0));
|
||||
Assert.Equal(Math.Log(20.0), indicator.Last.Value, Tolerance);
|
||||
|
||||
// Correct last value
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 100.0), isNew: false);
|
||||
Assert.Equal(Math.Log(100.0), indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_IterativeCorrection_RestoresState()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
double[] values = { 5.0, 10.0, 8.0, 12.0, 7.0, 15.0, 11.0 };
|
||||
|
||||
// Process all values
|
||||
foreach (var v in values)
|
||||
{
|
||||
indicator.Update(new TValue(time, v));
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
double finalResult = indicator.Last.Value;
|
||||
|
||||
// Reset and process with corrections
|
||||
indicator.Reset();
|
||||
time = DateTime.UtcNow;
|
||||
foreach (var v in values)
|
||||
{
|
||||
// Submit wrong value first
|
||||
indicator.Update(new TValue(time, 1.0));
|
||||
// Correct it
|
||||
indicator.Update(new TValue(time, v), isNew: false);
|
||||
time = time.AddMinutes(1);
|
||||
}
|
||||
|
||||
Assert.Equal(finalResult, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaN_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
double beforeNaN = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.NaN));
|
||||
Assert.Equal(beforeNaN, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Infinity_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 15.0));
|
||||
double beforeInf = indicator.Last.Value;
|
||||
|
||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.Equal(beforeInf, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NonPositive_UsesLastValidValue()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, 10.0));
|
||||
double beforeZero = indicator.Last.Value;
|
||||
|
||||
// Zero
|
||||
indicator.Update(new TValue(time.AddMinutes(1), 0.0));
|
||||
Assert.Equal(beforeZero, indicator.Last.Value, Tolerance);
|
||||
|
||||
// Negative
|
||||
indicator.Update(new TValue(time.AddMinutes(2), -5.0));
|
||||
Assert.Equal(beforeZero, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(i), i * 2.0));
|
||||
}
|
||||
|
||||
Assert.True(indicator.IsHot);
|
||||
indicator.Reset();
|
||||
Assert.True(indicator.IsHot); // Still hot (no warmup)
|
||||
Assert.Equal(default, indicator.Last);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
int eventCount = 0;
|
||||
indicator.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Chaining_Constructor_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Logtrans(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, Math.E), true);
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow.AddMinutes(1), Math.E * Math.E), true);
|
||||
Assert.Equal(2.0, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_TSeries_MatchesStreaming()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 20000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
// Streaming
|
||||
var streaming = new Logtrans();
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
streaming.Update(source[i]);
|
||||
streamingResults.Add(streaming.Last.Value);
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batch = Logtrans.Calculate(source);
|
||||
|
||||
// Compare all values
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], batch[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_MatchesTSeries()
|
||||
{
|
||||
int count = 50;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 20001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
// TSeries batch
|
||||
var batchResult = Logtrans.Calculate(source);
|
||||
|
||||
// Span calculation
|
||||
var values = source.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Logtrans.Calculate(values, output);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult[i].Value, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_Span_ValidatesArguments()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
Span<double> output = stackalloc double[10];
|
||||
Logtrans.Calculate(ReadOnlySpan<double>.Empty, output);
|
||||
});
|
||||
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
ReadOnlySpan<double> source = stackalloc double[10];
|
||||
Span<double> output = stackalloc double[5];
|
||||
Logtrans.Calculate(source, output);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LogtransExptransInverse_ReturnsOriginal()
|
||||
{
|
||||
var logtrans = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
double original = 42.0;
|
||||
|
||||
logtrans.Update(new TValue(time, original));
|
||||
double logtransResult = logtrans.Last.Value;
|
||||
|
||||
// exp(logtrans(x)) should equal x
|
||||
Assert.Equal(original, Math.Exp(logtransResult), Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// LOGTRANS validation tests - validates against Math.Log (standard library)
|
||||
/// No external TA libraries implement LOG directly, so we validate against .NET Math.
|
||||
/// </summary>
|
||||
public class LogtransValidationTests
|
||||
{
|
||||
private const double Tolerance = 1e-14; // Very tight - should match exactly
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_Batch_MatchesMathLog()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 30000);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var result = Logtrans.Calculate(source);
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
double expected = Math.Log(source[i].Value);
|
||||
Assert.Equal(expected, result[i].Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_Streaming_MatchesMathLog()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 30001);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var indicator = new Logtrans();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
indicator.Update(source[i]);
|
||||
double expected = Math.Log(source[i].Value);
|
||||
Assert.Equal(expected, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_Span_MatchesMathLog()
|
||||
{
|
||||
int count = 100;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 30002);
|
||||
var bars = gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var source = bars.Close;
|
||||
|
||||
var values = source.Values.ToArray();
|
||||
var output = new double[count];
|
||||
Logtrans.Calculate(values, output);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
double expected = Math.Log(values[i]);
|
||||
Assert.Equal(expected, output[i], Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_KnownIdentities()
|
||||
{
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// ln(1) = 0
|
||||
indicator.Update(new TValue(time, 1.0));
|
||||
Assert.Equal(0.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// ln(e) = 1
|
||||
indicator.Update(new TValue(time.AddMinutes(1), Math.E));
|
||||
Assert.Equal(1.0, indicator.Last.Value, Tolerance);
|
||||
|
||||
// ln(e^n) = n
|
||||
for (int n = 2; n <= 5; n++)
|
||||
{
|
||||
indicator.Update(new TValue(time.AddMinutes(n), Math.Pow(Math.E, n)));
|
||||
Assert.Equal(n, indicator.Last.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_ProductRule()
|
||||
{
|
||||
// ln(a*b) = ln(a) + ln(b)
|
||||
double a = 2.5;
|
||||
double b = 3.7;
|
||||
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double lnA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, b));
|
||||
double lnB = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, a * b));
|
||||
double lnAB = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(lnA + lnB, lnAB, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_QuotientRule()
|
||||
{
|
||||
// ln(a/b) = ln(a) - ln(b)
|
||||
double a = 10.0;
|
||||
double b = 2.5;
|
||||
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double lnA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, b));
|
||||
double lnB = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, a / b));
|
||||
double lnADivB = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(lnA - lnB, lnADivB, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Logtrans_PowerRule()
|
||||
{
|
||||
// ln(a^n) = n * ln(a)
|
||||
double a = 3.0;
|
||||
int n = 4;
|
||||
|
||||
var indicator = new Logtrans();
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
indicator.Update(new TValue(time, a));
|
||||
double lnA = indicator.Last.Value;
|
||||
|
||||
indicator.Reset();
|
||||
indicator.Update(new TValue(time, Math.Pow(a, n)));
|
||||
double lnAPowN = indicator.Last.Value;
|
||||
|
||||
Assert.Equal(n * lnA, lnAPowN, Tolerance);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// LOGTRANS: Natural Logarithm Transformer
|
||||
// Transforms values using natural logarithm (base e)
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Numerics;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// LOGTRANS: Natural Logarithm Transformer
|
||||
/// Applies ln(x) transformation to input values.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Key properties:
|
||||
/// - Compresses large values, expands small values
|
||||
/// - Useful for transforming multiplicative relationships to additive
|
||||
/// - Domain: x > 0 (non-positive inputs use last valid value)
|
||||
/// - Common in financial returns: ln(P_t / P_{t-1})
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Logtrans : AbstractBase
|
||||
{
|
||||
private record struct State(double LastValid);
|
||||
private State _state, _p_state;
|
||||
|
||||
public override bool IsHot => true; // No warmup needed
|
||||
|
||||
public Logtrans()
|
||||
{
|
||||
Name = "Logtrans";
|
||||
WarmupPeriod = 0;
|
||||
}
|
||||
|
||||
/// <param name="source">Source indicator for chaining</param>
|
||||
public Logtrans(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;
|
||||
|
||||
// Handle non-positive and non-finite values
|
||||
double value = input.Value;
|
||||
double result;
|
||||
|
||||
if (double.IsFinite(value) && value > 0)
|
||||
{
|
||||
result = Math.Log(value);
|
||||
_state = new State(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
result = _state.LastValid;
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
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 Logtrans();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates natural logarithm over a span of values using SIMD when available.
|
||||
/// </summary>
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
throw new ArgumentException("Source cannot be empty", nameof(source));
|
||||
if (output.Length < source.Length)
|
||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||
|
||||
double lastValid = 0.0;
|
||||
int i = 0;
|
||||
|
||||
// SIMD path for AVX2 (process 4 doubles at a time)
|
||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
||||
{
|
||||
int vectorLength = source.Length - (source.Length % Vector256<double>.Count);
|
||||
|
||||
for (; i < vectorLength; i += Vector256<double>.Count)
|
||||
{
|
||||
// Process scalar for proper last-valid handling (Logtrans has no SIMD intrinsic)
|
||||
for (int j = 0; j < Vector256<double>.Count; j++)
|
||||
{
|
||||
double val = source[i + j];
|
||||
if (double.IsFinite(val) && val > 0)
|
||||
{
|
||||
lastValid = Math.Log(val);
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i + j] = lastValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
for (; i < source.Length; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val) && val > 0)
|
||||
{
|
||||
lastValid = Math.Log(val);
|
||||
output[i] = lastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = lastValid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
# LOGTRANS: Natural Logarithm Transformer
|
||||
|
||||
> "The logarithm is one of the most useful mathematical functions, turning multiplicative relationships into additive ones—a property that makes many financial calculations tractable."
|
||||
|
||||
The LOG transformer applies the natural logarithm function $\ln(x)$ to input values. This point-wise transformation compresses large values and expands small ones, making it essential for analyzing multiplicative processes like compounded returns.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
The natural logarithm is defined as the inverse of the exponential function:
|
||||
|
||||
$$
|
||||
y = \ln(x) \quad \text{where} \quad e^y = x
|
||||
$$
|
||||
|
||||
Key identities:
|
||||
|
||||
- $\ln(1) = 0$
|
||||
- $\ln(e) = 1$
|
||||
- $\ln(e^n) = n$
|
||||
|
||||
### Logarithm Rules
|
||||
|
||||
**Product Rule:**
|
||||
$$
|
||||
\ln(a \cdot b) = \ln(a) + \ln(b)
|
||||
$$
|
||||
|
||||
**Quotient Rule:**
|
||||
$$
|
||||
\ln\left(\frac{a}{b}\right) = \ln(a) - \ln(b)
|
||||
$$
|
||||
|
||||
**Power Rule:**
|
||||
$$
|
||||
\ln(a^n) = n \cdot \ln(a)
|
||||
$$
|
||||
|
||||
## Financial Applications
|
||||
|
||||
### Log Returns
|
||||
|
||||
Log returns (continuously compounded returns) are computed as:
|
||||
|
||||
$$
|
||||
r_t = \ln\left(\frac{P_t}{P_{t-1}}\right) = \ln(P_t) - \ln(P_{t-1})
|
||||
$$
|
||||
|
||||
Log returns have desirable properties:
|
||||
- **Additive over time**: Multi-period return is the sum of single-period returns
|
||||
- **Symmetric**: A +10% log return followed by -10% returns to original price
|
||||
- **Approximately equal** to simple returns for small changes
|
||||
|
||||
### Volatility Analysis
|
||||
|
||||
Log-transformed prices are often used in volatility modeling because:
|
||||
- Standard deviation of log returns estimates volatility
|
||||
- Log prices follow geometric Brownian motion (GBM) under common models
|
||||
|
||||
## Domain Restrictions
|
||||
|
||||
The natural logarithm is only defined for positive real numbers:
|
||||
|
||||
$$
|
||||
\text{Domain}: x > 0
|
||||
$$
|
||||
|
||||
Invalid inputs (zero, negative, NaN, Infinity) return the last valid output value—a common pattern in financial indicators to prevent propagation of invalid data.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count
|
||||
|
||||
| Operation | Count | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| Math.Log | 1 | Single transcendental function call |
|
||||
| Comparison | 2 | Finite check, positive check |
|
||||
|
||||
**Cycles per value:** ~15-25 (dominated by log computation)
|
||||
|
||||
### SIMD Considerations
|
||||
|
||||
The Calculate span method includes AVX2 detection but falls back to scalar processing for proper last-valid-value handling. Pure SIMD vectorization of log is possible but requires handling domain violations differently.
|
||||
|
||||
## API Usage
|
||||
|
||||
### Streaming Mode
|
||||
|
||||
```csharp
|
||||
var log = new Logtrans();
|
||||
var result = log.Update(new TValue(time, price));
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```csharp
|
||||
var logPrices = Logtrans.Calculate(priceSeries);
|
||||
```
|
||||
|
||||
### Span Mode
|
||||
|
||||
```csharp
|
||||
Logtrans.Calculate(sourceSpan, outputSpan);
|
||||
```
|
||||
|
||||
### Chaining
|
||||
|
||||
```csharp
|
||||
var logTransform = new Logtrans(priceSource);
|
||||
// logTransform.Last updates automatically when priceSource publishes
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Zero/Negative Inputs**: Log of zero or negative numbers is undefined. The implementation substitutes last valid value.
|
||||
|
||||
2. **Numerical Precision**: For values very close to 1, use `Math.Log1p(x-1)` for better precision (not implemented here).
|
||||
|
||||
3. **Overflow Potential**: $\exp(\ln(x)) = x$ only within floating-point precision limits.
|
||||
|
||||
4. **Inverse Relationship**: Remember that LOG compresses large values—a 10x price increase only doubles the log value.
|
||||
|
||||
## References
|
||||
|
||||
- Wilmott, P. (2006). "Paul Wilmott on Quantitative Finance." Wiley.
|
||||
- Hull, J. (2018). "Options, Futures, and Other Derivatives." Pearson.
|
||||
@@ -0,0 +1,28 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Logarithmic Transformation (LOG)", "Logtrans", overlay=false)
|
||||
|
||||
//@function Applies a natural logarithmic transformation (y = ln(x)) to the input series.
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/log.md
|
||||
//@param source series float The input series to transform. Must contain positive values.
|
||||
//@returns series float The logarithmically transformed series. Returns na if source <= 0.
|
||||
//@optimized for performance and dirty data
|
||||
logT(series float source) =>
|
||||
if na(source)
|
||||
runtime.error("Parameter 'source' cannot be na.")
|
||||
if source <= 0
|
||||
na
|
||||
else
|
||||
math.log(source)
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_source = input(close, "Source")
|
||||
|
||||
// Calculation
|
||||
transformedSource = logT(i_source)
|
||||
|
||||
// Plot
|
||||
plot(transformedSource, "Log Transformation", color=color.green, color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user