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,134 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class Oc2IndicatorTests
{
[Fact]
public void Oc2Indicator_Constructor_SetsDefaults()
{
var indicator = new MidbodyIndicator();
Assert.True(indicator.ShowColdValues);
Assert.Equal("MIDBODY - Open-Close Average", indicator.Name);
Assert.False(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void Oc2Indicator_ShortName_IsOc2()
{
var indicator = new MidbodyIndicator();
Assert.Equal("MIDBODY", indicator.ShortName);
}
[Fact]
public void Oc2Indicator_MinHistoryDepths_EqualsOne()
{
var indicator = new MidbodyIndicator();
Assert.Equal(1, MidbodyIndicator.MinHistoryDepths);
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
}
[Fact]
public void Oc2Indicator_Initialize_CreatesInternalIndicator()
{
var indicator = new MidbodyIndicator();
indicator.Initialize();
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void Oc2Indicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new MidbodyIndicator();
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 Oc2Indicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new MidbodyIndicator();
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, 115, 105, 112, 1500);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void Oc2Indicator_ShowColdValues_CanBeToggled()
{
var indicator = new MidbodyIndicator();
Assert.True(indicator.ShowColdValues);
indicator.ShowColdValues = false;
Assert.False(indicator.ShowColdValues);
indicator.ShowColdValues = true;
Assert.True(indicator.ShowColdValues);
}
[Fact]
public void Oc2Indicator_SourceCodeLink_IsValid()
{
var indicator = new MidbodyIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Midbody.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void Oc2Indicator_ComputesCorrectOc2()
{
var indicator = new MidbodyIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
// O=100, C=105 → (100+105)/2 = 102.5
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(102.5, val, 10);
}
[Fact]
public void Oc2Indicator_IsHotImmediately()
{
var indicator = new MidbodyIndicator();
indicator.Initialize();
var now = DateTime.UtcNow;
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
}
+260
View File
@@ -0,0 +1,260 @@
// Midbody Unit Tests
using Xunit;
namespace QuanTAlib.Tests;
public class Oc2Tests
{
private readonly GBM _gbm;
private const double Tolerance = 1e-10;
public Oc2Tests()
{
_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_DefaultParameters_SetsCorrectValues()
{
var indicator = new Midbody();
Assert.Equal("Midbody", indicator.Name);
Assert.Equal(1, indicator.WarmupPeriod);
}
[Fact]
public void Constructor_WithSource_SubscribesToEvents()
{
var source = new TSeries();
var indicator = new Midbody(source);
source.Add(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(default, indicator.Last);
}
#endregion
#region Basic Calculation Tests
[Fact]
public void Update_Bar_ReturnsOC2()
{
var indicator = new Midbody();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result = indicator.Update(bar);
// (100 + 105) / 2 = 102.5
Assert.Equal(102.5, result.Value, Tolerance);
}
[Fact]
public void Update_Bar_MatchesTBarOC2()
{
var indicator = new Midbody();
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
var result = indicator.Update(bar);
Assert.Equal(bar.OC2, result.Value, Tolerance);
}
[Fact]
public void Update_TValue_ReturnsIdentity()
{
var indicator = new Midbody();
var result = indicator.Update(new TValue(DateTime.UtcNow, 42.0));
Assert.Equal(42.0, result.Value, Tolerance);
}
#endregion
#region State and Bar Correction Tests
[Fact]
public void IsHot_AfterFirstBar_ReturnsTrue()
{
var indicator = new Midbody();
Assert.False(indicator.IsHot);
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
Assert.True(indicator.IsHot);
}
[Fact]
public void Update_IsNewFalse_RestoresPreviousState()
{
var indicator = new Midbody();
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
indicator.Update(new TBar(time.AddMinutes(1), 105, 115, 95, 110, 1000), isNew: true);
var corrected = indicator.Update(new TBar(time.AddMinutes(1), 106, 120, 80, 111, 1000), isNew: false);
double expected = (106 + 111) * 0.5;
Assert.Equal(expected, corrected.Value, Tolerance);
}
[Fact]
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
{
var indicator = new Midbody();
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
var bar = new TBar(time.AddMinutes(1), 105, 115, 95, 110, 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 Midbody();
indicator.Update(new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000));
Assert.True(indicator.IsHot);
indicator.Reset();
Assert.False(indicator.IsHot);
Assert.Equal(default, indicator.Last);
}
#endregion
#region NaN/Infinity Robustness Tests
[Fact]
public void Update_NaN_UsesLastValidValue()
{
var indicator = new Midbody();
var time = DateTime.UtcNow;
indicator.Update(new TBar(time, 100, 110, 90, 105, 1000), isNew: true);
double validResult = indicator.Last.Value;
var nanBar = new TBar(time.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN, 1000);
var result = indicator.Update(nanBar, isNew: true);
Assert.True(double.IsFinite(result.Value));
Assert.Equal(validResult, result.Value, Tolerance);
}
#endregion
#region Consistency Tests (All Modes)
[Fact]
public void AllModes_ProduceConsistentResults()
{
var bars = GenerateBars(100);
// Mode 1: Streaming
var streaming = new Midbody();
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 = Midbody.Batch(bars);
// Mode 3: Span batch
double[] spanOutput = new double[bars.Count];
Midbody.Batch(bars.OpenValues, bars.CloseValues, spanOutput);
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingResults[i], batchResult.Values[i], Tolerance);
Assert.Equal(streamingResults[i], spanOutput[i], Tolerance);
}
}
[Fact]
public void AllBars_MatchTBarOC2()
{
var bars = GenerateBars(50);
var indicator = new Midbody();
for (int i = 0; i < bars.Count; i++)
{
var result = indicator.Update(bars[i], isNew: true);
Assert.Equal(bars[i].OC2, result.Value, Tolerance);
}
}
#endregion
#region Batch Validation Tests
[Fact]
public void Batch_MismatchedLengths_ThrowsArgumentException()
{
double[] open = new double[10];
double[] close = new double[5]; // mismatched
double[] output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Midbody.Batch(open, close, output));
Assert.Equal("close", ex.ParamName);
}
[Fact]
public void Batch_OutputTooShort_ThrowsArgumentException()
{
double[] open = new double[10];
double[] close = new double[10];
double[] output = new double[5]; // too short
var ex = Assert.Throws<ArgumentException>(() => Midbody.Batch(open, close, output));
Assert.Equal("output", ex.ParamName);
}
[Fact]
public void Batch_EmptyInput_NoOutput()
{
var bars = new TBarSeries();
var result = Midbody.Batch(bars);
Assert.Empty(result);
}
[Fact]
public void Batch_LargeDataset_NoStackOverflow()
{
var bars = GenerateBars(10_000);
double[] output = new double[bars.Count];
Midbody.Batch(bars.OpenValues, bars.CloseValues, output);
Assert.True(double.IsFinite(output[^1]));
}
#endregion
#region Event Chaining Tests
[Fact]
public void Pub_EventFires_OnUpdate()
{
var indicator = new Midbody();
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) = Midbody.Calculate(bars);
Assert.Equal(bars.Count, results.Count);
Assert.True(ind.IsHot);
}
#endregion
}
@@ -0,0 +1,218 @@
using System.Runtime.CompilerServices;
using Skender.Stock.Indicators;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation for Midbody (Open-Close Average) = (O+C)/2.
/// Cross-validated against Skender CandlePart.OC2.
/// Note: TA-Lib does not have an OC2 function.
/// </summary>
public sealed class Oc2ValidationTests : IDisposable
{
private readonly ValidationTestData _data = new();
private readonly ITestOutputHelper _output;
private bool _disposed;
public Oc2ValidationTests(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) Formula verification: (O+C)/2 ──────────────────────────────────────
[Fact]
public void Validate_Formula_Manual()
{
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 20.0, low: 5.0, close: 15.0, volume: 1000);
var ind = new Midbody();
var result = ind.Update(bar, isNew: true);
double expected = (10.0 + 15.0) / 2.0; // = 12.5
Assert.Equal(expected, result.Value, 1e-12);
_output.WriteLine($"Midbody formula: expected={expected}, actual={result.Value}: PASSED");
}
// ── B) Streaming == Batch span ────────────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch()
{
const int N = 200;
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 Midbody();
for (int i = 0; i < N; i++) { ind.Update(bars[i], isNew: true); }
double streamVal = ind.Last.Value;
// Batch span
double[] o = new double[N], c = new double[N];
for (int i = 0; i < N; i++) { o[i] = bars[i].Open; c[i] = bars[i].Close; }
var qlOut = new double[N];
Midbody.Batch(o.AsSpan(), c.AsSpan(), qlOut.AsSpan());
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
}
// ── C) Always hot after first bar ─────────────────────────────────────────
[Fact]
public void Validate_AlwaysHotAfterFirstBar()
{
var ind = new Midbody();
Assert.False(ind.IsHot);
ind.Update(new TBar(DateTime.UtcNow, 10, 12, 8, 11, 1000), isNew: true);
Assert.True(ind.IsHot);
_output.WriteLine("Midbody always hot after first bar: PASSED");
}
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
[Fact]
public void Validate_BatchBarSeries_Equals_Calculate()
{
var (results, _) = Midbody.Calculate(_data.Bars);
var batchResult = Midbody.Batch(_data.Bars);
for (int i = 0; i < _data.Bars.Count; i++)
{
Assert.Equal(batchResult.Values[i], results.Values[i], 1e-12);
}
_output.WriteLine("Midbody Batch(TBarSeries) == Calculate: PASSED");
}
// ── E) Determinism ────────────────────────────────────────────────────────
[Fact]
public void Validate_Deterministic()
{
var r1 = Midbody.Batch(_data.Bars);
var r2 = Midbody.Batch(_data.Bars);
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
_output.WriteLine("Midbody determinism: PASSED");
}
// ═══════════════════════════════════════════════════════════════════════════
// Skender.Stock.Indicators Validation — CandlePart.OC2
// ═══════════════════════════════════════════════════════════════════════════
// ── F) Skender OC2 batch validation (Midbody mapping) ───────────────────────────────────────
[Fact]
public void Validate_Against_Skender_OC2_Batch()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OC2)
.ToList();
var qlResult = Midbody.Batch(_data.Bars);
Assert.Equal(qlResult.Count, skenderResults.Count);
int count = qlResult.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
double qlVal = qlResult.Values[i];
double skVal = skenderResults[i].Value;
Assert.True(
Math.Abs(qlVal - skVal) <= ValidationHelper.SkenderTolerance,
$"Mismatch at index {i}: QuanTAlib={qlVal:G17}, Skender={skVal:G17}, Diff={Math.Abs(qlVal - skVal):G17}");
}
_output.WriteLine($"Midbody vs Skender OC2 batch: {count} bars, last {count - start} verified within {ValidationHelper.SkenderTolerance}: PASSED");
}
// ── G) Skender OC2 streaming validation (Midbody mapping) ───────────────────────────────────
[Fact]
public void Validate_Against_Skender_OC2_Streaming()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OC2)
.ToList();
var ind = new Midbody();
int count = _data.Bars.Count;
double[] streamValues = new double[count];
for (int i = 0; i < count; i++)
{
var result = ind.Update(_data.Bars[i], isNew: true);
streamValues[i] = result.Value;
}
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
double qlVal = streamValues[i];
double skVal = skenderResults[i].Value;
Assert.True(
Math.Abs(qlVal - skVal) <= ValidationHelper.SkenderTolerance,
$"Mismatch at index {i}: QuanTAlib={qlVal:G17}, Skender={skVal:G17}");
}
_output.WriteLine($"Midbody streaming vs Skender OC2: {count} bars, last {count - start} verified: PASSED");
}
// ── H) Skender OC2 span validation (Midbody mapping) ────────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Against_Skender_OC2_Span()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OC2)
.ToList();
int count = _data.Bars.Count;
double[] o = new double[count], c = new double[count];
for (int i = 0; i < count; i++)
{
o[i] = _data.Bars[i].Open;
c[i] = _data.Bars[i].Close;
}
var qlOut = new double[count];
Midbody.Batch(o.AsSpan(), c.AsSpan(), qlOut.AsSpan());
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
double qlVal = qlOut[i];
double skVal = skenderResults[i].Value;
Assert.True(
Math.Abs(qlVal - skVal) <= ValidationHelper.SkenderTolerance,
$"Span mismatch at index {i}: QuanTAlib={qlVal:G17}, Skender={skVal:G17}");
}
_output.WriteLine($"Midbody span vs Skender OC2: {count} bars, last {count - start} verified: PASSED");
}
}