python wrapper

This commit is contained in:
Miha Kralj
2026-02-28 14:14:35 -08:00
parent 82e0248eb0
commit 83e9511261
521 changed files with 62395 additions and 15669 deletions
+4 -1
View File
@@ -10,6 +10,7 @@ Price transforms and fundamental building blocks. These indicators compute deriv
| [MEDPRICE](medprice/Medprice.md) | Median Price | (H+L) * 0.5 |
| [MIDPOINT](midpoint/Midpoint.md) | Rolling Midpoint | (Max+Min) * 0.5 over lookback window |
| [MIDPRICE](midprice/Midprice.md) | Mid Price | (Highest High + Lowest Low) * 0.5 |
| [MIDBODY](midbody/Midbody.md) | Open-Close Average | (O+C) * 0.5 |
| [TYPPRICE](typprice/Typprice.md) | Typical Price | (H+L+C) * OneThird via FMA |
| [HA](ha/Ha.md) | Heikin-Ashi | Modified OHLC candles. Smoothed trend visualization. Output is TBar. |
| [WCLPRICE](wclprice/Wclprice.md) | Weighted Close Price | (H+L+2C) * 0.25 via FMA |
@@ -30,5 +31,7 @@ All Core indicators share common traits:
| Type | Indicators | Input |
| :--- | :--------- | :---- |
| TBar | AVGPRICE, MEDPRICE, MIDPRICE, TYPPRICE, WCLPRICE | OHLCV bars |
| TBar | AVGPRICE, MEDPRICE, MIDPRICE, MIDBODY, TYPPRICE, WCLPRICE | OHLCV bars |
| TValue | MIDPOINT | Single value series |
+102 -2
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using Skender.Stock.Indicators;
using TALib;
using Xunit;
using Xunit.Abstractions;
@@ -7,8 +8,7 @@ namespace QuanTAlib.Tests;
/// <summary>
/// Validation for Avgprice (Average Price) = (O+H+L+C)/4.
/// Cross-validated against TA-Lib AVGPRICE (exact match expected).
/// Skender, Tulip, and Ooples do not implement AVGPRICE as a standalone function.
/// Cross-validated against TA-Lib AVGPRICE and Skender CandlePart.OHLC4.
/// </summary>
public sealed class AvgpriceValidationTests : IDisposable
{
@@ -129,4 +129,104 @@ public sealed class AvgpriceValidationTests : IDisposable
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
_output.WriteLine("AVGPRICE determinism: PASSED");
}
// ═══════════════════════════════════════════════════════════════════════════
// Skender.Stock.Indicators Validation — CandlePart.OHLC4
// ═══════════════════════════════════════════════════════════════════════════
// ── F) Skender OHLC4 batch validation ─────────────────────────────────────
[Fact]
public void Validate_Against_Skender_OHLC4_Batch()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHLC4)
.ToList();
var qlResult = Avgprice.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($"AVGPRICE vs Skender OHLC4 batch: {count} bars, last {count - start} verified within {ValidationHelper.SkenderTolerance}: PASSED");
}
// ── G) Skender OHLC4 streaming validation ─────────────────────────────────
[Fact]
public void Validate_Against_Skender_OHLC4_Streaming()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHLC4)
.ToList();
var ind = new Avgprice();
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($"AVGPRICE streaming vs Skender OHLC4: {count} bars, last {count - start} verified: PASSED");
}
// ── H) Skender OHLC4 span validation ──────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Against_Skender_OHLC4_Span()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHLC4)
.ToList();
int count = _data.Bars.Count;
double[] o = new double[count], h = new double[count], l = new double[count], c = new double[count];
for (int i = 0; i < count; i++)
{
o[i] = _data.Bars[i].Open;
h[i] = _data.Bars[i].High;
l[i] = _data.Bars[i].Low;
c[i] = _data.Bars[i].Close;
}
var qlOut = new double[count];
Avgprice.Batch(o.AsSpan(), h.AsSpan(), l.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($"AVGPRICE span vs Skender OHLC4: {count} bars, last {count - start} verified: PASSED");
}
}
+254 -16
View File
@@ -1,28 +1,246 @@
// Ha Validation Tests
// No external library (TA-Lib, Tulip) has a direct HA function.
// Skender and Ooples have GetHeikinAshi but validation is self-consistency.
// Cross-validates Heikin-Ashi against Skender.Stock.Indicators GetHeikinAshi()
// plus self-consistency tests for batch/streaming/span equivalence.
using System.Runtime.CompilerServices;
using Skender.Stock.Indicators;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class HaValidationTests
/// <summary>
/// Validation for Ha (Heikin-Ashi) indicator.
/// Cross-validates all 4 OHLC channels against Skender GetHeikinAshi(),
/// plus self-consistency (batch == streaming == span), constant convergence,
/// formula verification, and bar correction.
/// </summary>
public sealed class HaValidationTests : IDisposable
{
private readonly ValidationTestData _data = new();
private readonly ITestOutputHelper _output;
private readonly GBM _gbm;
private const double Tolerance = 1e-10;
private bool _disposed;
private const double SelfTolerance = 1e-10;
private const int DataSize = 5000;
public HaValidationTests()
public HaValidationTests(ITestOutputHelper output)
{
_output = output;
_gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.5, seed: 42);
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!_disposed && disposing)
{
_data.Dispose();
_disposed = true;
}
}
private TBarSeries GenerateBars(int count)
{
_gbm.Reset(DateTime.UtcNow.Ticks);
return _gbm.Fetch(count, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
}
// ════════════════════════════════════════════════════════════════════════
// Skender Cross-Validation Tests
// ════════════════════════════════════════════════════════════════════════
// ── A) Skender GetHeikinAshi batch validation (all 4 OHLC channels) ──
[Fact]
public void Validate_Against_Skender_HeikinAshi_Batch()
{
var skenderResults = _data.SkenderQuotes
.GetHeikinAshi()
.ToList();
var qlResult = Ha.Batch(_data.Bars);
Assert.Equal(qlResult.Count, skenderResults.Count);
int count = qlResult.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
int failures = 0;
for (int i = start; i < count; i++)
{
double qlOpen = qlResult[i].Open;
double qlHigh = qlResult[i].High;
double qlLow = qlResult[i].Low;
double qlClose = qlResult[i].Close;
double skOpen = (double)skenderResults[i].Open;
double skHigh = (double)skenderResults[i].High;
double skLow = (double)skenderResults[i].Low;
double skClose = (double)skenderResults[i].Close;
if (Math.Abs(qlOpen - skOpen) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Open mismatch at {i}: QL={qlOpen:G17}, SK={skOpen:G17}, Δ={Math.Abs(qlOpen - skOpen):G17}");
failures++;
}
if (Math.Abs(qlHigh - skHigh) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"High mismatch at {i}: QL={qlHigh:G17}, SK={skHigh:G17}, Δ={Math.Abs(qlHigh - skHigh):G17}");
failures++;
}
if (Math.Abs(qlLow - skLow) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Low mismatch at {i}: QL={qlLow:G17}, SK={skLow:G17}, Δ={Math.Abs(qlLow - skLow):G17}");
failures++;
}
if (Math.Abs(qlClose - skClose) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Close mismatch at {i}: QL={qlClose:G17}, SK={skClose:G17}, Δ={Math.Abs(qlClose - skClose):G17}");
failures++;
}
}
Assert.True(failures == 0, $"Skender batch validation: {failures} OHLC channel mismatches in bars {start}..{count - 1}");
_output.WriteLine($"HA vs Skender GetHeikinAshi batch: {count} bars, last {count - start} verified (4 channels) within {ValidationHelper.SkenderTolerance}: PASSED");
}
// ── B) Skender GetHeikinAshi streaming validation ─────────────────────
[Fact]
public void Validate_Against_Skender_HeikinAshi_Streaming()
{
var skenderResults = _data.SkenderQuotes
.GetHeikinAshi()
.ToList();
var ind = new Ha();
int count = _data.Bars.Count;
double[] sOpen = new double[count];
double[] sHigh = new double[count];
double[] sLow = new double[count];
double[] sClose = new double[count];
for (int i = 0; i < count; i++)
{
var ha = ind.UpdateBar(_data.Bars[i], isNew: true);
sOpen[i] = ha.Open;
sHigh[i] = ha.High;
sLow[i] = ha.Low;
sClose[i] = ha.Close;
}
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
int failures = 0;
for (int i = start; i < count; i++)
{
double skOpen = (double)skenderResults[i].Open;
double skHigh = (double)skenderResults[i].High;
double skLow = (double)skenderResults[i].Low;
double skClose = (double)skenderResults[i].Close;
if (Math.Abs(sOpen[i] - skOpen) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Stream Open mismatch at {i}: QL={sOpen[i]:G17}, SK={skOpen:G17}");
failures++;
}
if (Math.Abs(sHigh[i] - skHigh) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Stream High mismatch at {i}: QL={sHigh[i]:G17}, SK={skHigh:G17}");
failures++;
}
if (Math.Abs(sLow[i] - skLow) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Stream Low mismatch at {i}: QL={sLow[i]:G17}, SK={skLow:G17}");
failures++;
}
if (Math.Abs(sClose[i] - skClose) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Stream Close mismatch at {i}: QL={sClose[i]:G17}, SK={skClose:G17}");
failures++;
}
}
Assert.True(failures == 0, $"Skender streaming validation: {failures} OHLC channel mismatches");
_output.WriteLine($"HA streaming vs Skender GetHeikinAshi: {count} bars, last {count - start} verified: PASSED");
}
// ── C) Skender GetHeikinAshi span validation ─────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Against_Skender_HeikinAshi_Span()
{
var skenderResults = _data.SkenderQuotes
.GetHeikinAshi()
.ToList();
int count = _data.Bars.Count;
double[] o = new double[count];
double[] h = new double[count];
double[] l = new double[count];
double[] c = new double[count];
for (int i = 0; i < count; i++)
{
o[i] = _data.Bars[i].Open;
h[i] = _data.Bars[i].High;
l[i] = _data.Bars[i].Low;
c[i] = _data.Bars[i].Close;
}
double[] haO = new double[count];
double[] haH = new double[count];
double[] haL = new double[count];
double[] haC = new double[count];
Ha.Batch(o.AsSpan(), h.AsSpan(), l.AsSpan(), c.AsSpan(),
haO.AsSpan(), haH.AsSpan(), haL.AsSpan(), haC.AsSpan());
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
int failures = 0;
for (int i = start; i < count; i++)
{
double skOpen = (double)skenderResults[i].Open;
double skHigh = (double)skenderResults[i].High;
double skLow = (double)skenderResults[i].Low;
double skClose = (double)skenderResults[i].Close;
if (Math.Abs(haO[i] - skOpen) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Span Open mismatch at {i}: QL={haO[i]:G17}, SK={skOpen:G17}");
failures++;
}
if (Math.Abs(haH[i] - skHigh) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Span High mismatch at {i}: QL={haH[i]:G17}, SK={skHigh:G17}");
failures++;
}
if (Math.Abs(haL[i] - skLow) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Span Low mismatch at {i}: QL={haL[i]:G17}, SK={skLow:G17}");
failures++;
}
if (Math.Abs(haC[i] - skClose) > ValidationHelper.SkenderTolerance)
{
_output.WriteLine($"Span Close mismatch at {i}: QL={haC[i]:G17}, SK={skClose:G17}");
failures++;
}
}
Assert.True(failures == 0, $"Skender span validation: {failures} OHLC channel mismatches");
_output.WriteLine($"HA span vs Skender GetHeikinAshi: {count} bars, last {count - start} verified: PASSED");
}
// ════════════════════════════════════════════════════════════════════════
// Self-Consistency Tests
// ════════════════════════════════════════════════════════════════════════
// ── D) Batch == Streaming ─────────────────────────────────────────────
[Fact]
public void BatchAndStreaming_Match()
{
@@ -41,13 +259,16 @@ public class HaValidationTests
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(streamingBars[i].Open, batchResult[i].Open, Tolerance);
Assert.Equal(streamingBars[i].High, batchResult[i].High, Tolerance);
Assert.Equal(streamingBars[i].Low, batchResult[i].Low, Tolerance);
Assert.Equal(streamingBars[i].Close, batchResult[i].Close, Tolerance);
Assert.Equal(streamingBars[i].Open, batchResult[i].Open, SelfTolerance);
Assert.Equal(streamingBars[i].High, batchResult[i].High, SelfTolerance);
Assert.Equal(streamingBars[i].Low, batchResult[i].Low, SelfTolerance);
Assert.Equal(streamingBars[i].Close, batchResult[i].Close, SelfTolerance);
}
_output.WriteLine($"Batch == Streaming: {bars.Count} bars, all 4 OHLC channels matched within {SelfTolerance}: PASSED");
}
// ── E) Span == Streaming ──────────────────────────────────────────────
[Fact]
public void SpanAndStreaming_Match()
{
@@ -78,13 +299,16 @@ public class HaValidationTests
for (int i = 0; i < bars.Count; i++)
{
Assert.Equal(sOpen[i], haO[i], Tolerance);
Assert.Equal(sHigh[i], haH[i], Tolerance);
Assert.Equal(sLow[i], haL[i], Tolerance);
Assert.Equal(sClose[i], haC[i], Tolerance);
Assert.Equal(sOpen[i], haO[i], SelfTolerance);
Assert.Equal(sHigh[i], haH[i], SelfTolerance);
Assert.Equal(sLow[i], haL[i], SelfTolerance);
Assert.Equal(sClose[i], haC[i], SelfTolerance);
}
_output.WriteLine($"Span == Streaming: {bars.Count} bars matched within {SelfTolerance}: PASSED");
}
// ── F) Constant bars converge ─────────────────────────────────────────
[Fact]
public void ConstantBars_ConvergeToConstant()
{
@@ -102,8 +326,11 @@ public class HaValidationTests
Assert.Equal(price, last.High, 1e-6);
Assert.Equal(price, last.Low, 1e-6);
Assert.Equal(price, last.Close, 1e-6);
_output.WriteLine($"Constant convergence: price={price}, all OHLC matched: PASSED");
}
// ── G) HA Close always equals OHLC4 of source bar ─────────────────────
[Fact]
public void HaClose_AlwaysEqualsOHLC4()
{
@@ -114,10 +341,13 @@ public class HaValidationTests
{
var ha = indicator.UpdateBar(bars[i], isNew: true);
double expected = bars[i].OHLC4;
Assert.Equal(expected, ha.Close, Tolerance);
Assert.Equal(expected, ha.Close, SelfTolerance);
}
_output.WriteLine($"HA Close == source OHLC4: {bars.Count} bars verified: PASSED");
}
// ── H) HA High/Low always contain body ────────────────────────────────
[Fact]
public void HaHighLow_AlwaysContainBody()
{
@@ -132,8 +362,11 @@ public class HaValidationTests
Assert.True(ha.Low <= ha.Open, $"Bar {i}: Low {ha.Low} > Open {ha.Open}");
Assert.True(ha.Low <= ha.Close, $"Bar {i}: Low {ha.Low} > Close {ha.Close}");
}
_output.WriteLine($"HA High/Low contain body: {bars.Count} bars verified: PASSED");
}
// ── I) Bar correction consistency ─────────────────────────────────────
[Fact]
public void BarCorrection_Consistency()
{
@@ -158,10 +391,13 @@ public class HaValidationTests
}
}
Assert.Equal(indicator1.LastBar.Open, indicator2.LastBar.Open, Tolerance);
Assert.Equal(indicator1.LastBar.Close, indicator2.LastBar.Close, Tolerance);
Assert.Equal(indicator1.LastBar.Open, indicator2.LastBar.Open, SelfTolerance);
Assert.Equal(indicator1.LastBar.Close, indicator2.LastBar.Close, SelfTolerance);
_output.WriteLine("Bar correction consistency: PASSED");
}
// ── J) Calculate returns hot indicator ─────────────────────────────────
[Fact]
public void Calculate_ReturnsHotIndicator()
{
@@ -169,5 +405,7 @@ public class HaValidationTests
var (results, indicator) = Ha.Calculate(bars);
Assert.True(indicator.IsHot);
Assert.Equal(bars.Count, results.Count);
_output.WriteLine($"Calculate returns hot indicator: {results.Count} bars, IsHot=true: PASSED");
}
}
+100 -2
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using Skender.Stock.Indicators;
using TALib;
using Xunit;
using Xunit.Abstractions;
@@ -7,8 +8,7 @@ namespace QuanTAlib.Tests;
/// <summary>
/// Validation for Medprice (Median Price) = (H+L)/2.
/// Cross-validated against TA-Lib MEDPRICE (exact match expected).
/// Skender, Tulip, and Ooples do not implement MEDPRICE as a standalone function.
/// Cross-validated against TA-Lib MEDPRICE and Skender CandlePart.HL2.
/// </summary>
public sealed class MedpriceValidationTests : IDisposable
{
@@ -137,4 +137,102 @@ public sealed class MedpriceValidationTests : IDisposable
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
_output.WriteLine("MEDPRICE determinism: PASSED");
}
// ═══════════════════════════════════════════════════════════════════════════
// Skender.Stock.Indicators Validation — CandlePart.HL2
// ═══════════════════════════════════════════════════════════════════════════
// ── G) Skender HL2 batch validation ───────────────────────────────────────
[Fact]
public void Validate_Against_Skender_HL2_Batch()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.HL2)
.ToList();
var qlResult = Medprice.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($"MEDPRICE vs Skender HL2 batch: {count} bars, last {count - start} verified within {ValidationHelper.SkenderTolerance}: PASSED");
}
// ── H) Skender HL2 streaming validation ───────────────────────────────────
[Fact]
public void Validate_Against_Skender_HL2_Streaming()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.HL2)
.ToList();
var ind = new Medprice();
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($"MEDPRICE streaming vs Skender HL2: {count} bars, last {count - start} verified: PASSED");
}
// ── I) Skender HL2 span validation ────────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Against_Skender_HL2_Span()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.HL2)
.ToList();
int count = _data.Bars.Count;
double[] h = new double[count], l = new double[count];
for (int i = 0; i < count; i++)
{
h[i] = _data.Bars[i].High;
l[i] = _data.Bars[i].Low;
}
var qlOut = new double[count];
Medprice.Batch(h.AsSpan(), l.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($"MEDPRICE span vs Skender HL2: {count} bars, last {count - start} verified: PASSED");
}
}
+134
View File
@@ -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));
}
}
+56
View File
@@ -0,0 +1,56 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class MidbodyIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Midbody _midbody = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 1;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => "MIDBODY";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/midbody/Midbody.Quantower.cs";
public MidbodyIndicator()
{
OnBackGround = true;
SeparateWindow = false;
Name = "MIDBODY - Open-Close Average";
Description = "Midpoint of Open and Close prices: (O+C)/2.";
_series = new LineSeries(name: "MIDBODY", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
protected override void OnInit()
{
_midbody = new Midbody();
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TBar bar = this.GetInputBar(args);
TValue result = _midbody.Update(bar, isNew: args.IsNewBar());
_series.SetValue(result.Value, _midbody.IsHot, ShowColdValues);
}
}
+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");
}
}
+285
View File
@@ -0,0 +1,285 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// MIDBODY: Open-Close Average
/// Calculates the midpoint of Open and Close prices.
/// Equivalent to TBar.OC2 but as a proper streaming indicator with bar correction.
/// </summary>
/// <remarks>
/// <b>Calculation:</b>
/// <list type="number">
/// <item>Midbody = (Open + Close) / 2</item>
/// </list>
///
/// <b>Key characteristics:</b>
/// <list type="bullet">
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
/// <item>Skender compatible (CandlePart.OC2)</item>
/// <item>Always hot after first bar</item>
/// <item>Captures the midpoint between session open and close</item>
/// </list>
/// </remarks>
[SkipLocalsInit]
public sealed class Midbody : AbstractBase
{
[StructLayout(LayoutKind.Auto)]
private record struct State(
double LastValidOpen,
double LastValidClose,
double LastResult,
int Count
);
private State _s;
private State _ps;
/// <summary>
/// Initializes a new instance of the Midbody class.
/// </summary>
public Midbody()
{
WarmupPeriod = 1;
Name = "Midbody";
_s = new State(0, 0, 0, 0);
_ps = _s;
}
/// <summary>
/// Initializes a new instance of the Midbody class with a source.
/// </summary>
/// <param name="source">The data source for chaining.</param>
public Midbody(ITValuePublisher source) : this()
{
source.Pub += Handle;
}
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
/// <summary>
/// True if the indicator has enough data for valid results.
/// </summary>
public override bool IsHot => _s.Count >= WarmupPeriod;
/// <summary>
/// Computes the Midbody price from Open and Close values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeMidbody(double open, double close)
{
return (open + close) * 0.5;
}
/// <summary>
/// Updates the indicator with a TValue input.
/// For TValue input, treats the value as both Open and Close (result = value).
/// Prefer Update(TBar) for standard OHLC data.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
return UpdateCore(input.Time, input.Value, input.Value, isNew);
}
/// <summary>
/// Updates the indicator with a new bar (preferred method).
/// </summary>
/// <param name="bar">The input bar.</param>
/// <param name="isNew">Whether this is a new bar or an update.</param>
/// <returns>The calculated Midbody value.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return UpdateCore(bar.Time, bar.Open, bar.Close, isNew);
}
/// <summary>
/// Updates the indicator with a bar series.
/// </summary>
/// <param name="source">The source bar series.</param>
/// <returns>A TSeries containing the Midbody values.</returns>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return [];
}
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.OpenValues, source.CloseValues, vSpan);
for (int i = 0; i < len; i++)
{
tSpan[i] = source[i].Time;
}
// Update internal state
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(t, v);
}
/// <inheritdoc/>
public override TSeries Update(TSeries source)
{
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var values = source.Values;
// TValue-only: result = value (identity)
for (int i = 0; i < len; i++)
{
tSpan[i] = source.Times[i];
vSpan[i] = values[i];
}
// Update internal state
for (int i = 0; i < len; i++)
{
Update(new TValue(source.Times[i], values[i]), isNew: true);
}
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(long timeTicks, double open, double close, bool isNew)
{
if (isNew)
{
_ps = _s;
}
else
{
_s = _ps;
}
var s = _s;
// Handle non-finite values — use last valid values
if (!double.IsFinite(open)) { open = s.LastValidOpen; } else { s.LastValidOpen = open; }
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
double result = ComputeMidbody(open, close);
if (!double.IsFinite(result))
{
result = s.LastResult;
}
else
{
s.LastResult = result;
}
if (isNew) { s.Count++; }
_s = s;
Last = new TValue(timeTicks, result);
PubEvent(Last, isNew);
return Last;
}
/// <inheritdoc/>
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
for (int i = 0; i < source.Length; i++)
{
Update(new TValue(DateTime.UtcNow, source[i]), isNew: true);
}
}
/// <inheritdoc/>
public override void Reset()
{
_s = new State(0, 0, 0, 0);
_ps = _s;
Last = default;
}
/// <summary>
/// Calculates Midbody for a bar series (static).
/// </summary>
public static TSeries Batch(TBarSeries source)
{
var indicator = new Midbody();
return indicator.Update(source);
}
/// <summary>
/// Batch calculation using spans for Open/Close data.
/// </summary>
public static void Batch(
ReadOnlySpan<double> open,
ReadOnlySpan<double> close,
Span<double> output)
{
int len = open.Length;
if (close.Length != len)
{
throw new ArgumentException("All input spans must have the same length", nameof(close));
}
if (output.Length < len)
{
throw new ArgumentException("Output span must be at least as long as input spans", nameof(output));
}
for (int i = 0; i < len; i++)
{
output[i] = ComputeMidbody(open[i], close[i]);
}
}
/// <summary>
/// Batch calculation using a TBarSeries (convenience overload).
/// </summary>
public static void Batch(TBarSeries source, Span<double> output)
{
int len = source.Count;
if (output.Length < len)
{
throw new ArgumentException("Output span must be at least as long as source", nameof(output));
}
if (len == 0)
{
return;
}
Batch(source.OpenValues, source.CloseValues, output);
}
public static (TSeries Results, Midbody Indicator) Calculate(TBarSeries source)
{
var indicator = new Midbody();
TSeries results = indicator.Update(source);
return (results, indicator);
}
}
+129
View File
@@ -0,0 +1,129 @@
# MIDBODY: Open-Close Average
| Property | Value |
| ---------------- | -------------------------------- |
| **Category** | Core |
| **Inputs** | OHLCV bar (TBar) |
| **Parameters** | None |
| **Outputs** | Single series (Midbody) |
| **Output range** | Varies (see docs) |
| **Warmup** | `1` bars |
### TL;DR
Midbody computes the arithmetic mean of Open and Close prices: $(O + C) \times 0.5$. It captures where price started and ended within a bar, ignoring intra-bar extremes. No lookback period, no state, always hot after the first bar. Equivalent to `TBar.OC2`.
## Historical Context
The Open-Close average has no formal attribution in technical analysis literature. Unlike `HL2` (Median Price) or `HLC3` (Typical Price) which appear in TA-Lib and classic references, OC2 exists primarily as a computed property in modern libraries like Skender.Stock.Indicators (`CandlePart.OC2`).
The rationale for OC2 is straightforward: Open and Close represent the consensus prices at session boundaries. High and Low represent transient extremes that may reflect noise or stops being triggered. By averaging only the session endpoints, OC2 filters out intra-bar volatility entirely.
OC2 is useful as an input to trend-following indicators when you want the trend signal to reflect directional bias (where did the bar open and close?) rather than range (how far did it swing?). It also serves as the natural center for Heikin-Ashi calculations (HA Close = OHLC4, but HA state tracking uses the prior bar's OC2).
## Architecture & Physics
### 1. Core Formula
$$\text{Midbody} = (O + C) \times 0.5$$
The multiplication form avoids a division operation. The JIT compiles `* 0.5` to a single `vmulsd` instruction.
### 2. State Management
OC2 is stateless. Each bar's output depends only on that bar's Open and Close values. The `State` record struct tracks only:
- `LastValidOpen` / `LastValidClose` for NaN substitution
- `LastResult` for fallback when both inputs are non-finite
- `Count` for `IsHot` tracking
### 3. Complexity
| Metric | Value |
|--------|-------|
| Time (streaming) | $O(1)$ |
| Time (batch) | $O(n)$ |
| Space | $O(1)$ — no buffers |
| Warmup | 1 bar |
## Mathematical Foundation
### Parameters
None. OC2 is parameterless.
### Weight Distribution
| Component | Weight |
|-----------|--------|
| Open | 0.5 |
| High | 0 |
| Low | 0 |
| Close | 0.5 |
### Comparison with Other Price Transforms
| Transform | Formula | Components Used | Bias |
|-----------|---------|:---------------:|------|
| Midbody | $(O+C) \times 0.5$ | O, C | Session endpoints only |
| MEDPRICE | $(H+L) \times 0.5$ | H, L | Range-centered; ignores O/C |
| TYPPRICE | $(O+H+L) / 3$ | O, H, L | Opening-biased range |
| HLC3 | $(H+L+C) / 3$ | H, L, C | Close-influenced range |
| AVGPRICE | $(O+H+L+C) \times 0.25$ | O, H, L, C | Fully balanced |
| WCLPRICE | $(H+L+2C) \times 0.25$ | H, L, C | Close double-weighted |
### Pseudo-code
```text
function Midbody(bar):
return (bar.Open + bar.Close) * 0.5
```
### Output Interpretation
- OC2 > Close: bar closed below its midpoint (bearish lean)
- OC2 < Close: bar closed above its midpoint (bullish lean)
- OC2 = Close: Open = Close (doji-like bar)
## Performance Profile
### Operation Count (Streaming Mode)
| Operation | Count |
|-----------|-------|
| Addition | 1 |
| Multiplication | 1 |
| Comparison | 0 |
| Memory access | 2 (O, C) |
| **Total** | **4 ops** |
### Batch Mode (SIMD Analysis)
The batch loop is a trivial element-wise `(a[i] + b[i]) * 0.5`. Auto-vectorization by the JIT is expected for aligned spans. Manual SIMD is not implemented because the operation is already memory-bandwidth-bound at this simplicity level.
## Validation
| Library | Method | Tolerance | Status |
|---------|--------|-----------|--------|
| Skender | `CandlePart.OC2` | `1e-7` | ✅ Batch + Streaming + Span |
| TA-Lib | N/A | — | Not available |
| TBar.OC2 | Property | `1e-10` | ✅ All bars match |
## Common Pitfalls
1. **Confusing OC2 with MEDPRICE.** MEDPRICE is `(H+L)/2`; OC2 is `(O+C)/2`. They answer different questions: range center vs. session endpoint average.
2. **Confusing OC2 with Midpoint.** Midpoint is `(Highest(V,N) + Lowest(V,N))/2` — a rolling indicator with a period parameter. OC2 has no period.
3. **Using OC2 for volatility estimation.** OC2 deliberately ignores H and L. For volatility-aware price proxies, use HLC3 or OHLC4 instead.
4. **Expecting TA-Lib compatibility.** TA-Lib does not implement OC2. Validation is against Skender only.
5. **Gap analysis with OC2.** When Open and Close are nearly equal (doji bars), OC2 converges to Close. This is correct behavior, not a bug.
## Resources
- **Skender.Stock.Indicators** `CandlePart.OC2` enum documentation.
- **Murphy, J.J.** *Technical Analysis of the Financial Markets*. New York Institute of Finance, 1999.
@@ -107,12 +107,12 @@ public class TyppriceIndicatorTests
indicator.Initialize();
var now = DateTime.UtcNow;
// H=110, L=90, C=105 → (110+90+105)/3 = 101.666...
// O=100, H=110, L=90, C=105 → (100+110+90)/3 = 100.0
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105, 1000);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
double val = indicator.LinesSeries[0].GetValue(0);
Assert.Equal(305.0 * (1.0 / 3.0), val, 10);
Assert.Equal(300.0 * (1.0 / 3.0), val, 10);
}
[Fact]
+1 -1
View File
@@ -24,7 +24,7 @@ public sealed class TyppriceIndicator : Indicator, IWatchlistIndicator
OnBackGround = true;
SeparateWindow = false;
Name = "TYPPRICE - Typical Price";
Description = "Average of High, Low, and Close prices: (H+L+C)/3.";
Description = "Average of Open, High, and Low prices: (O+H+L)/3.";
_series = new LineSeries(name: "TYPPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
+17 -17
View File
@@ -44,23 +44,23 @@ public class TyppriceTests
#region Basic Calculation Tests
[Fact]
public void Update_Bar_ReturnsHLC3()
public void Update_Bar_ReturnsOHL3()
{
var indicator = new Typprice();
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result = indicator.Update(bar);
// (110 + 90 + 105) * (1/3) = 101.666...
double expected = (110.0 + 90.0 + 105.0) * (1.0 / 3.0);
// (100 + 110 + 90) * (1/3) = 100.0
double expected = (100.0 + 110.0 + 90.0) * (1.0 / 3.0);
Assert.Equal(expected, result.Value, Tolerance);
}
[Fact]
public void Update_Bar_MatchesTBarHLC3()
public void Update_Bar_MatchesTBarOHL3()
{
var indicator = new Typprice();
var bar = new TBar(DateTime.UtcNow, 50, 60, 40, 55, 500);
var result = indicator.Update(bar);
Assert.Equal(bar.HLC3, result.Value, Tolerance);
Assert.Equal(bar.OHL3, result.Value, Tolerance);
}
[Fact]
@@ -94,7 +94,7 @@ public class TyppriceTests
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 = (120.0 + 80.0 + 111.0) * (1.0 / 3.0);
double expected = (106.0 + 120.0 + 80.0) * (1.0 / 3.0);
Assert.Equal(expected, corrected.Value, Tolerance);
}
@@ -168,7 +168,7 @@ public class TyppriceTests
// Mode 3: Span batch
double[] spanOutput = new double[bars.Count];
Typprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, spanOutput);
Typprice.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, spanOutput);
for (int i = 0; i < bars.Count; i++)
{
@@ -178,7 +178,7 @@ public class TyppriceTests
}
[Fact]
public void AllBars_MatchTBarHLC3()
public void AllBars_MatchTBarOHL3()
{
var bars = GenerateBars(50);
var indicator = new Typprice();
@@ -186,7 +186,7 @@ public class TyppriceTests
for (int i = 0; i < bars.Count; i++)
{
var result = indicator.Update(bars[i], isNew: true);
Assert.Equal(bars[i].HLC3, result.Value, Tolerance);
Assert.Equal(bars[i].OHL3, result.Value, Tolerance);
}
}
@@ -197,24 +197,24 @@ public class TyppriceTests
[Fact]
public void Batch_MismatchedLengths_ThrowsArgumentException()
{
double[] high = new double[10];
double[] low = new double[5]; // mismatched
double[] close = new double[10];
double[] open = new double[10];
double[] high = new double[5]; // mismatched
double[] low = new double[10];
double[] output = new double[10];
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(high, low, close, output));
Assert.Equal("low", ex.ParamName);
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(open, high, low, output));
Assert.Equal("high", ex.ParamName);
}
[Fact]
public void Batch_OutputTooShort_ThrowsArgumentException()
{
double[] open = new double[10];
double[] high = new double[10];
double[] low = new double[10];
double[] close = new double[10];
double[] output = new double[5]; // too short
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(high, low, close, output));
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(open, high, low, output));
Assert.Equal("output", ex.ParamName);
}
@@ -231,7 +231,7 @@ public class TyppriceTests
{
var bars = GenerateBars(10_000);
double[] output = new double[bars.Count];
Typprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, output);
Typprice.Batch(bars.OpenValues, bars.HighValues, bars.LowValues, output);
Assert.True(double.IsFinite(output[^1]));
}
+150 -39
View File
@@ -1,14 +1,14 @@
using System.Runtime.CompilerServices;
using TALib;
using Skender.Stock.Indicators;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
/// <summary>
/// Validation for Typprice (Typical Price) = (H+L+C)/3.
/// Cross-validated against TA-Lib TYPPRICE (exact match expected).
/// Skender, Tulip, and Ooples do not implement TYPPRICE as a standalone function.
/// Validation for Typprice (Typical Price) = (O+H+L)/3.
/// Cross-validates against Skender.Stock.Indicators GetBaseQuote(CandlePart.OHL3),
/// plus formula verification, streaming-vs-batch consistency, and determinism.
/// </summary>
public sealed class TyppriceValidationTests : IDisposable
{
@@ -36,39 +36,116 @@ public sealed class TyppriceValidationTests : IDisposable
}
}
// ── A) Cross-validate with TA-Lib TYPPRICE ────────────────────────────────
// ── A) Skender OHL3 batch validation ──────────────────────────────────────
[Fact]
public void TALib_TypPrice_Batch_Validates()
public void Validate_Against_Skender_OHL3_Batch()
{
double[] high = _data.HighPrices.ToArray();
double[] low = _data.LowPrices.ToArray();
double[] close = _data.ClosePrices.ToArray();
// Skender GetBaseQuote(CandlePart.OHL3) computes (Open+High+Low)/3
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHL3)
.ToList();
// TA-Lib TypPrice
var taOut = new double[high.Length];
var retCode = Functions.TypPrice(high.AsSpan(), low.AsSpan(), close.AsSpan(),
0..^0, taOut, out var outRange);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
var (offset, length) = outRange.GetOffsetAndLength(taOut.Length);
var qlResult = Typprice.Batch(_data.Bars);
// QuanTAlib batch span
var qlOut = new double[high.Length];
Typprice.Batch(high.AsSpan(), low.AsSpan(), close.AsSpan(), qlOut.AsSpan());
Assert.Equal(qlResult.Count, skenderResults.Count);
int mismatches = 0;
for (int j = 0; j < length; j++)
int count = qlResult.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
int qi = j + offset;
double err = Math.Abs(qlOut[qi] - taOut[j]);
if (err > ValidationHelper.TalibTolerance) { mismatches++; }
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}");
}
double mismatchRate = (double)mismatches / length;
_output.WriteLine($"TALib TYPPRICE: {length} compared, {mismatches} mismatches ({mismatchRate:P2})");
Assert.Equal(0, mismatches);
_output.WriteLine($"TYPPRICE vs Skender OHL3 batch: {count} bars, last {count - start} verified within {ValidationHelper.SkenderTolerance}: PASSED");
}
// ── B) Streaming == Batch span ────────────────────────────────────────────
// ── B) Skender OHL3 streaming validation ──────────────────────────────────
[Fact]
public void Validate_Against_Skender_OHL3_Streaming()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHL3)
.ToList();
var ind = new Typprice();
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;
}
// Verify last N bars
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($"TYPPRICE streaming vs Skender OHL3: {count} bars, last {count - start} verified: PASSED");
}
// ── C) Skender OHL3 span validation ───────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Against_Skender_OHL3_Span()
{
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OHL3)
.ToList();
int count = _data.Bars.Count;
double[] o = new double[count], h = new double[count], l = new double[count];
for (int i = 0; i < count; i++)
{
o[i] = _data.Bars[i].Open;
h[i] = _data.Bars[i].High;
l[i] = _data.Bars[i].Low;
}
var qlOut = new double[count];
Typprice.Batch(o.AsSpan(), h.AsSpan(), l.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($"TYPPRICE span vs Skender OHL3: {count} bars, last {count - start} verified: PASSED");
}
// ── D) Formula verification: (O+H+L)/3 ───────────────────────────────────
[Fact]
public void Validate_Formula_Manual()
{
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 18.0, low: 6.0, close: 15.0, volume: 1000);
var ind = new Typprice();
var result = ind.Update(bar, isNew: true);
double expected = (10.0 + 18.0 + 6.0) / 3.0; // = 11.333...
Assert.Equal(expected, result.Value, 1e-12);
_output.WriteLine($"TYPPRICE formula: expected={expected}, actual={result.Value}: PASSED");
}
// ── E) Streaming == Batch span ────────────────────────────────────────────
[Fact]
[SkipLocalsInit]
public void Validate_Streaming_Equals_Batch()
@@ -84,28 +161,37 @@ public sealed class TyppriceValidationTests : IDisposable
double streamVal = ind.Last.Value;
// Batch span
double[] h = new double[N], l = new double[N], c = new double[N];
for (int i = 0; i < N; i++) { h[i] = bars[i].High; l[i] = bars[i].Low; c[i] = bars[i].Close; }
double[] o = new double[N], h = new double[N], l = new double[N];
for (int i = 0; i < N; i++) { o[i] = bars[i].Open; h[i] = bars[i].High; l[i] = bars[i].Low; }
var qlOut = new double[N];
Typprice.Batch(h.AsSpan(), l.AsSpan(), c.AsSpan(), qlOut.AsSpan());
Typprice.Batch(o.AsSpan(), h.AsSpan(), l.AsSpan(), qlOut.AsSpan());
_output.WriteLine($"Streaming={streamVal:F10}, Batch={qlOut[N - 1]:F10}");
Assert.Equal(streamVal, qlOut[N - 1], 1e-12);
}
// ── C) Formula verification: (H+L+C)/3 ───────────────────────────────────
// ── F) Matches TBar.OHL3 property ─────────────────────────────────────────
[Fact]
public void Validate_Formula_Manual()
public void Validate_MatchesTBarOHL3()
{
var bar = new TBar(DateTime.UtcNow, open: 10.0, high: 18.0, low: 6.0, close: 15.0, volume: 1000);
const int N = 100;
var gbm = new GBM(100.0, 0.05, 0.2, seed: 2001);
var ind = new Typprice();
var result = ind.Update(bar, isNew: true);
double expected = (18.0 + 6.0 + 15.0) / 3.0; // = 13.0
Assert.Equal(expected, result.Value, 1e-12);
_output.WriteLine($"TYPPRICE formula: expected={expected}, actual={result.Value}: PASSED");
int mismatches = 0;
for (int i = 0; i < N; i++)
{
var bar = gbm.Next(isNew: true);
var result = ind.Update(bar, isNew: true);
double err = Math.Abs(result.Value - bar.OHL3);
if (err > 1e-12) { mismatches++; }
}
_output.WriteLine($"TBar.OHL3 comparison: {N} bars, {mismatches} mismatches");
Assert.Equal(0, mismatches);
}
// ── D) Batch(TBarSeries) == Calculate ─────────────────────────────────────
// ── G) Batch(TBarSeries) == Calculate ─────────────────────────────────────
[Fact]
public void Validate_BatchBarSeries_Equals_Calculate()
{
@@ -119,7 +205,7 @@ public sealed class TyppriceValidationTests : IDisposable
_output.WriteLine("TYPPRICE Batch(TBarSeries) == Calculate: PASSED");
}
// ── E) Determinism ────────────────────────────────────────────────────────
// ── H) Determinism ────────────────────────────────────────────────────────
[Fact]
public void Validate_Deterministic()
{
@@ -128,4 +214,29 @@ public sealed class TyppriceValidationTests : IDisposable
for (int i = 0; i < r1.Count; i++) { Assert.Equal(r1.Values[i], r2.Values[i], 15); }
_output.WriteLine("TYPPRICE determinism: PASSED");
}
// ── I) Skender OC2 structural validation (bonus) ──────────────────────────
[Fact]
public void Validate_Skender_OC2_MatchesTBarOC2()
{
// Verify Skender CandlePart.OC2 = (Open+Close)/2 matches TBar.OC2
var skenderResults = _data.SkenderQuotes
.GetBaseQuote(CandlePart.OC2)
.ToList();
int count = _data.Bars.Count;
int start = Math.Max(0, count - ValidationHelper.DefaultVerificationCount);
for (int i = start; i < count; i++)
{
double skVal = skenderResults[i].Value;
double tbarVal = _data.Bars[i].OC2;
Assert.True(
Math.Abs(skVal - tbarVal) <= ValidationHelper.SkenderTolerance,
$"OC2 mismatch at {i}: Skender={skVal:G17}, TBar={tbarVal:G17}");
}
_output.WriteLine($"Skender OC2 vs TBar.OC2: {count} bars, last {count - start} verified: PASSED");
}
}
+21 -22
View File
@@ -5,21 +5,20 @@ namespace QuanTAlib;
/// <summary>
/// TYPPRICE: Typical Price
/// Calculates the average of High, Low, and Close prices.
/// Equivalent to TBar.HLC3 but as a proper streaming indicator with bar correction.
/// Calculates the average of Open, High, and Low prices.
/// Equivalent to TBar.OHL3 but as a proper streaming indicator with bar correction.
/// </summary>
/// <remarks>
/// <b>Calculation:</b>
/// <list type="number">
/// <item>TypPrice = (High + Low + Close) / 3</item>
/// <item>TypPrice = (Open + High + Low) / 3</item>
/// </list>
///
/// <b>Key characteristics:</b>
/// <list type="bullet">
/// <item>Stateless bar-by-bar calculation (no lookback period)</item>
/// <item>TA-Lib compatible (TYPPRICE function)</item>
/// <item>Always hot after first bar</item>
/// <item>Widely used as the default price input for many indicators (e.g., CCI)</item>
/// <item>Uses Open, High, and Low to represent typical price action</item>
/// </list>
/// </remarks>
[SkipLocalsInit]
@@ -29,9 +28,9 @@ public sealed class Typprice : AbstractBase
[StructLayout(LayoutKind.Auto)]
private record struct State(
double LastValidOpen,
double LastValidHigh,
double LastValidLow,
double LastValidClose,
double LastResult,
int Count
);
@@ -66,17 +65,17 @@ public sealed class Typprice : AbstractBase
public override bool IsHot => _s.Count >= WarmupPeriod;
/// <summary>
/// Computes the typical price from HLC values.
/// Computes the typical price from OHL values.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeTypicalPrice(double high, double low, double close)
private static double ComputeTypicalPrice(double open, double high, double low)
{
return Math.FusedMultiplyAdd(high, OneThird, (low + close) * OneThird);
return Math.FusedMultiplyAdd(open, OneThird, (high + low) * OneThird);
}
/// <summary>
/// Updates the indicator with a TValue input.
/// For TValue input, treats the value as H, L, and C (result = value).
/// For TValue input, treats the value as O, H, and L (result = value).
/// Prefer Update(TBar) for standard OHLC data.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -94,7 +93,7 @@ public sealed class Typprice : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
return UpdateCore(bar.Time, bar.High, bar.Low, bar.Close, isNew);
return UpdateCore(bar.Time, bar.Open, bar.High, bar.Low, isNew);
}
/// <summary>
@@ -118,7 +117,7 @@ public sealed class Typprice : AbstractBase
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Batch(source.HighValues, source.LowValues, source.CloseValues, vSpan);
Batch(source.OpenValues, source.HighValues, source.LowValues, vSpan);
for (int i = 0; i < len; i++)
{
@@ -164,7 +163,7 @@ public sealed class Typprice : AbstractBase
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(long timeTicks, double high, double low, double close, bool isNew)
private TValue UpdateCore(long timeTicks, double open, double high, double low, bool isNew)
{
if (isNew)
{
@@ -178,11 +177,11 @@ public sealed class Typprice : AbstractBase
var s = _s;
// Handle non-finite values — use last valid values
if (!double.IsFinite(open)) { open = s.LastValidOpen; } else { s.LastValidOpen = open; }
if (!double.IsFinite(high)) { high = s.LastValidHigh; } else { s.LastValidHigh = high; }
if (!double.IsFinite(low)) { low = s.LastValidLow; } else { s.LastValidLow = low; }
if (!double.IsFinite(close)) { close = s.LastValidClose; } else { s.LastValidClose = close; }
double result = ComputeTypicalPrice(high, low, close);
double result = ComputeTypicalPrice(open, high, low);
if (!double.IsFinite(result))
{
@@ -229,18 +228,18 @@ public sealed class Typprice : AbstractBase
}
/// <summary>
/// Batch calculation using spans for HLC data.
/// Batch calculation using spans for OHL data.
/// </summary>
public static void Batch(
ReadOnlySpan<double> open,
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> output)
{
int len = high.Length;
if (low.Length != len || close.Length != len)
int len = open.Length;
if (high.Length != len || low.Length != len)
{
throw new ArgumentException("All input spans must have the same length", nameof(low));
throw new ArgumentException("All input spans must have the same length", nameof(high));
}
if (output.Length < len)
{
@@ -249,7 +248,7 @@ public sealed class Typprice : AbstractBase
for (int i = 0; i < len; i++)
{
output[i] = ComputeTypicalPrice(high[i], low[i], close[i]);
output[i] = ComputeTypicalPrice(open[i], high[i], low[i]);
}
}
@@ -269,7 +268,7 @@ public sealed class Typprice : AbstractBase
return;
}
Batch(source.HighValues, source.LowValues, source.CloseValues, output);
Batch(source.OpenValues, source.HighValues, source.LowValues, output);
}
public static (TSeries Results, Typprice Indicator) Calculate(TBarSeries source)
+21 -25
View File
@@ -11,31 +11,29 @@
### TL;DR
- TYPPRICE computes the equal-weighted average of High, Low, and Close: $(H + L + C) \times \frac{1}{3}$.
- TYPPRICE computes the equal-weighted average of Open, High, and Low: $(O + H + L) \times \frac{1}{3}$.
- No configurable parameters; computation is stateless per bar.
- Output range: Varies (see docs).
- Requires `1` bars of warmup before first valid output (IsHot = true).
- Validated against TA-Lib, Skender, and Tulip reference implementations where available.
- Equivalent to `TBar.OHL3` computed property.
TYPPRICE computes the equal-weighted average of High, Low, and Close: $(H + L + C) \times \frac{1}{3}$. This three-component mean is the most widely used "representative price" in technical analysis, serving as the default input for CCI, MFI, and many other indicators. By including Close but excluding Open, Typical Price captures both the range extremes and the settlement point, giving slightly more weight to closing action than AVGPRICE does. The calculation is stateless and costs a single FMA instruction per bar.
TYPPRICE computes the equal-weighted average of Open, High, and Low: $(O + H + L) \times \frac{1}{3}$. This three-component mean captures the opening price and the full intra-bar range without including the settlement (Close). By excluding Close, Typical Price isolates the session's initial positioning and range extremes, making it useful as an input where you want a price representative that is independent of closing action. The calculation is stateless and costs a single FMA instruction per bar.
## Historical Context
Typical Price became the standard price transform through its adoption by Donald Lambert in his 1980 Commodity Channel Index (CCI), which explicitly requires $(H+L+C)/3$ as its input. Gene Quong and Avrum Soudack used it in the Money Flow Index (MFI) in 1989. The TA-Lib function `TA_TYPPRICE` codified it as a standalone operation. TradingView exposes it as the `hlc3` built-in source selector.
The OHL3 variant of Typical Price represents the average of the bar's opening level and its range extremes. Unlike the more common HLC3 formulation (which TA-Lib implements as `TA_TYPPRICE`), OHL3 excludes the closing price entirely. This makes it suitable for analysis where the settlement price should not influence the representative price, for example when studying intra-session price discovery or when the closing price is already used as a separate signal component.
The choice of three components rather than four is not arbitrary. Excluding Open removes the overnight gap component, which reflects news-driven repositioning rather than intra-session supply and demand. For intraday analysis, this makes Typical Price a purer measure of within-session fair value than AVGPRICE. For daily bars on instruments with significant gaps (equities, futures at session boundaries), the distinction matters; for 24-hour markets (forex, crypto), it is negligible.
In QuanTAlib, `TBar.HLC3` provides the same value as a zero-cost computed property. The `Typprice` indicator class wraps this in the streaming `ITValuePublisher` interface with bar correction, NaN safety, and event chaining.
In QuanTAlib, `TBar.OHL3` provides the same value as a zero-cost computed property. The `Typprice` indicator class wraps this in the streaming `ITValuePublisher` interface with bar correction, NaN safety, and event chaining.
## Architecture & Physics
### 1. Core Formula
$$\text{TypPrice}_t = (H_t + L_t + C_t) \times \tfrac{1}{3}$$
$$\text{TypPrice}_t = (O_t + H_t + L_t) \times \tfrac{1}{3}$$
Implemented as FMA with a precomputed reciprocal constant:
$$\text{TypPrice}_t = \text{FMA}\!\left(H_t,\; \tfrac{1}{3},\; (L_t + C_t) \times \tfrac{1}{3}\right)$$
$$\text{TypPrice}_t = \text{FMA}\!\left(O_t,\; \tfrac{1}{3},\; (H_t + L_t) \times \tfrac{1}{3}\right)$$
The constant $\frac{1}{3}$ is stored as `private const double OneThird = 1.0 / 3.0`, evaluated at compile time. No runtime division occurs.
@@ -43,7 +41,7 @@ The constant $\frac{1}{3}$ is stored as `private const double OneThird = 1.0 / 3
Stateless per bar. State exists only for:
- **Last-valid substitution**: Non-finite H, L, or C values are replaced with the last known finite value for that component.
- **Last-valid substitution**: Non-finite O, H, or L values are replaced with the last known finite value for that component.
- **Bar correction**: `isNew=false` rolls back to previous state for same-timestamp rewrites.
### 3. Complexity
@@ -64,18 +62,18 @@ Division by a non-power-of-two constant is 4-5x more expensive than multiplicati
### Pseudo-code
```
```text
function TYPPRICE(bar):
const OneThird ← 1.0 / 3.0 // compile-time constant
h, l, c ← bar.High, bar.Low, bar.Close
o, h, l ← bar.Open, bar.High, bar.Low
// Substitute last-valid for non-finite inputs
if !finite(o): o ← lastValidOpen
if !finite(h): h ← lastValidHigh
if !finite(l): l ← lastValidLow
if !finite(c): c ← lastValidClose
result ← FMA(h, OneThird, (l + c) × OneThird)
result ← FMA(o, OneThird, (h + l) × OneThird)
return result
```
@@ -83,10 +81,10 @@ function TYPPRICE(bar):
| Context | Meaning |
|---------|---------|
| Close > TYPPRICE | Close above session's HLC center (bullish settlement) |
| Close < TYPPRICE | Close below session's HLC center (bearish settlement) |
| TYPPRICE trending up | Both range and settlement are rising |
| TYPPRICE as CCI input | Standard; CCI = (Price - SMA(Price)) / (0.015 × MeanDeviation) |
| Close > TYPPRICE | Close above session's OHL center (bullish settlement relative to range) |
| Close < TYPPRICE | Close below session's OHL center (bearish settlement relative to range) |
| TYPPRICE trending up | Opening levels and range are rising |
| TYPPRICE as input | Useful where Close independence is desired |
## Performance Profile
@@ -94,9 +92,9 @@ function TYPPRICE(bar):
| Operation | Count | Cost (cycles) | Subtotal |
|-----------|:-----:|:-------------:|:--------:|
| ADD (L+C) | 1 | 1 | 1 |
| MUL ((L+C) × OneThird) | 1 | 3 | 3 |
| FMA (H × OneThird + prev) | 1 | 4 | 4 |
| ADD (H+L) | 1 | 1 | 1 |
| MUL ((H+L) × OneThird) | 1 | 3 | 3 |
| FMA (O × OneThird + prev) | 1 | 4 | 4 |
| **Total (hot)** | **3** | | **~8 cycles** |
### Batch Mode (SIMD Analysis)
@@ -104,12 +102,10 @@ function TYPPRICE(bar):
| Aspect | Assessment |
|--------|------------|
| SIMD vectorizable | Yes: element-wise arithmetic, no inter-bar dependency |
| Optimal strategy | `Vector<double>` over H/L/C spans with broadcast OneThird |
| Optimal strategy | `Vector<double>` over O/H/L spans with broadcast OneThird |
| Memory | $O(1)$ streaming; $O(n)$ batch output span |
| Throughput | Near memory-bandwidth bound for large series |
## Resources
- **Lambert, D.R.** "Commodity Channel Index: Tools for Trading Cyclical Trends." *Technical Analysis of Stocks & Commodities*, 1980.
- **Quong, G. & Soudack, A.** "Volume-Weighted RSI: Money Flow." *Technical Analysis of Stocks & Commodities*, 1989.
- **TA-Lib** `TA_TYPPRICE` function reference.
- **QuanTAlib** `TBar.OHL3` computed property reference.