mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-21 20:18:05 +00:00
filters update
This commit is contained in:
@@ -19,10 +19,14 @@ Dynamics indicators measure trend strength, speed, and direction. Unlike momentu
|
||||
| [DX](dx/Dx.md) | Directional Movement Index | Raw directional strength. Unsmoothed ADX component. |
|
||||
| [HT_TRENDMODE](ht_trendmode/Ht_trendmode.md) | Ehlers Hilbert Transform Trend vs Cycle Mode | Ehlers Hilbert Transform. Binary trend/cycle mode detection. |
|
||||
| [ICHIMOKU](ichimoku/Ichimoku.md) | Ichimoku Cloud | Five-line system. Cloud defines support/resistance zones. |
|
||||
| [IMI](imi/Imi.md) | Intraday Momentum Index | RSI variant using open-close range. Intraday overbought/oversold. |
|
||||
| [IMPULSE](impulse/Impulse.md) | Elder Impulse System | EMA + MACD histogram alignment. Color-coded trend/momentum filter. |
|
||||
| [QSTICK](qstick/Qstick.md) | Qstick | MA of (Close - Open). Positive = buying pressure. |
|
||||
| [SUPER](super/Super.md) | SuperTrend | ATR-based trailing stop. Flips on breakout. Color-coded direction. |
|
||||
| [TTM_TREND](ttm_trend/TtmTrend.md) | TTM Trend | Fast 6-period EMA. Color-coded trend from John Carter. |
|
||||
| [TTM_SQUEEZE](ttm_squeeze/TtmSqueeze.md) | TTM Squeeze | BB inside KC squeeze detection with linear regression momentum. John Carter. |
|
||||
| [VORTEX](vortex/Vortex.md) | Vortex Indicator | VI+ and VI- measure positive/negative trend movement. |
|
||||
| GATOR | Williams Gator Oscillator | Histogram of Alligator line differences. |
|
||||
| GHLA | Gann High-Low Activator | SMA(High)/SMA(Low) alternating on crossover. |
|
||||
| PFE | Polarized Fractal Efficiency | Trend efficiency: straight-line / total path distance. |
|
||||
| RAVI | Chande Range Action Verification Index | \|SMA(short) − SMA(long)\| / SMA(long) × 100. |
|
||||
| VHF | Vertical Horizontal Filter | Max-min range / sum of absolute changes. |
|
||||
|
||||
@@ -43,8 +43,8 @@ chop(simple int length) =>
|
||||
float price_range = hhv - llv
|
||||
float chop_value = na
|
||||
if win >= 2 and price_range > 0
|
||||
float log_ratio = math.log10(sum_tr / price_range)
|
||||
float log_len = math.log10(win)
|
||||
float log_ratio = math.log(sum_tr / price_range) / math.log(10)
|
||||
float log_len = math.log(win) / math.log(10)
|
||||
chop_value := 100.0 * log_ratio / log_len
|
||||
chop_value := math.max(0.0, math.min(100.0, chop_value))
|
||||
chop_value
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ImiIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void ImiIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new ImiIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("Intraday Momentum Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(0, ImiIndicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_ShortName_IncludesParameters()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 20 };
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("IMI", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new ImiIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Imi.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_Initialize_CreatesInternalImi()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 14 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist (single IMI line)
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for Period
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
|
||||
// Process update for each bar to simulate history loading
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// Line series should have a value
|
||||
double imi = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(imi));
|
||||
Assert.InRange(imi, 0.0, 100.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_AllUpBars_Returns100()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
// Up bars: close > open
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 115, 99, 110);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// All up bars should result in 100
|
||||
Assert.Equal(100.0, indicator.LinesSeries[0].GetValue(0), 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_AllDownBars_Returns0()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
// Down bars: close < open
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 110, 115, 99, 100);
|
||||
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
}
|
||||
|
||||
// All down bars should result in 0
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 0.0001);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImiIndicator_MixedBars_Returns50()
|
||||
{
|
||||
var indicator = new ImiIndicator { Period = 2 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Up bar: gain = 10
|
||||
indicator.HistoricalData.AddBar(now, 100, 115, 99, 110);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Down bar: loss = 10
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 110, 115, 99, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Equal gains and losses should result in 50
|
||||
Assert.Equal(50.0, indicator.LinesSeries[0].GetValue(0), 0.0001);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class ImiIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 1000, 1, 0)]
|
||||
public int Period { get; set; } = 14;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Imi _imi = null!;
|
||||
private readonly LineSeries _imiSeries;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"IMI {Period}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/dynamics/imi/Imi.Quantower.cs";
|
||||
|
||||
public ImiIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "Intraday Momentum Index";
|
||||
Description = "Technical indicator combining candlestick analysis with RSI-like calculation (Tushar Chande)";
|
||||
|
||||
_imiSeries = new LineSeries(name: "IMI", color: Color.Yellow, width: 2, style: LineStyle.Solid);
|
||||
|
||||
AddLineSeries(_imiSeries);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnInit()
|
||||
{
|
||||
_imi = new Imi(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue result = _imi.Update(this.GetInputBar(args), args.IsNewBar());
|
||||
|
||||
_imiSeries.SetValue(result.Value, _imi.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -1,486 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class ImiTests
|
||||
{
|
||||
private const double Precision = 1e-10;
|
||||
|
||||
#region Constructor Tests
|
||||
|
||||
[Fact]
|
||||
public void Constructor_DefaultPeriod_Is14()
|
||||
{
|
||||
var imi = new Imi();
|
||||
Assert.Equal(14, imi.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_CustomPeriod_IsSet()
|
||||
{
|
||||
var imi = new Imi(20);
|
||||
Assert.Equal(20, imi.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_Period1_IsValid()
|
||||
{
|
||||
var imi = new Imi(1);
|
||||
Assert.Equal(1, imi.Period);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_ZeroPeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Imi(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NegativePeriod_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Imi(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Name_ReflectsPeriod()
|
||||
{
|
||||
var imi = new Imi(10);
|
||||
Assert.Equal("IMI(10)", imi.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WarmupPeriod_EqualsToPeriod()
|
||||
{
|
||||
var imi = new Imi(14);
|
||||
Assert.Equal(14, imi.WarmupPeriod);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IsHot Tests
|
||||
|
||||
[Fact]
|
||||
public void IsHot_BeforeWarmup_ReturnsFalse()
|
||||
{
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
Assert.False(imi.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsHot_AfterWarmup_ReturnsTrue()
|
||||
{
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
Assert.True(imi.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_AllUpBars_Returns100()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// All bars have Close > Open (bullish candlesticks)
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 108, 1000)); // +8
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 112, 104, 111, 1000)); // +6
|
||||
imi.Update(new TBar(baseTime + 120000, 108, 115, 107, 114, 1000)); // +6
|
||||
|
||||
Assert.Equal(100.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AllDownBars_Returns0()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// All bars have Close < Open (bearish candlesticks)
|
||||
imi.Update(new TBar(baseTime, 108, 110, 99, 100, 1000)); // -8
|
||||
imi.Update(new TBar(baseTime + 60000, 111, 112, 104, 105, 1000)); // -6
|
||||
imi.Update(new TBar(baseTime + 120000, 114, 115, 107, 108, 1000)); // -6
|
||||
|
||||
Assert.Equal(0.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MixedBars_CorrectCalculation()
|
||||
{
|
||||
var imi = new Imi(4);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Up bar: gain = 5, loss = 0
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 105, 1000));
|
||||
|
||||
// Down bar: gain = 0, loss = 3
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 106, 100, 102, 1000));
|
||||
|
||||
// Up bar: gain = 4, loss = 0
|
||||
imi.Update(new TBar(baseTime + 120000, 102, 108, 101, 106, 1000));
|
||||
|
||||
// Down bar: gain = 0, loss = 2
|
||||
imi.Update(new TBar(baseTime + 180000, 106, 107, 103, 104, 1000));
|
||||
|
||||
// Gains = 5 + 4 = 9, Losses = 3 + 2 = 5
|
||||
// IMI = 100 * 9 / (9 + 5) = 100 * 9 / 14 = 64.285714...
|
||||
double expected = 100.0 * 9.0 / 14.0;
|
||||
Assert.Equal(expected, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_AllDoji_Returns50()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// All bars have Close == Open (doji candlesticks)
|
||||
imi.Update(new TBar(baseTime, 100, 105, 95, 100, 1000));
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 108, 92, 100, 1000));
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 103, 97, 100, 1000));
|
||||
|
||||
// Sum of gains = 0, Sum of losses = 0, total = 0, returns 50 (neutral)
|
||||
Assert.Equal(50.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EqualGainsAndLosses_Returns50()
|
||||
{
|
||||
var imi = new Imi(2);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Up bar: gain = 5
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 105, 1000));
|
||||
|
||||
// Down bar: loss = 5
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 106, 99, 100, 1000));
|
||||
|
||||
// Gains = 5, Losses = 5, IMI = 50
|
||||
Assert.Equal(50.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rolling Window Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_RollingWindow_DropsOldValues()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Fill with up bars
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 110, 1000)); // +10
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 110, 99, 110, 1000)); // +10
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 110, 99, 110, 1000)); // +10
|
||||
Assert.Equal(100.0, imi.Last.Value, Precision);
|
||||
|
||||
// Add a down bar - oldest up bar should drop off
|
||||
imi.Update(new TBar(baseTime + 180000, 110, 111, 99, 100, 1000)); // -10
|
||||
|
||||
// Now: gains = 10 + 10 = 20, losses = 10
|
||||
// IMI = 100 * 20 / 30 = 66.666...
|
||||
double expected = 100.0 * 20.0 / 30.0;
|
||||
Assert.Equal(expected, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bar Correction Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_BarCorrection_RestoresPreviousState()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Fill initial data
|
||||
imi.Update(new TBar(baseTime, 100, 105, 95, 103, 1000));
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 105, 95, 104, 1000));
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 105, 95, 105, 1000));
|
||||
|
||||
// Add new bar (up)
|
||||
imi.Update(new TBar(baseTime + 180000, 100, 107, 99, 106, 1000), isNew: true);
|
||||
double valueAfterNew = imi.Last.Value;
|
||||
|
||||
// Correct the bar (now down)
|
||||
imi.Update(new TBar(baseTime + 180000, 106, 107, 93, 94, 1000), isNew: false);
|
||||
double valueAfterCorrection = imi.Last.Value;
|
||||
|
||||
// Values should differ based on the correction
|
||||
Assert.NotEqual(valueAfterNew, valueAfterCorrection);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleCorrections_ProduceConsistentResults()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Fill buffer
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 102, 1000));
|
||||
}
|
||||
|
||||
// New bar
|
||||
imi.Update(new TBar(baseTime + 3 * 60000, 100, 110, 99, 108, 1000), isNew: true);
|
||||
double firstValue = imi.Last.Value;
|
||||
|
||||
// Correction 1
|
||||
imi.Update(new TBar(baseTime + 3 * 60000, 100, 115, 99, 92, 1000), isNew: false);
|
||||
|
||||
// Correction 2 - same as first new bar
|
||||
imi.Update(new TBar(baseTime + 3 * 60000, 100, 110, 99, 108, 1000), isNew: false);
|
||||
double secondValue = imi.Last.Value;
|
||||
|
||||
Assert.Equal(firstValue, secondValue, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region NaN/Infinity Handling Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNOpen_KeepsPreviousValue()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
imi.Update(new TBar(baseTime, 100, 105, 95, 103, 1000));
|
||||
double validValue = imi.Last.Value;
|
||||
|
||||
imi.Update(new TBar(baseTime + 60000, double.NaN, 110, 99, 108, 1000));
|
||||
|
||||
Assert.Equal(validValue, imi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_NaNClose_KeepsPreviousValue()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
imi.Update(new TBar(baseTime, 100, 105, 95, 103, 1000));
|
||||
double validValue = imi.Last.Value;
|
||||
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 110, 99, double.NaN, 1000));
|
||||
|
||||
Assert.Equal(validValue, imi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_InfinityValues_KeepsPreviousValue()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
imi.Update(new TBar(baseTime, 100, 105, 95, 103, 1000));
|
||||
double validValue = imi.Last.Value;
|
||||
|
||||
imi.Update(new TBar(baseTime + 60000, double.PositiveInfinity, 110, 99, 108, 1000));
|
||||
|
||||
Assert.Equal(validValue, imi.Last.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reset Tests
|
||||
|
||||
[Fact]
|
||||
public void Reset_ClearsState()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 110, 99, 108, 1000));
|
||||
}
|
||||
|
||||
Assert.True(imi.IsHot);
|
||||
|
||||
imi.Reset();
|
||||
|
||||
Assert.False(imi.IsHot);
|
||||
Assert.Equal(0, imi.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reset_AllowsFreshStart()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// All up bars
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 110, 99, 108, 1000));
|
||||
}
|
||||
Assert.Equal(100.0, imi.Last.Value, Precision);
|
||||
|
||||
imi.Reset();
|
||||
|
||||
// All down bars
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
imi.Update(new TBar(baseTime + i * 60000, 108, 110, 99, 100, 1000));
|
||||
}
|
||||
Assert.Equal(0.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Prime Tests
|
||||
|
||||
[Fact]
|
||||
public void Prime_FillsBuffer()
|
||||
{
|
||||
var imi = new Imi(5);
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100, 110, 99, 108, 1000));
|
||||
}
|
||||
|
||||
imi.Prime(source);
|
||||
|
||||
Assert.True(imi.IsHot);
|
||||
Assert.Equal(100.0, imi.Last.Value, Precision);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_ReturnsSeriesOfCorrectLength()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100 + i, 110 + i, 90 + i, 105 + i, 1000));
|
||||
}
|
||||
|
||||
var result = Imi.Batch(source);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptySource_ReturnsEmpty()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
var result = Imi.Batch(source);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_CustomPeriod_AppliesCorrectly()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100, 110, 99, 108, 1000));
|
||||
}
|
||||
|
||||
var result = Imi.Batch(source, 5);
|
||||
|
||||
Assert.Equal(20, result.Count);
|
||||
Assert.Equal(100.0, result[^1].Value, Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Calculate_ReturnsBothResultsAndIndicator()
|
||||
{
|
||||
var source = new TBarSeries();
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
source.Add(new TBar(baseTime + i * 60000, 100, 110, 99, 108, 1000));
|
||||
}
|
||||
|
||||
var (results, indicator) = Imi.Calculate(source, 10);
|
||||
|
||||
Assert.Equal(20, results.Count);
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(10, indicator.Period);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Publishing Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_PublishesEvent()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
int eventCount = 0;
|
||||
imi.Pub += (object? sender, in TValueEventArgs args) => eventCount++;
|
||||
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 105, 1000));
|
||||
|
||||
Assert.Equal(1, eventCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_EventContainsCorrectValue()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
TValue? receivedValue = null;
|
||||
imi.Pub += (object? sender, in TValueEventArgs args) => receivedValue = args.Value;
|
||||
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
imi.Update(new TBar(baseTime, 100, 110, 99, 110, 1000));
|
||||
|
||||
Assert.NotNull(receivedValue);
|
||||
Assert.Equal(imi.Last.Value, receivedValue.Value.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GBM Random Data Test
|
||||
|
||||
[Fact]
|
||||
public void Update_GbmData_ReturnsValueInRange()
|
||||
{
|
||||
var imi = new Imi(14);
|
||||
var gbm = new GBM(seed: 42);
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
imi.Update(bars[i]);
|
||||
|
||||
// IMI should always be in [0, 100]
|
||||
Assert.InRange(imi.Last.Value, 0.0, 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for IMI (Intraday Momentum Index) implementation.
|
||||
/// These tests validate the calculation against the published formula by Tushar Chande:
|
||||
/// IMI = 100 × Sum(Gains) / (Sum(Gains) + Sum(Losses))
|
||||
/// where Gain = Close - Open if Close > Open, else 0
|
||||
/// and Loss = Open - Close if Close < Open, else 0
|
||||
/// </summary>
|
||||
public sealed class ImiValidationTests : IDisposable
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public ImiValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Cleanup if needed
|
||||
}
|
||||
|
||||
#region Manual Calculation Verification
|
||||
|
||||
[Fact]
|
||||
public void ManualCalculation_SimpleUpBars()
|
||||
{
|
||||
// Given 3 up bars with known gains
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Bar 1: Open=100, Close=105 → Gain=5
|
||||
imi.Update(new TBar(baseTime, 100, 108, 98, 105, 1000));
|
||||
|
||||
// Bar 2: Open=105, Close=108 → Gain=3
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 110, 104, 108, 1000));
|
||||
|
||||
// Bar 3: Open=108, Close=110 → Gain=2
|
||||
imi.Update(new TBar(baseTime + 120000, 108, 112, 107, 110, 1000));
|
||||
|
||||
// Total gains = 5 + 3 + 2 = 10
|
||||
// Total losses = 0
|
||||
// IMI = 100 × 10 / (10 + 0) = 100
|
||||
|
||||
Assert.Equal(100.0, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine($"Gains: 5 + 3 + 2 = 10");
|
||||
_output.WriteLine($"Losses: 0");
|
||||
_output.WriteLine($"IMI = 100 × 10 / 10 = {imi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualCalculation_SimpleDownBars()
|
||||
{
|
||||
// Given 3 down bars with known losses
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Bar 1: Open=105, Close=100 → Loss=5
|
||||
imi.Update(new TBar(baseTime, 105, 108, 98, 100, 1000));
|
||||
|
||||
// Bar 2: Open=100, Close=97 → Loss=3
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 102, 95, 97, 1000));
|
||||
|
||||
// Bar 3: Open=97, Close=95 → Loss=2
|
||||
imi.Update(new TBar(baseTime + 120000, 97, 99, 93, 95, 1000));
|
||||
|
||||
// Total gains = 0
|
||||
// Total losses = 5 + 3 + 2 = 10
|
||||
// IMI = 100 × 0 / (0 + 10) = 0
|
||||
|
||||
Assert.Equal(0.0, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine($"Gains: 0");
|
||||
_output.WriteLine($"Losses: 5 + 3 + 2 = 10");
|
||||
_output.WriteLine($"IMI = 100 × 0 / 10 = {imi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ManualCalculation_MixedBars()
|
||||
{
|
||||
// Given a mix of up and down bars
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Bar 1: Open=100, Close=106 → Gain=6
|
||||
imi.Update(new TBar(baseTime, 100, 108, 98, 106, 1000));
|
||||
|
||||
// Bar 2: Open=106, Close=102 → Loss=4
|
||||
imi.Update(new TBar(baseTime + 60000, 106, 108, 100, 102, 1000));
|
||||
|
||||
// Bar 3: Open=102, Close=105 → Gain=3
|
||||
imi.Update(new TBar(baseTime + 120000, 102, 107, 101, 105, 1000));
|
||||
|
||||
// Bar 4: Open=105, Close=105 → Doji (Gain=0, Loss=0)
|
||||
imi.Update(new TBar(baseTime + 180000, 105, 108, 102, 105, 1000));
|
||||
|
||||
// Bar 5: Open=105, Close=103 → Loss=2
|
||||
imi.Update(new TBar(baseTime + 240000, 105, 107, 101, 103, 1000));
|
||||
|
||||
// Total gains = 6 + 3 = 9
|
||||
// Total losses = 4 + 2 = 6
|
||||
// IMI = 100 × 9 / (9 + 6) = 100 × 9 / 15 = 60
|
||||
|
||||
double expected = 100.0 * 9.0 / 15.0;
|
||||
Assert.Equal(expected, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine($"Gains: 6 + 0 + 3 + 0 + 0 = 9");
|
||||
_output.WriteLine($"Losses: 0 + 4 + 0 + 0 + 2 = 6");
|
||||
_output.WriteLine($"IMI = 100 × 9 / 15 = {expected}");
|
||||
_output.WriteLine($"Actual: {imi.Last.Value}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rolling Window Validation
|
||||
|
||||
[Fact]
|
||||
public void RollingWindow_DropsOldestValue()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Fill with 3 up bars (gains: 5, 5, 5)
|
||||
imi.Update(new TBar(baseTime, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 108, 98, 105, 1000)); // +5
|
||||
|
||||
Assert.Equal(100.0, imi.Last.Value, 1e-10);
|
||||
|
||||
// Add a down bar (loss: 5) - oldest gain (5) drops off
|
||||
imi.Update(new TBar(baseTime + 180000, 105, 108, 98, 100, 1000)); // -5
|
||||
|
||||
// Now: gains = 5 + 5 = 10, losses = 5
|
||||
// IMI = 100 × 10 / 15 = 66.666...
|
||||
double expected = 100.0 * 10.0 / 15.0;
|
||||
Assert.Equal(expected, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine($"After 4th bar:");
|
||||
_output.WriteLine($" Window: [+5, +5, -5]");
|
||||
_output.WriteLine($" Gains: 5 + 5 = 10");
|
||||
_output.WriteLine($" Losses: 5");
|
||||
_output.WriteLine($" IMI = {expected}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Edge Case Validation
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_AllDojiBars_Returns50()
|
||||
{
|
||||
// When all bars are doji (Open == Close), IMI should be 50 (neutral)
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
// Doji: Open == Close
|
||||
imi.Update(new TBar(baseTime + i * 60000, 100, 105, 95, 100, 1000));
|
||||
}
|
||||
|
||||
Assert.Equal(50.0, imi.Last.Value, 1e-10);
|
||||
_output.WriteLine("All doji bars (O==C) → IMI = 50 (neutral)");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_VerySmallMovements()
|
||||
{
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Very small gains
|
||||
imi.Update(new TBar(baseTime, 100.0, 100.1, 99.9, 100.0001, 1000));
|
||||
imi.Update(new TBar(baseTime + 60000, 100.0, 100.1, 99.9, 100.0002, 1000));
|
||||
imi.Update(new TBar(baseTime + 120000, 100.0, 100.1, 99.9, 100.0003, 1000));
|
||||
|
||||
// All are tiny up bars, should still be 100
|
||||
Assert.Equal(100.0, imi.Last.Value, 1e-10);
|
||||
_output.WriteLine($"Very small gains still → IMI = {imi.Last.Value}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EdgeCase_Period1()
|
||||
{
|
||||
// With period 1, each bar is its own calculation
|
||||
var imi = new Imi(1);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Up bar
|
||||
imi.Update(new TBar(baseTime, 100, 110, 95, 108, 1000));
|
||||
Assert.Equal(100.0, imi.Last.Value, 1e-10);
|
||||
|
||||
// Down bar
|
||||
imi.Update(new TBar(baseTime + 60000, 108, 110, 95, 100, 1000));
|
||||
Assert.Equal(0.0, imi.Last.Value, 1e-10);
|
||||
|
||||
// Doji
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 105, 95, 100, 1000));
|
||||
Assert.Equal(50.0, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("Period=1: Each bar → immediate IMI response");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Investopedia Example Validation
|
||||
|
||||
[Fact]
|
||||
public void InvestopediaFormula_MatchesDefinition()
|
||||
{
|
||||
// Validate against Investopedia formula:
|
||||
// IMI = (Sum of Up Closes / (Sum of Up Closes + Sum of Down Closes)) × 100
|
||||
// Where Up Close = Close - Open when Close > Open
|
||||
|
||||
var imi = new Imi(4);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Day 1: Close > Open (Up day: +3)
|
||||
imi.Update(new TBar(baseTime, 50, 54, 49, 53, 1000));
|
||||
|
||||
// Day 2: Close < Open (Down day: -2)
|
||||
imi.Update(new TBar(baseTime + 86400000, 53, 54, 50, 51, 1000));
|
||||
|
||||
// Day 3: Close > Open (Up day: +4)
|
||||
imi.Update(new TBar(baseTime + 172800000, 51, 56, 50, 55, 1000));
|
||||
|
||||
// Day 4: Close > Open (Up day: +1)
|
||||
imi.Update(new TBar(baseTime + 259200000, 55, 57, 54, 56, 1000));
|
||||
|
||||
// Sum of Up Closes = 3 + 4 + 1 = 8
|
||||
// Sum of Down Closes = 2
|
||||
// IMI = 100 × 8 / (8 + 2) = 80
|
||||
|
||||
double expected = 100.0 * 8.0 / 10.0;
|
||||
Assert.Equal(expected, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("Investopedia formula validation:");
|
||||
_output.WriteLine($" Up gains: 3 + 4 + 1 = 8");
|
||||
_output.WriteLine($" Down losses: 2");
|
||||
_output.WriteLine($" IMI = 100 × 8 / 10 = {expected}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Comparison with RSI Concept
|
||||
|
||||
[Fact]
|
||||
public void ImiVsRsiConcept_UsesIntradayNotInterday()
|
||||
{
|
||||
// IMI differs from RSI in that it uses Open-to-Close (intraday)
|
||||
// rather than Close-to-Close (interday)
|
||||
|
||||
var imi = new Imi(3);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Bar 1: Open=100, Close=105 (up bar, +5)
|
||||
// Bar 2: Open=110, Close=108 (down bar, -2)
|
||||
// Note: This is up from prev close (105→108) but down intraday!
|
||||
// Bar 3: Open=105, Close=110 (up bar, +5)
|
||||
|
||||
imi.Update(new TBar(baseTime, 100, 108, 98, 105, 1000));
|
||||
imi.Update(new TBar(baseTime + 60000, 110, 112, 106, 108, 1000)); // Intraday down
|
||||
imi.Update(new TBar(baseTime + 120000, 105, 112, 104, 110, 1000));
|
||||
|
||||
// Gains = 5 + 5 = 10
|
||||
// Losses = 2
|
||||
// IMI = 100 × 10 / 12 = 83.333...
|
||||
|
||||
double expected = 100.0 * 10.0 / 12.0;
|
||||
Assert.Equal(expected, imi.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine("IMI uses Open-to-Close (intraday), not Close-to-Close (interday)");
|
||||
_output.WriteLine($"Bar 2: Opens at 110, closes at 108 → DOWN day for IMI");
|
||||
_output.WriteLine($"IMI = {imi.Last.Value:F4}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Overbought/Oversold Levels
|
||||
|
||||
[Fact]
|
||||
public void OverboughtLevel_Above70()
|
||||
{
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Create scenario with IMI > 70 (overbought)
|
||||
// Need gains > 2.33 × losses for IMI > 70
|
||||
// 4 up bars (+5 each), 1 down bar (-3)
|
||||
// Gains = 20, Losses = 3
|
||||
// IMI = 100 × 20/23 = 86.96
|
||||
|
||||
imi.Update(new TBar(baseTime, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 60000, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 120000, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 180000, 100, 108, 98, 105, 1000)); // +5
|
||||
imi.Update(new TBar(baseTime + 240000, 100, 102, 95, 97, 1000)); // -3
|
||||
|
||||
Assert.True(imi.Last.Value > 70);
|
||||
_output.WriteLine($"Overbought (>70): IMI = {imi.Last.Value:F2}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OversoldLevel_Below30()
|
||||
{
|
||||
var imi = new Imi(5);
|
||||
long baseTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
|
||||
// Create scenario with IMI < 30 (oversold)
|
||||
// Need losses > 2.33 × gains for IMI < 30
|
||||
// 4 down bars (-5 each), 1 up bar (+3)
|
||||
// Gains = 3, Losses = 20
|
||||
// IMI = 100 × 3/23 = 13.04
|
||||
|
||||
imi.Update(new TBar(baseTime, 105, 108, 98, 100, 1000)); // -5
|
||||
imi.Update(new TBar(baseTime + 60000, 105, 108, 98, 100, 1000)); // -5
|
||||
imi.Update(new TBar(baseTime + 120000, 105, 108, 98, 100, 1000)); // -5
|
||||
imi.Update(new TBar(baseTime + 180000, 105, 108, 98, 100, 1000)); // -5
|
||||
imi.Update(new TBar(baseTime + 240000, 100, 108, 98, 103, 1000)); // +3
|
||||
|
||||
Assert.True(imi.Last.Value < 30);
|
||||
_output.WriteLine($"Oversold (<30): IMI = {imi.Last.Value:F2}");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// IMI: Intraday Momentum Index
|
||||
// Developed by Tushar Chande
|
||||
// Combines candlestick analysis with RSI-like calculation
|
||||
// Uses gain/loss based on intraday Open-Close relationship
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// IMI: Intraday Momentum Index
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A technical indicator developed by Tushar Chande that combines candlestick analysis
|
||||
/// with RSI-like overbought/oversold signals. Unlike RSI which uses close-to-close changes,
|
||||
/// IMI uses the relationship between each bar's open and close prices.
|
||||
///
|
||||
/// Calculation:
|
||||
/// <c>Gain = Close - Open (when Close > Open, otherwise 0)</c>
|
||||
/// <c>Loss = Open - Close (when Close < Open, otherwise 0)</c>
|
||||
/// <c>IMI = 100 × Sum(Gains, n) / (Sum(Gains, n) + Sum(Losses, n))</c>
|
||||
///
|
||||
/// Key Levels:
|
||||
/// - Above 70: Overbought condition
|
||||
/// - Below 30: Oversold condition
|
||||
/// - 50: Neutral (equal up and down momentum)
|
||||
///
|
||||
/// Sources:
|
||||
/// - Investopedia: https://www.investopedia.com/terms/i/intraday-momentum-index-imi.asp
|
||||
/// - CQG: https://help.cqg.com/cqgic/25/Documents/intradaymomentumindeximi.htm
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Imi : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _gains;
|
||||
private readonly RingBuffer _losses;
|
||||
|
||||
// Rolling sums for O(1) updates
|
||||
private double _gainSum;
|
||||
private double _lossSum;
|
||||
|
||||
// Bar correction state
|
||||
private double _savedGainSum;
|
||||
private double _savedLossSum;
|
||||
|
||||
/// <summary>
|
||||
/// Display name for the indicator.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Event publisher for value updates.
|
||||
/// </summary>
|
||||
public event TValuePublishedHandler? Pub;
|
||||
|
||||
/// <summary>
|
||||
/// Current IMI value.
|
||||
/// </summary>
|
||||
public TValue Last { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the indicator has enough data for a full period calculation.
|
||||
/// </summary>
|
||||
public bool IsHot => _gains.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// The period parameter.
|
||||
/// </summary>
|
||||
public int Period => _period;
|
||||
|
||||
/// <summary>
|
||||
/// The number of bars required for the indicator to warm up.
|
||||
/// </summary>
|
||||
public int WarmupPeriod { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates IMI indicator with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Lookback period (must be >= 1)</param>
|
||||
public Imi(int period = 14)
|
||||
{
|
||||
if (period < 1)
|
||||
{
|
||||
throw new ArgumentException("Period must be at least 1", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
Name = $"IMI({period})";
|
||||
WarmupPeriod = period;
|
||||
|
||||
_gains = new RingBuffer(period);
|
||||
_losses = new RingBuffer(period);
|
||||
|
||||
_gainSum = 0.0;
|
||||
_lossSum = 0.0;
|
||||
_savedGainSum = 0.0;
|
||||
_savedLossSum = 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the indicator state.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset()
|
||||
{
|
||||
_gains.Clear();
|
||||
_losses.Clear();
|
||||
_gainSum = 0.0;
|
||||
_lossSum = 0.0;
|
||||
_savedGainSum = 0.0;
|
||||
_savedLossSum = 0.0;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void PubEvent(TValue value, bool isNew = true) =>
|
||||
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
|
||||
|
||||
/// <summary>
|
||||
/// Updates the IMI indicator with a new bar.
|
||||
/// </summary>
|
||||
/// <param name="input">The price bar (Open, Close required)</param>
|
||||
/// <param name="isNew">True for new bar, false for update of current bar</param>
|
||||
/// <returns>The current IMI value</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar input, bool isNew = true)
|
||||
{
|
||||
double open = input.Open;
|
||||
double close = input.Close;
|
||||
|
||||
// Handle NaN/Infinity inputs
|
||||
if (!double.IsFinite(open) || !double.IsFinite(close))
|
||||
{
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// Save state for potential correction
|
||||
_savedGainSum = _gainSum;
|
||||
_savedLossSum = _lossSum;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore state for correction
|
||||
_gainSum = _savedGainSum;
|
||||
_lossSum = _savedLossSum;
|
||||
}
|
||||
|
||||
// Calculate gain and loss for this bar
|
||||
double gain = 0.0;
|
||||
double loss = 0.0;
|
||||
|
||||
if (close > open)
|
||||
{
|
||||
gain = close - open;
|
||||
}
|
||||
else if (close < open)
|
||||
{
|
||||
loss = open - close;
|
||||
}
|
||||
// When close == open, both gain and loss remain 0
|
||||
|
||||
// Update rolling sums: subtract old value if buffer is full
|
||||
if (_gains.IsFull)
|
||||
{
|
||||
_gainSum -= _gains[0];
|
||||
_lossSum -= _losses[0];
|
||||
}
|
||||
|
||||
// Add new values to buffers
|
||||
_gains.Add(gain, isNew);
|
||||
_losses.Add(loss, isNew);
|
||||
_gainSum += gain;
|
||||
_lossSum += loss;
|
||||
|
||||
// Calculate IMI
|
||||
double total = _gainSum + _lossSum;
|
||||
double imi = total > 0 ? 100.0 * _gainSum / total : 50.0;
|
||||
|
||||
Last = new TValue(input.Time, imi);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates IMI for the entire bar series.
|
||||
/// </summary>
|
||||
public TSeries Update(TBarSeries source)
|
||||
{
|
||||
if (source.Count == 0)
|
||||
{
|
||||
return new TSeries([], []);
|
||||
}
|
||||
|
||||
int len = source.Count;
|
||||
var tList = new List<long>(len);
|
||||
var vList = new List<double>(len);
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
var bar = source[i];
|
||||
Update(bar, isNew: true);
|
||||
tList.Add(bar.Time);
|
||||
vList.Add(Last.Value);
|
||||
}
|
||||
|
||||
return new TSeries(tList, vList);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Primes the indicator with historical bar data.
|
||||
/// </summary>
|
||||
public void Prime(TBarSeries source)
|
||||
{
|
||||
for (int i = 0; i < source.Count; i++)
|
||||
{
|
||||
Update(source[i], isNew: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates IMI for the entire bar series using default parameters.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var imi = new Imi();
|
||||
return imi.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates IMI for the entire bar series using custom period.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source, int period)
|
||||
{
|
||||
var imi = new Imi(period);
|
||||
return imi.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates IMI and returns both results and the warm indicator.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Imi Indicator) Calculate(TBarSeries source, int period = 14)
|
||||
{
|
||||
var imi = new Imi(period);
|
||||
var results = imi.Update(source);
|
||||
return (results, imi);
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
# IMI: Intraday Momentum Index
|
||||
|
||||
The Intraday Momentum Index measures buying and selling pressure using the open-to-close relationship within each bar, rather than the close-to-close changes used by RSI. Each bar is classified as a gain (close > open) or loss (close < open), with the magnitude being the absolute open-close difference. Rolling sums of gains and losses over the lookback period produce an RSI-like ratio scaled to 0-100. This bridges Japanese candlestick analysis with Western oscillator theory: bullish candles contribute to the gain sum, bearish candles contribute to the loss sum. Unlike RSI, IMI does not require a previous close and uses simple rolling sums rather than exponential smoothing, making it more responsive but noisier. Output is bounded 0-100 with conventional overbought (>70) and oversold (<30) zones.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Tushar Chande introduced the Intraday Momentum Index in *The New Technical Trader* (1994), alongside innovations like the Chande Momentum Oscillator. Chande observed that traditional momentum indicators like RSI ignored the intraday price action captured by candlestick patterns. By using the open-close relationship instead of close-close changes, IMI measures a fundamentally different quantity: the directional conviction *within* each bar rather than the change *between* bars. On daily charts, the open-close relationship has clear meaning — it captures overnight positioning gaps plus session direction. The indicator is self-contained within each bar, requiring no previous bar's close, which makes it particularly clean for session-based analysis. The formula structure deliberately mirrors RSI (sum of gains over total) to provide familiar overbought/oversold levels while measuring intra-session momentum.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Gain/Loss Classification
|
||||
|
||||
Each bar is classified based on the open-close relationship:
|
||||
|
||||
$$G_t = \begin{cases} C_t - O_t & \text{if } C_t > O_t \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
$$L_t = \begin{cases} O_t - C_t & \text{if } C_t < O_t \\ 0 & \text{otherwise} \end{cases}$$
|
||||
|
||||
Doji bars ($C = O$) contribute zero to both sums.
|
||||
|
||||
### 2. Rolling Sums
|
||||
|
||||
Simple rolling sums over the lookback window (no exponential smoothing):
|
||||
|
||||
$$\text{SumGains}_t = \sum_{i=t-N+1}^{t} G_i$$
|
||||
|
||||
$$\text{SumLosses}_t = \sum_{i=t-N+1}^{t} L_i$$
|
||||
|
||||
Implemented with ring buffers and incremental add/subtract for $O(1)$ per bar.
|
||||
|
||||
### 3. IMI Value
|
||||
|
||||
$$\text{IMI}_t = 100 \times \frac{\text{SumGains}_t}{\text{SumGains}_t + \text{SumLosses}_t}$$
|
||||
|
||||
When both sums are zero (all doji bars in window), IMI defaults to 50.0 (neutral).
|
||||
|
||||
### 4. Complexity
|
||||
|
||||
- **Time:** $O(1)$ per bar — rolling sum add/subtract
|
||||
- **Space:** $O(N)$ — two ring buffers for gain and loss history
|
||||
- **Warmup:** $N$ bars
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Symbol | Parameter | Default | Constraint |
|
||||
|--------|-----------|---------|------------|
|
||||
| $N$ | period | 14 | $N \geq 1$ |
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
Initialize:
|
||||
gainBuf = RingBuffer(period)
|
||||
lossBuf = RingBuffer(period)
|
||||
gainSum = lossSum = 0
|
||||
bar_count = 0
|
||||
|
||||
On each bar (open, close, isNew):
|
||||
if !isNew: restore previous state
|
||||
|
||||
// Classify bar
|
||||
diff = close - open
|
||||
gain = diff > 0 ? diff : 0
|
||||
loss = diff < 0 ? -diff : 0
|
||||
|
||||
// Update rolling sums
|
||||
if gainBuf is full:
|
||||
gainSum -= gainBuf.Oldest
|
||||
lossSum -= lossBuf.Oldest
|
||||
gainBuf.Add(gain)
|
||||
lossBuf.Add(loss)
|
||||
gainSum += gain
|
||||
lossSum += loss
|
||||
|
||||
// IMI calculation
|
||||
total = gainSum + lossSum
|
||||
IMI = total > 0 ? 100 × gainSum / total : 50.0
|
||||
|
||||
output = IMI
|
||||
```
|
||||
|
||||
### IMI vs RSI Comparison
|
||||
|
||||
| Property | RSI | IMI |
|
||||
|----------|-----|-----|
|
||||
| Input | Close-to-close change | Open-to-close change |
|
||||
| Measures | Inter-session momentum | Intra-session momentum |
|
||||
| Smoothing | Wilder's RMA (exponential) | Simple rolling sum |
|
||||
| Previous bar | Required ($C_{t-1}$) | Not required (self-contained) |
|
||||
| Response | Smoother, more lag | More responsive, noisier |
|
||||
| Range | 0-100 | 0-100 |
|
||||
|
||||
### Interpretation
|
||||
|
||||
| IMI Value | Meaning |
|
||||
|-----------|---------|
|
||||
| > 70 | Overbought — strong bullish intra-session pressure |
|
||||
| < 30 | Oversold — strong bearish intra-session pressure |
|
||||
| 50 | Neutral — balanced buying/selling within bars |
|
||||
| Rising toward 70 | Increasing proportion of bullish candles |
|
||||
| Falling toward 30 | Increasing proportion of bearish candles |
|
||||
|
||||
### Timeframe Sensitivity
|
||||
|
||||
On daily charts, the open-close relationship captures overnight gaps plus session direction — the most informative timeframe for IMI. On very short intraday charts (1-minute), the open-close relationship carries less structural information since the open price has minimal gap significance. Choose timeframes where the opening price carries genuine information about session sentiment.
|
||||
|
||||
### OHLC Requirement
|
||||
|
||||
IMI requires both Open and Close prices per bar. It implements `ITValuePublisher` directly rather than `AbstractBase` since it operates on `TBar` (OHLC) input, not single `TValue` input.
|
||||
|
||||
## Resources
|
||||
|
||||
- Chande, T.S. & Kroll, S. — *The New Technical Trader* (John Wiley & Sons, 1994)
|
||||
- PineScript reference: `imi.pine` in indicator directory
|
||||
@@ -1,44 +0,0 @@
|
||||
// The MIT License (MIT)
|
||||
// © mihakralj
|
||||
//@version=6
|
||||
indicator("Intraday Momentum Index (IMI)", "IMI", overlay=false)
|
||||
|
||||
//@function Calculates IMI using intraday price ranges (open vs close)
|
||||
//@param period Number of bars used in the calculation
|
||||
//@returns IMI value (0-100)
|
||||
//@optimized Uses circular buffer for O(1) per-bar complexity
|
||||
imi(simple int period) =>
|
||||
if period <= 0
|
||||
runtime.error("Period must be greater than 0")
|
||||
float gain = 0.0
|
||||
float loss = 0.0
|
||||
if close > open
|
||||
gain := close - open
|
||||
else if close < open
|
||||
loss := open - close
|
||||
var array<float> gain_buffer = array.new_float(period, 0.0)
|
||||
var array<float> loss_buffer = array.new_float(period, 0.0)
|
||||
var int idx = 0
|
||||
var float gain_sum = 0.0
|
||||
var float loss_sum = 0.0
|
||||
gain_sum -= array.get(gain_buffer, idx)
|
||||
loss_sum -= array.get(loss_buffer, idx)
|
||||
array.set(gain_buffer, idx, gain)
|
||||
array.set(loss_buffer, idx, loss)
|
||||
gain_sum += gain
|
||||
loss_sum += loss
|
||||
idx := (idx + 1) % period
|
||||
float total = gain_sum + loss_sum
|
||||
float imi_value = total != 0.0 ? 100.0 * gain_sum / total : 50.0
|
||||
imi_value
|
||||
|
||||
// ---------- Main loop ----------
|
||||
|
||||
// Inputs
|
||||
i_period = input.int(14, "Period", minval=1, tooltip="Number of bars used in the calculation")
|
||||
|
||||
// Calculate IMI
|
||||
imi_value = imi(i_period)
|
||||
|
||||
// Plot
|
||||
plot(imi_value, "IMI", color=color.yellow, linewidth=2)
|
||||
Reference in New Issue
Block a user