SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
@@ -0,0 +1,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);
}
}
+261
View File
@@ -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);
}
}
+163
View File
@@ -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;
}
}
+125
View File
@@ -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.
+28
View File
@@ -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)