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

251 lines
7.5 KiB
C#

namespace QuanTAlib.Tests;
public class BopTests
{
[Fact]
public void BasicCalculation()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
// Open=10, High=20, Low=5, Close=15
// Range = 20 - 5 = 15
// Diff = 15 - 10 = 5
// BOP = 5 / 15 = 0.3333...
var result = bop.Update(bar);
Assert.Equal(1.0 / 3.0, result.Value, 6);
}
[Fact]
public void HighEqualsLow()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 10, 10, 10, 100);
// Range = 0
// BOP should be 0
var result = bop.Update(bar);
Assert.Equal(0, result.Value);
}
[Fact]
public void BuyersDominate()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 20, 10, 20, 100);
// Open=10, High=20, Low=10, Close=20
// Range = 10
// Diff = 10
// BOP = 1
var result = bop.Update(bar);
Assert.Equal(1, result.Value);
}
[Fact]
public void SellersDominate()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 20, 20, 10, 10, 100);
// Open=20, High=20, Low=10, Close=10
// Range = 10
// Diff = -10
// BOP = -1
var result = bop.Update(bar);
Assert.Equal(-1, result.Value);
}
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
// BOP is stateless - each bar is calculated independently
// isNew parameter is accepted but doesn't affect stateless calculation
var bop = new Bop();
// bar1: BOP = (15-10)/(20-5) = 5/15 = 0.333
var bar1 = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
// bar2: BOP = (25-15)/(30-10) = 10/20 = 0.5
var bar2 = new TBar(DateTime.UtcNow, 15, 30, 10, 25, 100);
bop.Update(bar1, isNew: true);
var val1 = bop.Last.Value;
bop.Update(bar2, isNew: true);
var val2 = bop.Last.Value;
// Different bars produce different BOP values
Assert.NotEqual(val1, val2);
Assert.Equal(1.0 / 3.0, val1, 6); // bar1 BOP
Assert.Equal(0.5, val2, 6); // bar2 BOP
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
// BOP is stateless - each bar is calculated independently
// isNew=false still calculates the new value
var bop = new Bop();
var bar1 = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 10, 25, 5, 20, 100);
var val1 = bop.Update(bar1, isNew: true);
var val2 = bop.Update(bar2, isNew: false);
// Different bars produce different values (BOP has no state to preserve)
Assert.NotEqual(val1.Value, val2.Value);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
var originalValue = bop.Update(bar, isNew: true);
for (int i = 0; i < 5; i++)
{
var modified = new TBar(bar.Time, bar.Open, bar.High + i, bar.Low, bar.Close, bar.Volume);
bop.Update(modified, isNew: false);
}
var restored = bop.Update(bar, isNew: false);
Assert.Equal(originalValue.Value, restored.Value, 9);
}
[Fact]
public void Reset_ClearsState()
{
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
bop.Update(bar);
bop.Reset();
Assert.Equal(0, bop.Last.Value);
}
[Fact]
public void IsHot_AlwaysTrueForBop()
{
// BOP has no warmup - IsHot is always true (static property)
Assert.True(Bop.IsHot);
var bop = new Bop();
var bar = new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100);
bop.Update(bar);
Assert.True(Bop.IsHot);
}
[Fact]
public void NaN_Input_ProducesNaN()
{
// BOP is stateless and doesn't track last valid value
// NaN input propagates through the calculation
var bop = new Bop();
var barNaN = new TBar(DateTime.UtcNow, double.NaN, 20, 5, 15, 100);
var result = bop.Update(barNaN);
// BOP = (Close - Open) / (High - Low) = (15 - NaN) / (20 - 5) = NaN
Assert.True(double.IsNaN(result.Value));
}
[Fact]
public void Infinity_Input_ProducesInfinity()
{
// BOP is stateless and doesn't track last valid value
// Infinity input propagates through the calculation
var bop = new Bop();
var barInf = new TBar(DateTime.UtcNow, double.PositiveInfinity, 20, 5, 15, 100);
var result = bop.Update(barInf);
// BOP = (Close - Open) / (High - Low) = (15 - Infinity) / (20 - 5) = -Infinity
Assert.True(double.IsInfinity(result.Value));
}
[Fact]
public void BatchMatchesStreaming()
{
var bop = new Bop();
var bars = new TBarSeries();
bars.Add(new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100));
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(1), 15, 25, 10, 20, 100));
var batchResult = Bop.Update(bars);
bop.Reset();
var streamResult1 = bop.Update(bars[0]);
var streamResult2 = bop.Update(bars[1]);
Assert.Equal(batchResult[0].Value, streamResult1.Value);
Assert.Equal(batchResult[1].Value, streamResult2.Value);
}
[Fact]
public void SpanMatchesBatch()
{
var bars = new TBarSeries();
bars.Add(new TBar(DateTime.UtcNow, 10, 20, 5, 15, 100));
bars.Add(new TBar(DateTime.UtcNow.AddMinutes(1), 15, 25, 10, 20, 100));
var batchResult = Bop.Batch(bars);
var output = new double[bars.Count];
Bop.Batch(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, output);
Assert.Equal(batchResult[0].Value, output[0]);
Assert.Equal(batchResult[1].Value, output[1]);
}
[Fact]
public void AllModes_ProduceSameResult()
{
var gbm = new GBM(seed: 123);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 1. Batch Mode
var batchResult = Bop.Batch(bars);
double expected = batchResult.Last.Value;
// 2. Span Mode
var spanOutput = new double[bars.Count];
Bop.Batch(bars.Open.Values, bars.High.Values, bars.Low.Values, bars.Close.Values, spanOutput);
double spanResult = spanOutput[^1];
// 3. Streaming Mode
var streamBop = new Bop();
for (int i = 0; i < bars.Count; i++)
{
streamBop.Update(bars[i]);
}
double streamResult = streamBop.Last.Value;
Assert.Equal(expected, spanResult, 9);
Assert.Equal(expected, streamResult, 9);
}
[Fact]
public void SpanBatch_ProcessesMinimumLength()
{
// BOP.Calculate processes the minimum length of all arrays
// It doesn't throw when output is smaller - it just processes fewer elements
double[] open = [1, 2, 3];
double[] high = [2, 3, 4];
double[] low = [0, 1, 2];
double[] close = [1.5, 2.5, 3.5];
double[] smallOutput = new double[2];
// This should process 2 elements (minimum of all array lengths)
Bop.Batch(open, high, low, close, smallOutput);
// Verify values are calculated for the first 2 elements
Assert.Equal(0.25, smallOutput[0], 6); // (1.5 - 1) / (2 - 0) = 0.5/2 = 0.25
Assert.Equal(0.25, smallOutput[1], 6); // (2.5 - 2) / (3 - 1) = 0.5/2 = 0.25
}
}