mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 21:18: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,214 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SlopeIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void SlopeIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("SLOPE - First Derivative (Velocity)", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.False(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_MinHistoryDepths_IsTwo()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
Assert.Equal(2, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_ShortName_IsSlope()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
Assert.Equal("SLOPE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_Initialize_CreatesLineSeries()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries.Count);
|
||||
Assert.Equal("Slope", indicator.LinesSeries[0].Name);
|
||||
Assert.Equal("Zero", indicator.LinesSeries[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.Equal(1, indicator.LinesSeries[1].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_MultipleUpdates_ProducesCorrectSequence()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(
|
||||
now.AddMinutes(i),
|
||||
100 + i * 2,
|
||||
105 + i * 2,
|
||||
95 + i * 2,
|
||||
102 + i * 2);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
Assert.Equal(20, indicator.LinesSeries[0].Count);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(i)));
|
||||
Assert.Equal(0, indicator.LinesSeries[1].GetValue(i));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_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 SlopeIndicator { 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);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_ShowColdValues_False_SetsNaN()
|
||||
{
|
||||
var indicator = new SlopeIndicator { ShowColdValues = false };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsNaN(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_Uptrend_ProducesPositiveSlope()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 100 + i * 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastSlope = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastSlope > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_Downtrend_ProducesNegativeSlope()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
double price = 200 - i * 5;
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), price, price + 2, price - 2, price);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastSlope = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(lastSlope < 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SlopeIndicator_FlatPrices_ProducesZeroSlope()
|
||||
{
|
||||
var indicator = new SlopeIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 105, 95, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double lastSlope = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.Equal(0, lastSlope);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using static QuanTAlib.IndicatorExtensions;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SLOPE (First Derivative / Velocity) Quantower indicator.
|
||||
/// Measures the instantaneous rate of change between consecutive values.
|
||||
/// </summary>
|
||||
public class SlopeIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show Cold Values", sortIndex: 100)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Slope? _slope;
|
||||
private Func<IHistoryItem, double>? _selector;
|
||||
|
||||
public int MinHistoryDepths => 2;
|
||||
public override string ShortName => "SLOPE";
|
||||
|
||||
public SlopeIndicator()
|
||||
{
|
||||
Name = "SLOPE - First Derivative (Velocity)";
|
||||
Description = "Measures instantaneous rate of change between consecutive values";
|
||||
SeparateWindow = true;
|
||||
OnBackGround = false;
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_slope = new Slope();
|
||||
_selector = Source.GetPriceSelector();
|
||||
|
||||
AddLineSeries(new LineSeries("Slope", Momentum, 2, LineStyle.Histogramm));
|
||||
AddLineSeries(new LineSeries("Zero", Color.Gray, 1, LineStyle.Dot));
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
if (_slope == null || _selector == null) return;
|
||||
|
||||
var item = HistoricalData[0, SeekOriginHistory.End];
|
||||
double value = _selector(item);
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
TValue input = new(item.TimeLeft, value);
|
||||
_slope.Update(input, isNew);
|
||||
|
||||
bool isHot = _slope.IsHot;
|
||||
|
||||
LinesSeries[0].SetValue(_slope.Last.Value, isHot, ShowColdValues);
|
||||
LinesSeries[1].SetValue(0);
|
||||
|
||||
if (isHot || ShowColdValues)
|
||||
{
|
||||
double slope = _slope.Last.Value;
|
||||
Color color;
|
||||
if (slope > 0)
|
||||
color = Color.Green;
|
||||
else if (slope < 0)
|
||||
color = Color.Red;
|
||||
else
|
||||
color = Color.Gray;
|
||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class SlopeTests
|
||||
{
|
||||
[Fact]
|
||||
public void Properties_Accessible()
|
||||
{
|
||||
var slope = new Slope();
|
||||
Assert.Equal(0, slope.Last.Value);
|
||||
Assert.False(slope.IsHot);
|
||||
Assert.Contains("Slope", slope.Name, StringComparison.Ordinal);
|
||||
Assert.Equal(2, slope.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var slope = new Slope();
|
||||
slope.Update(new TValue(DateTime.UtcNow, 10));
|
||||
slope.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
double valueBefore = slope.Last.Value;
|
||||
|
||||
// Update with isNew=false should change the result
|
||||
slope.Update(new TValue(DateTime.UtcNow, 100), isNew: false);
|
||||
double valueAfter = slope.Last.Value;
|
||||
|
||||
Assert.NotEqual(valueBefore, valueAfter);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var slope = new Slope();
|
||||
slope.Update(new TValue(DateTime.UtcNow, 10));
|
||||
slope.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var slope = new Slope();
|
||||
slope.Update(new TValue(DateTime.UtcNow, 10));
|
||||
slope.Update(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
var resultPosInf = slope.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultPosInf.Value));
|
||||
|
||||
var resultNegInf = slope.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var slope = new Slope();
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
slope.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = slope.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
slope.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = slope.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
Slope.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllModes_ProduceSameResult()
|
||||
{
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode (static span)
|
||||
var tValues = series.Values.ToArray();
|
||||
var batchOutput = new double[tValues.Length];
|
||||
Slope.Calculate(tValues, batchOutput);
|
||||
double expected = batchOutput[^1];
|
||||
|
||||
// 2. Streaming Mode
|
||||
var streamingInd = new Slope();
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 3. TSeries Batch Mode
|
||||
var batchSeriesResult = Slope.Calculate(series);
|
||||
double tseriesResult = batchSeriesResult.Last.Value;
|
||||
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, tseriesResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculation_KnownValues()
|
||||
{
|
||||
// slope[i] = source[i] - source[i-1]
|
||||
// Data: 10, 20, 25, 30, 28
|
||||
// Slopes: 0, 10, 5, 5, -2
|
||||
|
||||
double[] data = [10, 20, 25, 30, 28];
|
||||
double[] expected = [0, 10, 5, 5, -2];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BecomesTrueAfterWarmup()
|
||||
{
|
||||
var slope = new Slope();
|
||||
|
||||
Assert.False(slope.IsHot);
|
||||
slope.Update(new TValue(DateTime.UtcNow, 10));
|
||||
Assert.False(slope.IsHot);
|
||||
slope.Update(new TValue(DateTime.UtcNow, 20));
|
||||
Assert.True(slope.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
slope.Update(new TValue(DateTime.UtcNow, i));
|
||||
}
|
||||
Assert.True(slope.IsHot);
|
||||
|
||||
slope.Reset();
|
||||
Assert.False(slope.IsHot);
|
||||
Assert.Equal(0, slope.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_Matches_Iterative()
|
||||
{
|
||||
int count = 1000;
|
||||
var data = new double[count];
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
data[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
// Iterative
|
||||
var slope = new Slope();
|
||||
var iterativeResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
iterativeResults[i] = slope.Last.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchResults = new double[count];
|
||||
Slope.Calculate(data, batchResults);
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i], batchResults[i], precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TSeries_Matches_Iterative()
|
||||
{
|
||||
int count = 1000;
|
||||
var data = new TSeries();
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var bar = gbm.Next();
|
||||
data.Add(new TValue(bar.Time, bar.Close));
|
||||
}
|
||||
|
||||
// Iterative
|
||||
var slope = new Slope();
|
||||
var iterativeResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
slope.Update(data[i]);
|
||||
iterativeResults[i] = slope.Last.Value;
|
||||
}
|
||||
|
||||
// TSeries Batch
|
||||
var slopeBatch = new Slope();
|
||||
var batchSeries = slopeBatch.Update(data);
|
||||
|
||||
// Compare
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i], batchSeries[i].Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventSubscription_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var slope = new Slope(source);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 10));
|
||||
source.Add(new TValue(DateTime.UtcNow, 20));
|
||||
|
||||
Assert.True(slope.IsHot);
|
||||
Assert.Equal(10, slope.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Slope using synthetic data with known mathematical results.
|
||||
/// </summary>
|
||||
public class SlopeValidationTests
|
||||
{
|
||||
[Fact]
|
||||
public void LinearSequence_ProducesConstantSlope()
|
||||
{
|
||||
// Linear sequence: 0, 2, 4, 6, 8, 10 (slope = 2)
|
||||
double[] data = [0, 2, 4, 6, 8, 10];
|
||||
double[] expected = [0, 2, 2, 2, 2, 2]; // First is 0 (no history), rest are 2
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConstantSequence_ProducesZeroSlope()
|
||||
{
|
||||
// Constant sequence: 5, 5, 5, 5, 5 (slope = 0)
|
||||
double[] data = [5, 5, 5, 5, 5];
|
||||
double[] expected = [0, 0, 0, 0, 0];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DecreasingSequence_ProducesNegativeSlope()
|
||||
{
|
||||
// Decreasing sequence: 10, 7, 4, 1, -2 (slope = -3)
|
||||
double[] data = [10, 7, 4, 1, -2];
|
||||
double[] expected = [0, -3, -3, -3, -3];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void QuadraticSequence_ProducesLinearSlope()
|
||||
{
|
||||
// Quadratic sequence: 0, 1, 4, 9, 16, 25 (x^2)
|
||||
// Slope: n^2 - (n-1)^2 = 2n - 1 → 1, 3, 5, 7, 9
|
||||
double[] data = [0, 1, 4, 9, 16, 25];
|
||||
double[] expected = [0, 1, 3, 5, 7, 9];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AlternatingSequence_ProducesAlternatingSlope()
|
||||
{
|
||||
// Alternating: 0, 10, 0, 10, 0
|
||||
double[] data = [0, 10, 0, 10, 0];
|
||||
double[] expected = [0, 10, -10, 10, -10];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FibonacciSequence_ProducesCorrectSlope()
|
||||
{
|
||||
// Fibonacci: 1, 1, 2, 3, 5, 8, 13
|
||||
// Slope: 0, 1, 1, 2, 3, 5
|
||||
double[] data = [1, 1, 2, 3, 5, 8, 13];
|
||||
double[] expected = [0, 0, 1, 1, 2, 3, 5];
|
||||
|
||||
var slope = new Slope();
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
var result = slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(expected[i], result.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BatchCalculation_MatchesSyntheticData()
|
||||
{
|
||||
double[] data = [0, 2, 4, 6, 8, 10];
|
||||
double[] expected = [0, 2, 2, 2, 2, 2];
|
||||
double[] output = new double[data.Length];
|
||||
|
||||
Slope.Calculate(data, output);
|
||||
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
{
|
||||
Assert.Equal(expected[i], output[i], precision: 9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LargeLinearSequence_ProducesConstantSlope()
|
||||
{
|
||||
// Generate 1000 points with slope = 0.5
|
||||
int count = 1000;
|
||||
double[] data = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
data[i] = 100.0 + i * 0.5;
|
||||
}
|
||||
|
||||
var slope = new Slope();
|
||||
// First element - no previous value, slope = 0
|
||||
slope.Update(new TValue(DateTime.UtcNow, data[0]));
|
||||
Assert.Equal(0.0, slope.Last.Value, precision: 9);
|
||||
|
||||
// Rest should have constant slope of 0.5
|
||||
for (int i = 1; i < count; i++)
|
||||
{
|
||||
slope.Update(new TValue(DateTime.UtcNow, data[i]));
|
||||
Assert.Equal(0.5, slope.Last.Value, precision: 9);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics;
|
||||
using System.Runtime.Intrinsics.Arm;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// SLOPE: First Derivative (Rate of Change)
|
||||
/// Measures the velocity of price movement - the instantaneous rate of change.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The first derivative approximates velocity: how fast the value is changing.
|
||||
///
|
||||
/// Formula:
|
||||
/// Slope_t = Value_t - Value_{t-1}
|
||||
///
|
||||
/// Key properties:
|
||||
/// - O(1) streaming complexity
|
||||
/// - Zero allocations in hot path
|
||||
/// - SIMD-optimized batch calculation
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Slope : AbstractBase
|
||||
{
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double PrevValue, double LastValidValue, int Count);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
public override bool IsHot => _state.Count >= 2;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Slope (first derivative) indicator.
|
||||
/// </summary>
|
||||
public Slope()
|
||||
{
|
||||
Name = "Slope";
|
||||
WarmupPeriod = 2;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Slope indicator with event subscription.
|
||||
/// </summary>
|
||||
public Slope(ITValuePublisher source) : this()
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void Handle(object? sender, in TValueEventArgs args) => Update(args.Value, args.IsNew);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double result;
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
if (_state.Count >= 1)
|
||||
{
|
||||
result = val - _state.PrevValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
_state.PrevValue = val;
|
||||
_state.Count = Math.Min(_state.Count + 1, 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Rollback for bar correction
|
||||
_state.LastValidValue = _p_state.LastValidValue;
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
if (_p_state.Count >= 1)
|
||||
{
|
||||
result = val - _p_state.PrevValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
_state.PrevValue = val;
|
||||
_state.Count = Math.Max(_p_state.Count, 1);
|
||||
}
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
ReadOnlySpan<double> sourceValues = source.Values;
|
||||
ReadOnlySpan<long> sourceTimes = source.Times;
|
||||
var t = new List<long>(len);
|
||||
var v = new List<double>(len);
|
||||
CollectionsMarshal.SetCount(t, len);
|
||||
CollectionsMarshal.SetCount(v, len);
|
||||
|
||||
var tSpan = CollectionsMarshal.AsSpan(t);
|
||||
var vSpan = CollectionsMarshal.AsSpan(v);
|
||||
|
||||
Calculate(sourceValues, vSpan);
|
||||
sourceTimes.CopyTo(tSpan);
|
||||
|
||||
// Prime state with last value
|
||||
if (len >= 1)
|
||||
{
|
||||
_state.PrevValue = double.IsFinite(sourceValues[len - 1]) ? sourceValues[len - 1] : _state.LastValidValue;
|
||||
_state.Count = Math.Min(len, 2);
|
||||
_state.LastValidValue = _state.PrevValue;
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
foreach (double val in source)
|
||||
{
|
||||
Update(new TValue(DateTime.MinValue, val));
|
||||
}
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source)
|
||||
{
|
||||
var slope = new Slope();
|
||||
return slope.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates first derivative (slope) for a span.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
// First element has no previous - set to 0
|
||||
output[0] = 0.0;
|
||||
if (len == 1) return;
|
||||
|
||||
int i = 1;
|
||||
|
||||
// Check if all values are finite before using SIMD
|
||||
// SIMD paths don't handle NaN/Infinity properly
|
||||
bool allFinite = !source.ContainsNonFinite();
|
||||
|
||||
// Only use SIMD if all values are finite
|
||||
if (allFinite)
|
||||
{
|
||||
// AVX512: 8 doubles at once
|
||||
if (Avx512F.IsSupported && len >= 9)
|
||||
{
|
||||
const int VectorWidth = 8;
|
||||
int simdEnd = len - VectorWidth + 1;
|
||||
ref double srcRef = ref MemoryMarshal.GetReference(source);
|
||||
ref double outRef = ref MemoryMarshal.GetReference(output);
|
||||
|
||||
for (; i < simdEnd; i += VectorWidth)
|
||||
{
|
||||
var current = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
|
||||
var prev = Vector512.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
|
||||
var diff = Avx512F.Subtract(current, prev);
|
||||
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
// AVX: 4 doubles at once
|
||||
else if (Avx.IsSupported && len >= 5)
|
||||
{
|
||||
const int VectorWidth = 4;
|
||||
int simdEnd = len - VectorWidth + 1;
|
||||
ref double srcRef = ref MemoryMarshal.GetReference(source);
|
||||
ref double outRef = ref MemoryMarshal.GetReference(output);
|
||||
|
||||
for (; i < simdEnd; i += VectorWidth)
|
||||
{
|
||||
var current = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
|
||||
var prev = Vector256.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
|
||||
var diff = Avx.Subtract(current, prev);
|
||||
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
// ARM64 Neon: 2 doubles at once
|
||||
else if (AdvSimd.Arm64.IsSupported && len >= 3)
|
||||
{
|
||||
const int VectorWidth = 2;
|
||||
int simdEnd = len - VectorWidth + 1;
|
||||
ref double srcRef = ref MemoryMarshal.GetReference(source);
|
||||
ref double outRef = ref MemoryMarshal.GetReference(output);
|
||||
|
||||
for (; i < simdEnd; i += VectorWidth)
|
||||
{
|
||||
var current = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i));
|
||||
var prev = Vector128.LoadUnsafe(ref Unsafe.Add(ref srcRef, i - 1));
|
||||
var diff = AdvSimd.Arm64.Subtract(current, prev);
|
||||
diff.StoreUnsafe(ref Unsafe.Add(ref outRef, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scalar fallback for remaining elements
|
||||
// Track last valid value forward to avoid O(n²) backward scanning
|
||||
double lastValid = 0.0;
|
||||
// Find first valid value if we're starting from the beginning
|
||||
if (i == 1)
|
||||
{
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (i > 1)
|
||||
{
|
||||
// We already processed some elements via SIMD, find last valid from processed
|
||||
for (int k = i - 1; k >= 0; k--)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double prevValid = lastValid;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double curr = source[i];
|
||||
double prev = source[i - 1];
|
||||
|
||||
// Handle NaN/Infinity using tracked last valid values
|
||||
if (double.IsFinite(curr))
|
||||
{
|
||||
lastValid = curr;
|
||||
}
|
||||
else
|
||||
{
|
||||
curr = lastValid;
|
||||
}
|
||||
|
||||
if (double.IsFinite(prev))
|
||||
{
|
||||
prevValid = prev;
|
||||
}
|
||||
else
|
||||
{
|
||||
prev = prevValid;
|
||||
}
|
||||
|
||||
output[i] = curr - prev;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
# SLOPE: First Derivative (Velocity)
|
||||
|
||||
> "The simplest measure of change reveals the most: is it going up, or going down?"
|
||||
|
||||
SLOPE measures the instantaneous rate of change—the velocity of a time series. As the first derivative, it answers the fundamental question: how fast is the value changing right now? A positive slope means ascending; negative means descending; zero means flat. This O(1) streaming implementation uses SIMD optimization for batch calculations and handles bar corrections via state rollback.
|
||||
|
||||
## Historical Context
|
||||
|
||||
The first derivative appears in Newton's calculus (1687) and forms the foundation of technical analysis. Every momentum indicator, every rate-of-change calculation, every velocity measure reduces to some form of first difference.
|
||||
|
||||
In discrete time series, the continuous derivative $\frac{dx}{dt}$ becomes the finite difference $\Delta x = x_t - x_{t-1}$. This simple subtraction underpins RSI's momentum, MACD's signal line, and every trend-following system that asks "which way is it moving?"
|
||||
|
||||
QuanTAlib implements SLOPE as a first-class indicator with full streaming support, SIMD batch optimization, and proper state management for bar corrections.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
SLOPE is a memoryless differentiator with minimal state requirements:
|
||||
|
||||
### 1. First Difference Operation
|
||||
|
||||
The fundamental operation:
|
||||
|
||||
$$
|
||||
S_t = V_t - V_{t-1}
|
||||
$$
|
||||
|
||||
where $V_t$ is the current value and $V_{t-1}$ is the previous value.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
State consists of:
|
||||
- `PrevValue`: The previous input value
|
||||
- `LastValidValue`: Last known finite value for NaN/Infinity substitution
|
||||
- `Count`: Number of values processed (0, 1, or 2+)
|
||||
|
||||
The indicator becomes "hot" (fully warmed up) after 2 values.
|
||||
|
||||
### 3. Bar Correction via Rollback
|
||||
|
||||
When `isNew=false`, the indicator rolls back to the previous state before recalculating:
|
||||
|
||||
$$
|
||||
\text{State}_{current} \leftarrow \text{State}_{previous}
|
||||
$$
|
||||
|
||||
This enables real-time bar updates without corrupting the running calculation.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Discrete First Derivative
|
||||
|
||||
For a time series $V$:
|
||||
|
||||
$$
|
||||
S_t = V_t - V_{t-1}
|
||||
$$
|
||||
|
||||
This is the forward difference approximation of the derivative.
|
||||
|
||||
### Interpretation
|
||||
|
||||
| Slope Value | Meaning |
|
||||
| :--- | :--- |
|
||||
| $S > 0$ | Price ascending (bullish) |
|
||||
| $S < 0$ | Price descending (bearish) |
|
||||
| $S = 0$ | Price unchanged (consolidation) |
|
||||
| $|S|$ large | Fast movement |
|
||||
| $|S|$ small | Slow movement |
|
||||
|
||||
### Relationship to Higher Derivatives
|
||||
|
||||
SLOPE forms the basis of the derivative chain:
|
||||
|
||||
$$
|
||||
\text{Accel}_t = \text{Slope}_t - \text{Slope}_{t-1}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{Jolt}_t = \text{Accel}_t - \text{Accel}_{t-1}
|
||||
$$
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode, Scalar)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| SUB | 1 | 1 | 1 |
|
||||
| MOV (state update) | 2 | 1 | 2 |
|
||||
| CMP (IsFinite check) | 1 | 1 | 1 |
|
||||
| **Total** | **4** | — | **~4 cycles** |
|
||||
|
||||
SLOPE is one of the fastest possible indicators—a single subtraction plus state bookkeeping.
|
||||
|
||||
### Batch Mode (512 values, SIMD)
|
||||
|
||||
| Architecture | Vector Width | Elements/Op | Total Ops (512 values) |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| AVX-512 | 512 bits | 8 doubles | 64 |
|
||||
| AVX | 256 bits | 4 doubles | 128 |
|
||||
| ARM64 Neon | 128 bits | 2 doubles | 256 |
|
||||
| Scalar | 64 bits | 1 double | 512 |
|
||||
|
||||
**Batch efficiency (512 bars):**
|
||||
|
||||
| Mode | Cycles/bar | Total (512 bars) | Speedup |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| Scalar streaming | 4 | 2,048 | 1× |
|
||||
| AVX-512 SIMD | 0.5 | 256 | 8× |
|
||||
| AVX SIMD | 1 | 512 | 4× |
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact finite difference |
|
||||
| **Timeliness** | 10/10 | Zero lag (instantaneous) |
|
||||
| **Smoothness** | 3/10 | Amplifies noise |
|
||||
| **Computational Cost** | 10/10 | Single subtraction |
|
||||
| **Memory** | 10/10 | ~48 bytes state |
|
||||
|
||||
## Validation
|
||||
|
||||
SLOPE is a fundamental operation. Validation confirms exact match with manual calculation.
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | Uses ROC (percent change) |
|
||||
| **Skender** | N/A | Uses Slope regression |
|
||||
| **Manual Calculation** | ✅ | Exact match |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Noise Amplification**: First derivatives amplify high-frequency noise. A 1% price wiggle becomes a full slope reversal. Consider smoothing the input or output for noisy data.
|
||||
|
||||
2. **Scale Dependency**: SLOPE output depends on input scale. A $100 stock has 100× larger slopes than a $1 stock. Normalize if comparing across instruments.
|
||||
|
||||
3. **Warmup Period**: SLOPE requires 2 values to produce meaningful output. The first output is always 0.
|
||||
|
||||
4. **Using isNew Incorrectly**: When processing live ticks within the same bar, use `Update(value, isNew: false)`. When a new bar opens, use `isNew: true` (default).
|
||||
|
||||
5. **Memory Footprint**: ~48 bytes per instance. Negligible for most use cases.
|
||||
|
||||
## References
|
||||
|
||||
- Newton, Isaac. (1687). "Philosophiæ Naturalis Principia Mathematica."
|
||||
- Numerical Methods: Finite Difference Approximations.
|
||||
@@ -0,0 +1,62 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Slope, Linear Regression (SLOPE)", "SLOPE", overlay=false, precision=8)
|
||||
|
||||
//@function Calculates slope (linear regression)
|
||||
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/numerics/slope.md
|
||||
//@param src Source series to calculate slope from
|
||||
//@param len Lookback period for calculation
|
||||
//@returns Slope value properly calculated
|
||||
slope(series float src, simple int len) =>
|
||||
if len <= 1
|
||||
runtime.error("Length must be greater than 1")
|
||||
var float sumX = 0.0
|
||||
var float sumY = 0.0
|
||||
var float sumXY = 0.0
|
||||
var float sumX2 = 0.0
|
||||
var int validCount = 0
|
||||
var array<float> x_values = array.new_float(len)
|
||||
var array<float> y_values = array.new_float(len)
|
||||
var int head = 0
|
||||
var int internal_time_counter = 0
|
||||
if internal_time_counter >= len
|
||||
float oldX = array.get(x_values, head)
|
||||
float oldY = array.get(y_values, head)
|
||||
if not na(oldY)
|
||||
sumX := sumX - oldX
|
||||
sumY := sumY - oldY
|
||||
sumXY := sumXY - oldX * oldY
|
||||
sumX2 := sumX2 - oldX * oldX
|
||||
validCount := validCount - 1
|
||||
float currentX = internal_time_counter
|
||||
float currentY = src
|
||||
array.set(x_values, head, currentX)
|
||||
array.set(y_values, head, currentY)
|
||||
if not na(currentY)
|
||||
sumX := sumX + currentX
|
||||
sumY := sumY + currentY
|
||||
sumXY := sumXY + currentX * currentY
|
||||
sumX2 := sumX2 + currentX * currentX
|
||||
validCount := validCount + 1
|
||||
head := (head + 1) % len
|
||||
internal_time_counter := internal_time_counter + 1
|
||||
float calculatedSlope = na
|
||||
if validCount >= 2
|
||||
float n = validCount
|
||||
float divisor = n * sumX2 - sumX * sumX
|
||||
if divisor != 0.0
|
||||
calculatedSlope := (n * sumXY - sumX * sumY) / divisor
|
||||
calculatedSlope
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=2)
|
||||
i_source = input.source(close, "Source")
|
||||
|
||||
// Calculation
|
||||
s = slope(i_source, i_period)
|
||||
|
||||
// Plot
|
||||
plot(s, "Slope", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user