mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-01 03:07:43 +00:00
060649192f
- Remove 'C# Implementation Considerations' sections from 34 indicator .md files - Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.) - Move test files into tests/ subdirectories for consistent project structure - Add trader-focused bullet points to indicator documentation
487 lines
13 KiB
C#
487 lines
13 KiB
C#
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
|
|
}
|