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

216 lines
7.2 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.Runtime.CompilerServices;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Self-consistency and external validation for FI (Force Index).
/// Force Index = EMA(rawForce, period) where rawForce = (close - prevClose) × volume.
/// Skender implements GetForceIndex(period) — that is validated here.
/// TA-Lib, Tulip, and Ooples do not provide a Force Index function.
/// Note: FI.Update(TValue) expects pre-computed rawForce values. The Skender comparison
/// uses the same underlying formula applied to the ValidationTestData bar series.
/// </summary>
public sealed class FiValidationTests : IDisposable
{
private readonly ValidationTestData _data = new();
private readonly ITestOutputHelper _output;
private bool _disposed;
public FiValidationTests(ITestOutputHelper output)
{
_output = output;
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_data.Dispose();
_disposed = true;
}
}
// ── A) Streaming == Calculate(TSeries) self-consistency ───────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch_Period13()
{
const int N = 200;
const int period = 13;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
var rawForce = new double[N];
double prevClose = gbm.Next(isNew: true).Close;
for (int i = 0; i < N; i++)
{
var bar = gbm.Next(isNew: true);
rawForce[i] = (bar.Close - prevClose) * bar.Volume;
prevClose = bar.Close;
}
// Streaming
var fi = new Fi(period);
for (int i = 0; i < N; i++)
{
fi.Update(new TValue(DateTime.UtcNow.AddSeconds(i), rawForce[i]), isNew: true);
}
double streamVal = fi.Last.Value;
// Batch span
var output = new double[N];
Fi.Calculate(rawForce.AsSpan(), output.AsSpan(), period);
_output.WriteLine($"Streaming FI={streamVal:F10}, Batch FI={output[N - 1]:F10}");
Assert.Equal(streamVal, output[N - 1], 1e-10);
}
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch_Period2()
{
const int N = 300;
const int period = 2;
var gbm = new GBM(100.0, 0.05, 0.3, seed: 2002);
var rawForce = new double[N];
double prevClose = gbm.Next(isNew: true).Close;
for (int i = 0; i < N; i++)
{
var bar = gbm.Next(isNew: true);
rawForce[i] = (bar.Close - prevClose) * bar.Volume;
prevClose = bar.Close;
}
var fi = new Fi(period);
for (int i = 0; i < N; i++)
{
fi.Update(new TValue(DateTime.UtcNow.AddSeconds(i), rawForce[i]), isNew: true);
}
var output = new double[N];
Fi.Calculate(rawForce.AsSpan(), output.AsSpan(), period);
Assert.Equal(fi.Last.Value, output[N - 1], 1e-10);
}
// ── B) EMA formula correctness at period=1 (EMA(1)==identity) ────────────
// Fi.Calculate applies EMA(rawForce, period). At period=1 alpha=1 so output==input.
// Skender.GetForceIndex is NOT comparable here: it uses its own EMA seeding
// on OHLCV bars, producing different warmup behavior than raw-pre-computed input.
[Fact]
public void Validate_Period1_OutputEqualsInput()
{
const int N = 50;
const int period = 1;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 3003);
double[] force = new double[N];
double prev = gbm.Next(isNew: true).Close;
for (int i = 0; i < N; i++)
{
var bar = gbm.Next(isNew: true);
force[i] = (bar.Close - prev) * bar.Volume;
prev = bar.Close;
}
var output = new double[N];
Fi.Calculate(force.AsSpan(), output.AsSpan(), period);
// At period=1, EMA alpha=1.0: every output should equal the input
for (int i = 0; i < N; i++)
{
Assert.Equal(force[i], output[i], 1e-12);
}
_output.WriteLine("FI period=1 → output==input (EMA alpha=1): PASSED");
}
// ── C) Positive force → positive output ───────────────────────────────────
[Fact]
public void Validate_PositiveForce_PositiveFI()
{
const int N = 60;
const int period = 5;
// Monotonically increasing force values
double[] force = new double[N];
for (int i = 0; i < N; i++) { force[i] = 100.0 + i * 10.0; }
var output = new double[N];
Fi.Calculate(force.AsSpan(), output.AsSpan(), period);
int warmup = period + 5;
for (int i = warmup; i < N; i++)
{
Assert.True(output[i] > 0,
$"FI should be positive for positive force at index {i}, got {output[i]}");
}
_output.WriteLine("FI positive force → positive output: PASSED");
}
// ── D) Determinism across runs ────────────────────────────────────────────
[Fact]
public void Validate_Deterministic()
{
const int N = 200;
const int period = 13;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 99);
double[] force = new double[N];
double prev = gbm.Next(isNew: true).Close;
for (int i = 0; i < N; i++)
{
var b = gbm.Next(isNew: true);
force[i] = (b.Close - prev) * b.Volume;
prev = b.Close;
}
var out1 = new double[N];
var out2 = new double[N];
Fi.Calculate(force.AsSpan(), out1.AsSpan(), period);
Fi.Calculate(force.AsSpan(), out2.AsSpan(), period);
for (int i = 0; i < N; i++) { Assert.Equal(out1[i], out2[i], 15); }
_output.WriteLine("FI determinism: PASSED");
}
// ── E) Batch TSeries == Batch span ────────────────────────────────────────
[Fact]
public void Validate_BatchTSeries_Equals_BatchSpan()
{
const int period = 13;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 44);
var t0 = DateTime.UtcNow;
var times = new System.Collections.Generic.List<long>(200);
var forces = new System.Collections.Generic.List<double>(200);
double prev = gbm.Next(isNew: true).Close;
for (int i = 0; i < 200; i++)
{
var bar = gbm.Next(isNew: true);
times.Add(t0.AddSeconds(i).Ticks);
forces.Add((bar.Close - prev) * bar.Volume);
prev = bar.Close;
}
var series = new TSeries(times, forces);
var seriesResult = Fi.Batch(series, period);
var spanOut = new double[200];
Fi.Calculate(forces.ToArray().AsSpan(), spanOut.AsSpan(), period);
for (int i = 0; i < 200; i++)
{
Assert.Equal(seriesResult.Values[i], spanOut[i], 1e-9);
}
_output.WriteLine("FI Batch(TSeries) == Calculate(Span): PASSED");
}
}