mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-20 19:48:05 +00:00
Remove multiple Pine Script indicators: SSFDSP, STARCHANNEL, STBANDS, STC, UBANDS, UCHANNEL, VWAPBANDS, and VWAPSD. These indicators were deleted to streamline the library and remove unused or redundant code.
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class FiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void FiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new FiIndicator();
|
||||
|
||||
Assert.Equal("FI - Force Index", indicator.Name);
|
||||
Assert.Equal(13, indicator.Period);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
Assert.Equal(13, indicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_ShortName_ReflectsPeriod()
|
||||
{
|
||||
var indicator = new FiIndicator { Period = 20 };
|
||||
Assert.Equal("FI(20)", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new FiIndicator { Period = 26 };
|
||||
|
||||
Assert.Equal(26, indicator.MinHistoryDepths);
|
||||
Assert.Equal(26, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_Initialize_CreatesInternalFi()
|
||||
{
|
||||
var indicator = new FiIndicator();
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new FiIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000 + (i * 100));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new FiIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i, 1000 + (i * 100));
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(30), 130, 140, 120, 135, 1500);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_Value_IsFinite()
|
||||
{
|
||||
var indicator = new FiIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
double open = 100 + i;
|
||||
double high = open + 10 + (i % 5);
|
||||
double low = open - 5;
|
||||
double close = (i % 2 == 0) ? high - 1 : low + 1;
|
||||
double volume = 1000 + (i * 100);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), open, high, low, close, volume);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(double.IsFinite(val), $"FI value {val} should be finite");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_PositiveForce_OnPriceIncrease()
|
||||
{
|
||||
var indicator = new FiIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar: baseline
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 100, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add bars with increasing prices and high volume
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + (i * 5), 110 + (i * 5), 95 + (i * 5), 105 + (i * 5), 5000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val > 0, $"FI should be positive on sustained price increase, got {val}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FiIndicator_NegativeForce_OnPriceDecrease()
|
||||
{
|
||||
var indicator = new FiIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// First bar: baseline
|
||||
indicator.HistoricalData.AddBar(now, 150, 155, 145, 150, 1000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add bars with decreasing prices and high volume
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 150 - (i * 5), 155 - (i * 5), 145 - (i * 5), 145 - (i * 5), 5000);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
}
|
||||
|
||||
double val = indicator.LinesSeries[0].GetValue(0);
|
||||
Assert.True(val < 0, $"FI should be negative on sustained price decrease, got {val}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class FiIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 10, 1, 500, 1, 0)]
|
||||
public int Period { get; set; } = 13;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Fi _fi = null!;
|
||||
private double _prevClose = double.NaN;
|
||||
private double _pPrevClose = double.NaN;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => Period;
|
||||
|
||||
public override string ShortName => $"FI({Period})";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/oscillators/fi/Fi.Quantower.cs";
|
||||
|
||||
public FiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "FI - Force Index";
|
||||
Description = "Force Index measures buying and selling pressure as EMA-smoothed price change × volume";
|
||||
|
||||
_series = new LineSeries(name: "FI", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_fi = new Fi(Period);
|
||||
_prevClose = double.NaN;
|
||||
_pPrevClose = double.NaN;
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_pPrevClose = _prevClose;
|
||||
}
|
||||
else
|
||||
{
|
||||
_prevClose = _pPrevClose;
|
||||
}
|
||||
|
||||
TBar bar = this.GetInputBar(args);
|
||||
double close = bar.Close;
|
||||
double volume = bar.Volume;
|
||||
|
||||
double rawForce;
|
||||
if (double.IsNaN(_prevClose))
|
||||
{
|
||||
rawForce = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
rawForce = (close - _prevClose) * volume;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
_prevClose = close;
|
||||
}
|
||||
|
||||
TValue input = new(bar.Time, rawForce);
|
||||
TValue result = _fi.Update(input, isNew);
|
||||
|
||||
_series.SetValue(result.Value, _fi.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class FiTests
|
||||
{
|
||||
// ── A) Constructor validation ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_Constructor_DefaultPeriod_Is13()
|
||||
{
|
||||
var fi = new Fi();
|
||||
Assert.Equal("Fi(13)", fi.Name);
|
||||
Assert.Equal(13, fi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Constructor_CustomPeriod_SetsCorrectly()
|
||||
{
|
||||
var fi = new Fi(20);
|
||||
Assert.Equal("Fi(20)", fi.Name);
|
||||
Assert.Equal(20, fi.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Constructor_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var ex = Assert.Throws<ArgumentException>(() => new Fi(0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
|
||||
ex = Assert.Throws<ArgumentException>(() => new Fi(-1));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Constructor_Period1_IsValid()
|
||||
{
|
||||
var fi = new Fi(1);
|
||||
Assert.Equal("Fi(1)", fi.Name);
|
||||
}
|
||||
|
||||
// ── B) Basic calculation ───────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_BasicCalculation_FirstBar_ReturnsInput()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// First bar: EMA initialized to input
|
||||
var val = fi.Update(new TValue(time, 400.0));
|
||||
Assert.Equal(400.0, val.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_BasicCalculation_EmaSmoothing()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// alpha = 2/(3+1) = 0.5, decay = 0.5
|
||||
// Bar 0: ema = 400, result = 400
|
||||
_ = fi.Update(new TValue(time, 400.0));
|
||||
|
||||
// Bar 1: ema = 0.5*(-400) + 0.5*400 = 0, e = 0.5, c = 1/(1-0.5) = 2
|
||||
// result = 2 * 0 = 0
|
||||
var val2 = fi.Update(new TValue(time.AddMinutes(1), -400.0));
|
||||
Assert.Equal(0.0, val2.Value, 10);
|
||||
|
||||
Assert.Equal(val2.Value, fi.Last.Value);
|
||||
Assert.Equal("Fi(3)", fi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_BasicCalculation_PositiveInput_PositiveOutput()
|
||||
{
|
||||
var fi = new Fi(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Feed constant positive raw force
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
fi.Update(new TValue(time.AddMinutes(i), 100.0));
|
||||
}
|
||||
|
||||
// After convergence, EMA of constant should equal constant
|
||||
Assert.True(fi.Last.Value > 0);
|
||||
}
|
||||
|
||||
// ── C) State + bar correction ──────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_IsNew_True_AdvancesState()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var val1 = fi.Update(new TValue(time, 100.0), isNew: true);
|
||||
var val2 = fi.Update(new TValue(time.AddMinutes(1), 200.0), isNew: true);
|
||||
|
||||
// Two distinct updates should give different values (EMA blending)
|
||||
Assert.NotEqual(val1.Value, val2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_IsNew_False_RollsBackState()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
_ = fi.Update(new TValue(time, 100.0), isNew: true);
|
||||
var val2 = fi.Update(new TValue(time.AddMinutes(1), 200.0), isNew: true);
|
||||
|
||||
// Correction: isNew=false rolls back to state after bar 1
|
||||
var val2Corrected = fi.Update(new TValue(time.AddMinutes(1), 300.0), isNew: false);
|
||||
|
||||
// Different input => different result
|
||||
Assert.NotEqual(val2.Value, val2Corrected.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_IterativeCorrections_RestoreState()
|
||||
{
|
||||
var fi = new Fi(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Build up state
|
||||
_ = fi.Update(new TValue(time, 100.0), isNew: true);
|
||||
_ = fi.Update(new TValue(time.AddMinutes(1), 200.0), isNew: true);
|
||||
|
||||
// Multiple corrections to bar 3
|
||||
_ = fi.Update(new TValue(time.AddMinutes(2), 50.0), isNew: true);
|
||||
_ = fi.Update(new TValue(time.AddMinutes(2), 80.0), isNew: false);
|
||||
_ = fi.Update(new TValue(time.AddMinutes(2), 120.0), isNew: false);
|
||||
var finalVal = fi.Update(new TValue(time.AddMinutes(2), 200.0), isNew: false);
|
||||
|
||||
// Should match a fresh computation with the final corrected value
|
||||
var fi2 = new Fi(5);
|
||||
_ = fi2.Update(new TValue(time, 100.0), isNew: true);
|
||||
_ = fi2.Update(new TValue(time.AddMinutes(1), 200.0), isNew: true);
|
||||
var expected = fi2.Update(new TValue(time.AddMinutes(2), 200.0), isNew: true);
|
||||
|
||||
Assert.Equal(expected.Value, finalVal.Value, 10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Reset_ClearsState()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
fi.Update(new TValue(time, 100.0));
|
||||
fi.Update(new TValue(time.AddMinutes(1), 200.0));
|
||||
|
||||
Assert.NotEqual(0, fi.Last.Value);
|
||||
|
||||
fi.Reset();
|
||||
Assert.False(fi.IsHot);
|
||||
Assert.Equal(0, fi.Last.Value);
|
||||
}
|
||||
|
||||
// ── D) Warmup/convergence ──────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_IsHot_FlipsWhenWarmupComplete()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
Assert.False(fi.IsHot);
|
||||
|
||||
// Feed enough data — warmup ends when e <= 1e-10
|
||||
// For period=3, alpha=0.5, decay=0.5, need ~34 bars (0.5^34 ≈ 5.8e-11)
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
fi.Update(new TValue(time.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
Assert.True(fi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_WarmupPeriod_EqualsPeriod()
|
||||
{
|
||||
var fi = new Fi(7);
|
||||
Assert.Equal(7, fi.WarmupPeriod);
|
||||
}
|
||||
|
||||
// ── E) Robustness ──────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_NaN_Input_UsesLastValid()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
fi.Update(new TValue(time, 100.0));
|
||||
fi.Update(new TValue(time.AddMinutes(1), 200.0));
|
||||
|
||||
// NaN should use last valid value
|
||||
var val = fi.Update(new TValue(time.AddMinutes(2), double.NaN));
|
||||
Assert.True(double.IsFinite(val.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Infinity_Input_UsesLastValid()
|
||||
{
|
||||
var fi = new Fi(3);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
fi.Update(new TValue(time, 100.0));
|
||||
var val = fi.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(val.Value));
|
||||
|
||||
val = fi.Update(new TValue(time.AddMinutes(2), double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(val.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_BatchNaN_Safe()
|
||||
{
|
||||
var fi = new Fi(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// Interleave NaNs with valid values
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
double value = (i % 7 == 3) ? double.NaN : (100.0 * Math.Sin(i));
|
||||
fi.Update(new TValue(time.AddMinutes(i), value));
|
||||
}
|
||||
|
||||
Assert.True(double.IsFinite(fi.Last.Value));
|
||||
}
|
||||
|
||||
// ── F) Consistency ─────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_Streaming_Matches_Batch()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 50;
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var fi = new Fi(period);
|
||||
var streamResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = fi.Update(source[i], isNew: true);
|
||||
streamResults[i] = val.Value;
|
||||
}
|
||||
|
||||
// Batch
|
||||
var batchSeries = Fi.Batch(source, period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(batchSeries[i].Value, streamResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Streaming_Matches_SpanCalculate()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 50;
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var fi = new Fi(period);
|
||||
var streamResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = fi.Update(source[i], isNew: true);
|
||||
streamResults[i] = val.Value;
|
||||
}
|
||||
|
||||
// Span calculate
|
||||
var spanOutput = new double[count];
|
||||
Fi.Calculate(source.Values, spanOutput, period);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(spanOutput[i], streamResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Eventing_Matches_Streaming()
|
||||
{
|
||||
int period = 5;
|
||||
int count = 50;
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source.Add(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.2)));
|
||||
}
|
||||
|
||||
// Streaming
|
||||
var fi1 = new Fi(period);
|
||||
var streamResults = new double[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var val = fi1.Update(source[i], isNew: true);
|
||||
streamResults[i] = val.Value;
|
||||
}
|
||||
|
||||
// Eventing via Update(TSeries) which resets and streams
|
||||
var fi2 = new Fi(period);
|
||||
var eventResults = fi2.Update(source);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Assert.Equal(eventResults[i].Value, streamResults[i], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// ── G) Span API tests ──────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[10];
|
||||
var output = new double[5];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Fi.Calculate(src, output));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_InvalidPeriod_ThrowsArgumentException()
|
||||
{
|
||||
var src = new double[10];
|
||||
var output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Fi.Calculate(src, output, 0));
|
||||
Assert.Equal("period", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_EmptyInput_NoOp()
|
||||
{
|
||||
ReadOnlySpan<double> src = [];
|
||||
Span<double> output = [];
|
||||
Fi.Calculate(src, output); // Should not throw
|
||||
Assert.True(true); // S2699: assertion confirms no-exception completion
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_NaN_HandledGracefully()
|
||||
{
|
||||
var src = new double[] { 100, double.NaN, 200, 300, double.NaN, 400 };
|
||||
var output = new double[6];
|
||||
|
||||
Fi.Calculate(src, output, 3);
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(output[i]), $"output[{i}] = {output[i]} should be finite");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_LargeData_NoStackOverflow()
|
||||
{
|
||||
int size = 10_000;
|
||||
var src = new double[size];
|
||||
var output = new double[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
src[i] = Math.Sin(i * 0.1) * 100;
|
||||
}
|
||||
|
||||
Fi.Calculate(src, output, 13);
|
||||
|
||||
Assert.True(double.IsFinite(output[size - 1]));
|
||||
}
|
||||
|
||||
// ── H) Chainability ────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Fi_PubEvent_FiresOnUpdate()
|
||||
{
|
||||
var fi = new Fi();
|
||||
bool eventFired = false;
|
||||
fi.Pub += (object? sender, in TValueEventArgs args) => eventFired = true;
|
||||
|
||||
fi.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.True(eventFired);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Chaining_EventBased()
|
||||
{
|
||||
var fi1 = new Fi(3);
|
||||
var fi2 = new Fi(fi1, 5);
|
||||
|
||||
var time = DateTime.UtcNow;
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
fi1.Update(new TValue(time.AddMinutes(i), 100.0 * Math.Sin(i * 0.3)));
|
||||
}
|
||||
|
||||
// fi2 should have received updates from fi1's Pub events
|
||||
Assert.True(double.IsFinite(fi2.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Calculate_StaticFactory_ReturnsResultsAndIndicator()
|
||||
{
|
||||
var time = DateTime.UtcNow;
|
||||
var source = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
source.Add(new TValue(time.AddMinutes(i), 100.0 + i));
|
||||
}
|
||||
|
||||
var (results, indicator) = Fi.Calculate(source, 5);
|
||||
|
||||
Assert.Equal(100, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_Prime_InitializesState()
|
||||
{
|
||||
var fi = new Fi(5);
|
||||
var source = new double[30];
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
source[i] = 100.0 + i;
|
||||
}
|
||||
|
||||
fi.Prime(source);
|
||||
Assert.True(double.IsFinite(fi.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Fi_ConstantInput_ConvergesToConstant()
|
||||
{
|
||||
var fi = new Fi(5);
|
||||
var time = DateTime.UtcNow;
|
||||
|
||||
// EMA of constant should converge to the constant
|
||||
for (int i = 0; i < 200; i++)
|
||||
{
|
||||
fi.Update(new TValue(time.AddMinutes(i), 42.0));
|
||||
}
|
||||
|
||||
Assert.Equal(42.0, fi.Last.Value, 6);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// FI: Force Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Measures buying/selling pressure as EMA-smoothed raw force.
|
||||
/// Input via Update(TValue) expects pre-computed raw force = (close − prevClose) × volume.
|
||||
/// The Quantower adapter handles OHLCV decomposition.
|
||||
///
|
||||
/// Calculation: <c>FI = EMA(rawForce, period)</c> with exponential warmup compensation.
|
||||
/// </remarks>
|
||||
/// <seealso href="fi.pine">Reference Pine Script implementation</seealso>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Fi : AbstractBase
|
||||
{
|
||||
private readonly double _alpha;
|
||||
private readonly double _decay;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double Ema,
|
||||
double E,
|
||||
bool Warmup,
|
||||
int Index,
|
||||
double LastValid);
|
||||
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
public override bool IsHot => !_s.Warmup;
|
||||
|
||||
public Fi(int period = 13)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1.", nameof(period));
|
||||
}
|
||||
|
||||
_alpha = 2.0 / (period + 1.0);
|
||||
_decay = 1.0 - _alpha;
|
||||
Name = $"Fi({period})";
|
||||
WarmupPeriod = period;
|
||||
|
||||
_s = new State(Ema: 0, E: 1.0, Warmup: true, Index: 0, LastValid: 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
public Fi(ITValuePublisher src, int period = 13) : this(period)
|
||||
{
|
||||
src.Pub += (object? sender, in TValueEventArgs e) =>
|
||||
{
|
||||
Update(e.Value, e.IsNew);
|
||||
};
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_ps = _s;
|
||||
}
|
||||
else
|
||||
{
|
||||
_s = _ps;
|
||||
}
|
||||
|
||||
var s = _s;
|
||||
|
||||
double value = input.Value;
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = s.LastValid;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.LastValid = value;
|
||||
}
|
||||
|
||||
double result;
|
||||
if (s.Index == 0)
|
||||
{
|
||||
s.Ema = value;
|
||||
result = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
s.Ema = Math.FusedMultiplyAdd(s.Ema, _decay, _alpha * value);
|
||||
|
||||
if (s.Warmup)
|
||||
{
|
||||
s.E *= _decay;
|
||||
double c = s.E > 1e-10 ? 1.0 / (1.0 - s.E) : 1.0;
|
||||
result = s.Ema * c;
|
||||
|
||||
if (s.E <= 1e-10)
|
||||
{
|
||||
s.Warmup = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
result = s.Ema;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
s.Index++;
|
||||
}
|
||||
|
||||
_s = s;
|
||||
|
||||
Last = new TValue(input.Time, result);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries source)
|
||||
{
|
||||
var t = new List<long>(source.Count);
|
||||
var v = new List<double>(source.Count);
|
||||
|
||||
Reset();
|
||||
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
var val = Update(source[i], isNew: true);
|
||||
t.Add(val.Time);
|
||||
v.Add(val.Value);
|
||||
}
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||
long baseTicks = DateTime.UtcNow.Ticks;
|
||||
|
||||
Reset();
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
Update(new TValue(new DateTime(baseTicks + (interval.Ticks * i), DateTimeKind.Utc), source[i]), isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
_s = new State(Ema: 0, E: 1.0, Warmup: true, Index: 0, LastValid: 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
public static TSeries Batch(TSeries source, int period = 13)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var t = source.Times.ToArray();
|
||||
var v = new double[source.Count];
|
||||
|
||||
Calculate(source.Values, v, period);
|
||||
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period = 13)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Output span must be the same length as input.", nameof(output));
|
||||
}
|
||||
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be >= 1.", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double alpha = 2.0 / (period + 1.0);
|
||||
double beta = 1.0 - alpha;
|
||||
|
||||
double ema = source[0];
|
||||
output[0] = ema;
|
||||
|
||||
double e = 1.0;
|
||||
bool warmup = true;
|
||||
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
double value = source[i];
|
||||
if (!double.IsFinite(value))
|
||||
{
|
||||
value = output[i - 1];
|
||||
}
|
||||
|
||||
ema = Math.FusedMultiplyAdd(ema, beta, alpha * value);
|
||||
|
||||
if (warmup)
|
||||
{
|
||||
e *= beta;
|
||||
double c = e > 1e-10 ? 1.0 / (1.0 - e) : 1.0;
|
||||
output[i] = ema * c;
|
||||
|
||||
if (e <= 1e-10)
|
||||
{
|
||||
warmup = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
output[i] = ema;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static (TSeries Results, Fi Indicator) Calculate(TSeries source, int period = 13)
|
||||
{
|
||||
var indicator = new Fi(period);
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
# FI: Force Index
|
||||
|
||||
> "Volume is the steam that makes the locomotive run. Price shows direction; volume shows conviction." -- Alexander Elder
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Category** | Oscillator |
|
||||
| **Inputs** | Raw force values (typically $\Delta\text{Close} \times \text{Volume}$) |
|
||||
| **Parameters** | `period` (default 13) |
|
||||
| **Outputs** | Single series (EMA-smoothed Force Index) |
|
||||
| **Output range** | Unbounded (centered around 0) |
|
||||
| **Warmup** | `period` bars |
|
||||
|
||||
### Key takeaways
|
||||
|
||||
- Combines price change and volume into a single measure of buying/selling pressure.
|
||||
- Raw force = (Close $-$ PrevClose) $\times$ Volume; the indicator applies EMA smoothing to the raw force.
|
||||
- Positive FI means buyers dominate; negative FI means sellers dominate. Magnitude reflects conviction.
|
||||
- The Quantower adapter handles OHLCV bar decomposition; the core `Update(TValue)` expects pre-computed raw force.
|
||||
- Uses EMA with exponential warmup compensation for bias-free early values and FMA optimization.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Alexander Elder introduced the Force Index in *Trading for a Living* (1993). Elder wanted an indicator that captured both the direction and the intensity of market moves. Price change alone tells you direction; volume alone tells you activity. Multiplying the two produces "force": a directional measure weighted by participation.
|
||||
|
||||
The raw Force Index is noisy. A single high-volume bar creates a spike that dwarfs surrounding values. Elder's solution was to smooth it with an EMA. A short-period EMA (2) provides a sensitive, fast-reacting version for short-term traders. A longer-period EMA (13) provides a smoother version for identifying intermediate-term trend strength.
|
||||
|
||||
The Force Index occupies a niche between pure momentum indicators (ROC, Momentum) and pure volume indicators (OBV, ADL). It explicitly fuses both dimensions, which makes it more informative than either alone but also more dependent on volume data quality.
|
||||
|
||||
## What It Measures and Why It Matters
|
||||
|
||||
Force Index measures the conviction behind price moves. A 5-point rise on 1 million shares has more "force" than a 5-point rise on 10,000 shares. The indicator quantifies this intuition.
|
||||
|
||||
When smoothed with a 13-period EMA, FI reveals the underlying trend of buying or selling pressure. Persistent positive FI means buyers are consistently dominant. A transition from positive to negative signals a shift in control from buyers to sellers. The zero-line crossover is the primary signal.
|
||||
|
||||
FI is particularly useful for confirming breakouts. A price breakout accompanied by rising FI suggests genuine buying interest. A breakout with declining FI suggests the move lacks volume support and may fail.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Core Formula
|
||||
|
||||
$$
|
||||
\text{RawForce}_t = (C_t - C_{t-1}) \times V_t
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{FI}_t = \text{EMA}(\text{RawForce}, N)_t
|
||||
$$
|
||||
|
||||
where:
|
||||
|
||||
- $C_t$ = close price at bar $t$
|
||||
- $V_t$ = volume at bar $t$
|
||||
- $N$ = EMA smoothing period (default 13)
|
||||
|
||||
### EMA with Warmup Compensation
|
||||
|
||||
$$
|
||||
\alpha = \frac{2}{N + 1}, \quad \beta = 1 - \alpha
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{EMA}_t^{\text{raw}} = \text{FMA}(\text{EMA}_{t-1}^{\text{raw}}, \beta, \alpha \cdot \text{RawForce}_t)
|
||||
$$
|
||||
|
||||
During warmup:
|
||||
|
||||
$$
|
||||
e_t = e_{t-1} \cdot \beta, \quad e_0 = 1
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{FI}_t = \frac{\text{EMA}_t^{\text{raw}}}{1 - e_t}
|
||||
$$
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
| Parameter | Symbol | Default | Constraint |
|
||||
|-----------|--------|---------|------------|
|
||||
| `period` | $N$ | 13 | $N \geq 1$ |
|
||||
|
||||
### Warmup Period
|
||||
|
||||
$$
|
||||
W = N
|
||||
$$
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Input Model
|
||||
|
||||
The core `Update(TValue)` method expects pre-computed raw force as input. In the Quantower adapter, the raw force is computed from OHLCV bars: `(close - prevClose) * volume`. This separation keeps the core indicator clean and reusable for any pre-computed force-like signal.
|
||||
|
||||
### 2. EMA with FMA
|
||||
|
||||
The IIR recursion uses `Math.FusedMultiplyAdd(ema, beta, alpha * value)` for a single fused operation, eliminating intermediate rounding and improving throughput.
|
||||
|
||||
### 3. Warmup Compensation
|
||||
|
||||
Exponential decay tracking ($e_t$) provides bias-free early values. When $e_t$ drops below $10^{-10}$, the warmup flag is cleared and the indicator switches to standard EMA output.
|
||||
|
||||
### 4. State Management
|
||||
|
||||
A `record struct State` holds `Ema`, `E` (warmup decay), `Warmup` flag, `Index`, and `LastValid`. The `_s` / `_ps` local-copy pattern supports bar correction.
|
||||
|
||||
### 5. Edge Cases
|
||||
|
||||
| Condition | Behavior |
|
||||
|-----------|----------|
|
||||
| `period < 1` | `ArgumentException` with `nameof(period)` |
|
||||
| `NaN` / `Infinity` input | Substitutes last valid value |
|
||||
| First bar | EMA seeded with the first input value |
|
||||
| Zero volume | Raw force = 0 (no conviction), EMA converges to zero |
|
||||
|
||||
## Interpretation and Signals
|
||||
|
||||
### Signal Zones
|
||||
|
||||
| Zone | Condition | Interpretation |
|
||||
|------|-----------|----------------|
|
||||
| Bullish | FI > 0 | Buyers in control; buying pressure dominates |
|
||||
| Bearish | FI < 0 | Sellers in control; selling pressure dominates |
|
||||
| Neutral | FI near 0 | Balance between buyers and sellers |
|
||||
|
||||
### Signal Patterns
|
||||
|
||||
- **Zero-line crossover**: FI crossing from negative to positive signals a shift to buying dominance. From positive to negative signals selling dominance.
|
||||
- **Divergence**: Price making new highs while FI peaks decline indicates weakening buying conviction. Price making new lows while FI troughs rise indicates weakening selling pressure.
|
||||
- **Spike analysis**: Large FI spikes identify climactic buying or selling. These often mark short-term exhaustion points.
|
||||
- **Trend confirmation**: Rising price with rising FI confirms the trend. Rising price with declining FI warns of potential reversal.
|
||||
|
||||
### Practical Notes
|
||||
|
||||
Elder recommended using two Force Index timeframes: a 2-period EMA for precise entry timing and a 13-period EMA for intermediate trend assessment. The 2-period version is extremely sensitive and best used with a longer-term trend filter. The 13-period version provides smoother signals suitable for position trading.
|
||||
|
||||
## Related Indicators
|
||||
|
||||
- [**Eri**](../eri/Eri.md): Elder Ray Index, another Elder creation that measures buying/selling pressure via High/Low relative to EMA, without volume.
|
||||
- [**Efi**](../../volume/efi/Efi.md): Elder Force Index in the volume category, which handles full OHLCV bar input.
|
||||
- [**Obv**](../../volume/obv/Obv.md): On-Balance Volume, cumulative volume-direction indicator without price-change weighting.
|
||||
- [**Mfi**](../../volume/mfi/Mfi.md): Money Flow Index, volume-weighted RSI that also combines price and volume.
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Batch | Streaming | Span | Notes |
|
||||
|---------|:-----:|:---------:|:----:|-------|
|
||||
| **TA-Lib** | -- | -- | -- | No direct FI function |
|
||||
| **Skender** | -- | -- | -- | `GetForceIndex()` available but not yet validated |
|
||||
| **Tulip** | -- | -- | -- | Not available |
|
||||
| **Ooples** | -- | -- | -- | Not available |
|
||||
|
||||
Internal consistency validated across streaming, batch, span, and eventing modes.
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Key Optimizations
|
||||
|
||||
- **FMA in EMA**: `Math.FusedMultiplyAdd(ema, beta, alpha * value)` for the IIR recursion.
|
||||
- **Precomputed constants**: `_alpha` and `_decay` are set once in the constructor.
|
||||
- **Zero allocation**: `record struct State` with local-copy pattern for register promotion.
|
||||
- **Aggressive inlining**: `[MethodImpl(AggressiveInlining)]` on `Update` and `Calculate`.
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count per bar |
|
||||
|-----------|---------------|
|
||||
| FMA | 1 (EMA update) |
|
||||
| MUL | 1 (alpha * value) |
|
||||
| NaN check | 1 |
|
||||
| Conditional | 1 (warmup check) |
|
||||
| MUL (warmup) | 1 (e *= beta, when active) |
|
||||
| DIV (warmup) | 1 (1 / (1 - e), when active) |
|
||||
| **Total** | **~4-6 ops** |
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Input is pre-computed force**: The core `Update(TValue)` expects `(close - prevClose) * volume` as input, not raw close prices. Passing raw close prices produces meaningless results. Use the Quantower adapter or compute raw force before calling.
|
||||
2. **Volume data quality**: FI is only as good as the volume data. In forex markets where volume is tick-based rather than share-based, FI values are less reliable.
|
||||
3. **Unbounded output**: FI has no fixed range. Visual scaling varies dramatically between instruments with different volumes and price ranges. Direct comparison across instruments is not meaningful.
|
||||
4. **Short-period noise**: A 2-period EMA FI is extremely volatile. It is meant for intrabar precision, not standalone signals.
|
||||
5. **Zero-line is not a standalone signal**: Crossing zero is necessary but not sufficient. Elder required trend confirmation (EMA slope) before acting on FI crossovers.
|
||||
6. **Warmup period affects early values**: The exponential warmup compensator provides mathematically correct early values, but practical reliability improves after $2N$ bars.
|
||||
|
||||
## References
|
||||
|
||||
- Elder, A. *Trading for a Living*. John Wiley & Sons, 1993. Chapter on Force Index.
|
||||
- Elder, A. *Come Into My Trading Room*. John Wiley & Sons, 2002.
|
||||
- Murphy, J. J. *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
|
||||
- Achelis, S. B. *Technical Analysis from A to Z*. McGraw-Hill, 2000.
|
||||
@@ -0,0 +1,56 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Force Index (FI)", "FI", overlay=false)
|
||||
|
||||
//@function Calculates Force Index — EMA-smoothed price change × volume
|
||||
//@param period EMA smoothing period for the raw force
|
||||
//@returns smoothed Force Index value
|
||||
fi(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
|
||||
float alpha = 2.0 / (period + 1.0)
|
||||
float beta = 1.0 - alpha
|
||||
|
||||
var float ema = 0.0
|
||||
var float e = 1.0
|
||||
var bool warmup = true
|
||||
var bool first = true
|
||||
var float prevClose = na
|
||||
|
||||
float src = nz(close)
|
||||
float vol = nz(volume)
|
||||
|
||||
float rawForce = not na(prevClose) ? (src - prevClose) * vol : 0.0
|
||||
prevClose := src
|
||||
|
||||
if first
|
||||
ema := rawForce
|
||||
first := false
|
||||
else
|
||||
ema := alpha * rawForce + beta * ema
|
||||
|
||||
float result = rawForce
|
||||
if warmup
|
||||
e *= beta
|
||||
float c = e > 1e-10 ? 1.0 / (1.0 - e) : 1.0
|
||||
result := ema * c
|
||||
if e <= 1e-10
|
||||
warmup := false
|
||||
else
|
||||
result := ema
|
||||
|
||||
result
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(13, "Period", minval=1, maxval=500)
|
||||
|
||||
// Calculation
|
||||
float forceIndex = fi(i_period)
|
||||
|
||||
// Plot
|
||||
plot(forceIndex, "Force Index", color.yellow, 2)
|
||||
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)
|
||||
Reference in New Issue
Block a user