Files
Miha Kralj 060649192f docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files
- 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
2026-03-12 12:34:16 -07:00

333 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}