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
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
@@ -0,0 +1,136 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class MidpriceIndicatorTests
{
[Fact]
public void MidpriceIndicator_Constructor_SetsDefaults()
{
var indicator = new MidpriceIndicator();
Assert.True(indicator.ShowColdValues);
Assert.Equal("MIDPRICE - Midpoint Price", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
Assert.Equal(14, indicator.Period);
}
[Fact]
public void MidpriceIndicator_ShortName_IncludesPeriod()
{
var indicator = new MidpriceIndicator();
Assert.Equal("MIDPRICE(14)", indicator.ShortName);
indicator.Period = 20;
Assert.Equal("MIDPRICE(20)", indicator.ShortName);
}
[Fact]
public void MidpriceIndicator_MinHistoryDepths_EqualsPeriod()
{
var indicator = new MidpriceIndicator { Period = 10 };
Assert.Equal(10, indicator.MinHistoryDepths);
Assert.Equal(10, ((IWatchlistIndicator)indicator).MinHistoryDepths);
indicator.Period = 25;
Assert.Equal(25, indicator.MinHistoryDepths);
}
[Fact]
public void MidpriceIndicator_Initialize_CreatesInternalIndicator()
{
var indicator = new MidpriceIndicator();
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void MidpriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new MidpriceIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
double basePrice = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void MidpriceIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new MidpriceIndicator { Period = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 105 + i, 95 + i, 102 + i, 1000);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
indicator.HistoricalData.AddBar(now.AddMinutes(10), 110, 120, 100, 115, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void MidpriceIndicator_ShowColdValues_CanBeToggled()
{
var indicator = new MidpriceIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void MidpriceIndicator_SourceCodeLink_IsValid()
{
var indicator = new MidpriceIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Midprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void MidpriceIndicator_Period_CanBeChanged()
{
var indicator = new MidpriceIndicator();
Assert.Equal(14, indicator.Period);
indicator.Period = 30;
Assert.Equal(30, indicator.Period);
}
[Fact]
public void MidpriceIndicator_IsHotAfterWarmup()
{
var indicator = new MidpriceIndicator { Period = 3 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 5; i++)
{
double basePrice = 100 + i;
indicator.HistoricalData.AddBar(now.AddMinutes(i), basePrice, basePrice + 5, basePrice - 5, basePrice + 1, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
}
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
}
+300
View File
@@ -0,0 +1,300 @@
// Midprice Unit Tests
using Xunit;
namespace QuanTAlib.Tests;
public class MidpriceTests
{
private readonly GBM _gbm;
private const double Tolerance = 1e-10;
public MidpriceTests()
{
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
}
private TBarSeries GenerateBars(int count)
{
_gbm.Reset(DateTime.UtcNow.Ticks);
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
#region Constructor Tests
[Fact]
public void Constructor_ValidPeriod_SetsCorrectValues()
{
var indicator = new Midprice(14);
Assert.Equal("Midprice(14)", indicator.Name);
Assert.Equal(14, indicator.WarmupPeriod);
}
[Fact]
public void Constructor_InvalidPeriod_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Midprice(0));
Assert.Throws<ArgumentException>(() => new Midprice(-1));
}
[Fact]
public void Constructor_Period1_IsValid()
{
var indicator = new Midprice(1);
Assert.Equal("Midprice(1)", indicator.Name);
Assert.Equal(1, indicator.WarmupPeriod);
}
[Fact]
public void Constructor_WithSource_SubscribesToEvents()
{
var source = new TSeries();
var indicator = new Midprice(source, 5);
source.Add(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(default, indicator.Last);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_SingleBar_ReturnsMidpointOfHL()
{
var indicator = new Midprice(1);
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result = indicator.Update(bar);
// Period=1: highest high = 110, lowest low = 90
// (110 + 90) / 2 = 100
Assert.Equal(100.0, result.Value, Tolerance);
}
[Fact]
public void Update_ThreeBars_UsesRollingWindow()
{
var indicator = new Midprice(3);
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
var result = indicator.Update(new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000), isNew: true);
// Highest high over 3 bars: max(105, 110, 108) = 110
// Lowest low over 3 bars: min(95, 93, 98) = 93
// Midprice = (110 + 93) / 2 = 101.5
Assert.Equal(101.5, result.Value, Tolerance);
}
[Fact]
public void Update_TValue_UsesSameValueForBothChannels()
{
var indicator = new Midprice(3);
var time = DateTime.UtcNow;
indicator.Update(new TValue(time, 100), isNew: true);
indicator.Update(new TValue(time.AddMinutes(1), 110), isNew: true);
var result = indicator.Update(new TValue(time.AddMinutes(2), 105), isNew: true);
// With TValue, H=L=value, so highest = 110, lowest = 100
// Midprice = (110 + 100) / 2 = 105
Assert.Equal(105.0, result.Value, Tolerance);
}
#endregion
#region Warmup Tests
[Fact]
public void IsHot_BeforeWarmup_ReturnsFalse()
{
var indicator = new Midprice(5);
Assert.False(indicator.IsHot);
for (int i = 0; i < 4; i++)
{
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
Assert.False(indicator.IsHot);
}
}
[Fact]
public void IsHot_AtWarmup_ReturnsTrue()
{
var indicator = new Midprice(5);
for (int i = 0; i < 5; i++)
{
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
}
Assert.True(indicator.IsHot);
}
#endregion
#region State and Bar Correction Tests
[Fact]
public void Update_IsNewFalse_RestoresPreviousState()
{
var indicator = new Midprice(3);
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
// New bar
indicator.Update(new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000), isNew: true);
// Correction on third bar
var corrected = indicator.Update(new TBar(time.AddMinutes(2), 106, 120, 80, 104, 1000), isNew: false);
// Highest high: max(105, 110, 120) = 120
// Lowest low: min(95, 93, 80) = 80
// Midprice = (120 + 80) / 2 = 100
Assert.Equal(100.0, corrected.Value, Tolerance);
}
[Fact]
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
{
var indicator = new Midprice(3);
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 105, 95, 102, 1000), isNew: true);
indicator.Update(new TBar(time.AddMinutes(1), 101, 110, 93, 108, 1000), isNew: true);
var bar = new TBar(time.AddMinutes(2), 106, 108, 98, 104, 1000);
var result1 = indicator.Update(bar, isNew: false);
var result2 = indicator.Update(bar, isNew: false);
var result3 = indicator.Update(bar, isNew: false);
Assert.Equal(result1.Value, result2.Value, Tolerance);
Assert.Equal(result2.Value, result3.Value, Tolerance);
}
[Fact]
public void Reset_ClearsState()
{
var indicator = new Midprice(5);
for (int i = 0; i < 10; i++)
{
indicator.Update(new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 105, 1000));
}
Assert.True(indicator.IsHot);
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(default, indicator.Last);
}
#endregion
#region Consistency Tests (All Modes)
[Fact]
public void AllModes_ProduceConsistentResults()
{
int period = 14;
var bars = GenerateBars(100);
// Mode 1: Streaming
var streaming = new Midprice(period);
double[] streamingResults = new double[bars.Count];
for (int i = 0; i < bars.Count; i++)
{
streamingResults[i] = streaming.Update(bars[i], isNew: true).Value;
}
// Mode 2: Batch (TBarSeries)
var batchResult = Midprice.Batch(bars, period);
// Mode 3: Span batch
double[] spanOutput = new double[bars.Count];
Midprice.Batch(bars.HighValues, bars.LowValues, spanOutput, period);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
}
}
#endregion
#region Batch Validation Tests
[Fact]
public void Batch_MismatchedLengths_ThrowsArgumentException()
{
double[] high = new double[10];
double[] low = new double[5]; // mismatched
double[] output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 5));
Assert.Equal("low", ex.ParamName);
}
[Fact]
public void Batch_OutputTooShort_ThrowsArgumentException()
{
double[] high = new double[10];
double[] low = new double[10];
double[] output = new double[5]; // too short
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 5));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_InvalidPeriod_ThrowsArgumentException()
{
double[] high = new double[10];
double[] low = new double[10];
double[] output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Midprice.Batch(high, low, output, 0));
Assert.Equal("period", ex.ParamName);
}
[Fact]
public void Batch_EmptyInput_NoOutput()
{
var bars = new TBarSeries();
var result = Midprice.Batch(bars, 5);
Assert.Empty(result);
}
[Fact]
public void Batch_LargeDataset_NoStackOverflow()
{
var bars = GenerateBars(10_000);
double[] output = new double[bars.Count];
Midprice.Batch(bars.HighValues, bars.LowValues, output, 14);
Assert.True(double.IsFinite(output[^1]));
}
#endregion
#region Event Chaining Tests
[Fact]
public void Pub_EventFires_OnUpdate()
{
var indicator = new Midprice(5);
bool fired = false;
indicator.Pub += (object? sender, in TValueEventArgs args) => fired = true;
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
Assert.True(fired);
}
[Fact]
public void Calculate_Static_ReturnsResultsAndIndicator()
{
var bars = GenerateBars(50);
var (results, ind) = Midprice.Calculate(bars, 14);
Assert.Equal(bars.Count, results.Count);
Assert.True(ind.IsHot);
}
#endregion
}
@@ -0,0 +1,166 @@
using System.Runtime.CompilerServices;
using TALib;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation for Midprice (Midpoint Price) = (Highest(H,N) + Lowest(L,N)) / 2.
/// Cross-validated against TA-Lib MIDPRICE (exact match expected).
/// Skender, Tulip, and Ooples do not implement MIDPRICE as a standalone function.
/// </summary>
public sealed class MidpriceValidationTests : IDisposable
{
private readonly ValidationTestData _data = new();
private readonly ITestOutputHelper _output;
private bool _disposed;
public MidpriceValidationTests(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) Cross-validate with TA-Lib MIDPRICE ────────────────────────────────
[Fact]
public void TALib_MidPrice_Batch_Validates_Period14()
{
const int period = 14;
double[] high = _data.HighPrices.ToArray();
double[] low = _data.LowPrices.ToArray();
// TA-Lib MidPrice
var taOut = new double[high.Length];
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
// QuanTAlib batch span
var qlOut = new double[high.Length];
Midprice.Batch(high.AsSpan(), low.AsSpan(), qlOut.AsSpan(), period);
int mismatches = 0;
for (int j = 0; j < length; j++)
{
int qi = j + offset;
double err = Math.Abs(qlOut[qi] - taOut[j]);
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
}
double mismatchRate = (double)mismatches / length;
_output.WriteLine($"TALib MIDPRICE(14): {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
Assert.Equal(0, mismatches);
}
[Fact]
public void TALib_MidPrice_Batch_Validates_Period5()
{
const int period = 5;
double[] high = _data.HighPrices.ToArray();
double[] low = _data.LowPrices.ToArray();
var taOut = new double[high.Length];
var retCode = Functions.MidPrice(high.AsSpan(), low.AsSpan(), 0..^0, taOut, out var outRange, period);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
var qlOut = new double[high.Length];
Midprice.Batch(high.AsSpan(), low.AsSpan(), qlOut.AsSpan(), period);
int mismatches = 0;
for (int j = 0; j < length; j++)
{
int qi = j + offset;
double err = Math.Abs(qlOut[qi] - taOut[j]);
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
}
_output.WriteLine($"TALib MIDPRICE(5): {length} compared, {mismatches} mismatches");
Assert.Equal(0, mismatches);
}
// ── B) Streaming == Batch span ────────────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch()
{
const int N = 200;
const int period = 14;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 1001);
var bars = new TBar[N];
for (int i = 0; i < N; i++) { bars[i] = gbm.Next(isNew: true); }
// Streaming
var ind = new Midprice(period);
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
double streamVal = ind.Last.Value;
// Batch span
double[] h = new double[N], l = new double[N];
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; }
var qlOut = new double[N];
Midprice.Batch(h.AsSpan(), l.AsSpan(), qlOut.AsSpan(), period);
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
}
// ── C) Formula verification: (HH5 + LL5) / 2 ─────────────────────────────
[Fact]
public void Validate_Formula_Manual()
{
// Prices for 5 bars: H=[10,12,15,11,13], L=[8,9,10,7,9]
// Highest H over 5 = 15, Lowest L over 5 = 7 → midprice = (15+7)/2 = 11
const int period = 5;
double[] highs = [10.0, 12.0, 15.0, 11.0, 13.0];
double[] lows = [8.0, 9.0, 10.0, 7.0, 9.0];
var output = new double[5];
Midprice.Batch(highs.AsSpan(), lows.AsSpan(), output.AsSpan(), period);
double expected = (15.0 + 7.0) / 2.0;
Assert.Equal(expected, output[4], 1e-12);
_output.WriteLine($"MIDPRICE formula: expected={expected}, actual={output[4]}: PASSED");
}
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
[Fact]
public void Validate_BatchBarSeries_Equals_Calculate()
{
const int period = 14;
var (results, _) = Midprice.Calculate(_data.Bars, period);
var batchResult = Midprice.Batch(_data.Bars, period);
for (int i = 0; i < _data.Bars.Count; i++)
{
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
}
_output.WriteLine("MIDPRICE Batch(TBarSeries) == Calculate: PASSED");
}
// ── E) Determinism ────────────────────────────────────────────────────────
[Fact]
public void Validate_Deterministic()
{
const int period = 14;
var r1 = Midprice.Batch(_data.Bars, period);
var r2 = Midprice.Batch(_data.Bars, period);
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
_output.WriteLine("MIDPRICE determinism: PASSED");
}
}