mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-26 06:18:05 +00:00
filters update
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TyppriceIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void TyppriceIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("TYPPRICE - Typical Price", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ShortName_IsTypprice()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.Equal("TYPPRICE", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_MinHistoryDepths_EqualsOne()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
|
||||
Assert.Equal(1, TyppriceIndicator.MinHistoryDepths);
|
||||
Assert.Equal(1, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_Initialize_CreatesInternalIndicator()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
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 TyppriceIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
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 TyppriceIndicator_ShowColdValues_CanBeToggled()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = false;
|
||||
Assert.False(indicator.ShowColdValues);
|
||||
|
||||
indicator.ShowColdValues = true;
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
Assert.Contains("Typprice.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_ComputesCorrectTypicalPrice()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// H=110, L=90, C=105 → (110+90+105)/3 = 101.666...
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TyppriceIndicator_IsHotImmediately()
|
||||
{
|
||||
var indicator = new TyppriceIndicator();
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class TyppriceIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Typprice _typprice = null!;
|
||||
private readonly LineSeries _series;
|
||||
|
||||
public static int MinHistoryDepths => 1;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => "TYPPRICE";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/core/typprice/Typprice.Quantower.cs";
|
||||
|
||||
public TyppriceIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
Name = "TYPPRICE - Typical Price";
|
||||
Description = "Average of High, Low, and Close prices: (H+L+C)/3.";
|
||||
|
||||
_series = new LineSeries(name: "TYPPRICE", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_typprice = new Typprice();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TBar bar = this.GetInputBar(args);
|
||||
TValue result = _typprice.Update(bar, isNew: args.IsNewBar());
|
||||
_series.SetValue(result.Value, _typprice.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
// Typprice Unit Tests
|
||||
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class TyppriceTests
|
||||
{
|
||||
private readonly GBM _gbm;
|
||||
private const double Tolerance = 1e-10;
|
||||
|
||||
public TyppriceTests()
|
||||
{
|
||||
_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 Typprice();
|
||||
Assert.Equal("Typprice", indicator.Name);
|
||||
Assert.Equal(1, indicator.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithSource_SubscribesToEvents()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var indicator = new Typprice(source);
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.NotEqual(default, indicator.Last);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Basic Calculation Tests
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_ReturnsHLC3()
|
||||
{
|
||||
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);
|
||||
Assert.Equal(expected, result.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_Bar_MatchesTBarHLC3()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_TValue_ReturnsIdentity()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
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 Typprice();
|
||||
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 Typprice();
|
||||
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 = (120.0 + 80.0 + 111.0) * (1.0 / 3.0);
|
||||
Assert.Equal(expected, corrected.Value, Tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_MultipleIsNewFalse_ProducesIdempotentResults()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
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 Typprice();
|
||||
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 Typprice();
|
||||
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 Typprice();
|
||||
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 = Typprice.Batch(bars);
|
||||
|
||||
// Mode 3: Span batch
|
||||
double[] spanOutput = new double[bars.Count];
|
||||
Typprice.Batch(bars.HighValues, bars.LowValues, 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_MatchTBarHLC3()
|
||||
{
|
||||
var bars = GenerateBars(50);
|
||||
var indicator = new Typprice();
|
||||
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
var result = indicator.Update(bars[i], isNew: true);
|
||||
Assert.Equal(bars[i].HLC3, result.Value, Tolerance);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Batch Validation Tests
|
||||
|
||||
[Fact]
|
||||
public void Batch_MismatchedLengths_ThrowsArgumentException()
|
||||
{
|
||||
double[] high = new double[10];
|
||||
double[] low = new double[5]; // mismatched
|
||||
double[] close = new double[10];
|
||||
double[] output = new double[10];
|
||||
|
||||
var ex = Assert.Throws<ArgumentException>(() => Typprice.Batch(high, low, close, output));
|
||||
Assert.Equal("low", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_OutputTooShort_ThrowsArgumentException()
|
||||
{
|
||||
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));
|
||||
Assert.Equal("output", ex.ParamName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_EmptyInput_NoOutput()
|
||||
{
|
||||
var bars = new TBarSeries();
|
||||
var result = Typprice.Batch(bars);
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Batch_LargeDataset_NoStackOverflow()
|
||||
{
|
||||
var bars = GenerateBars(10_000);
|
||||
double[] output = new double[bars.Count];
|
||||
Typprice.Batch(bars.HighValues, bars.LowValues, bars.CloseValues, output);
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Chaining Tests
|
||||
|
||||
[Fact]
|
||||
public void Pub_EventFires_OnUpdate()
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
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) = Typprice.Calculate(bars);
|
||||
Assert.Equal(bars.Count, results.Count);
|
||||
Assert.True(ind.IsHot);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <b>Calculation:</b>
|
||||
/// <list type="number">
|
||||
/// <item>TypPrice = (High + Low + Close) / 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>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Typprice : AbstractBase
|
||||
{
|
||||
private const double OneThird = 1.0 / 3.0;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(
|
||||
double LastValidHigh,
|
||||
double LastValidLow,
|
||||
double LastValidClose,
|
||||
double LastResult,
|
||||
int Count
|
||||
);
|
||||
private State _s;
|
||||
private State _ps;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Typprice class.
|
||||
/// </summary>
|
||||
public Typprice()
|
||||
{
|
||||
WarmupPeriod = 1;
|
||||
Name = "Typprice";
|
||||
_s = new State(0, 0, 0, 0, 0);
|
||||
_ps = _s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the Typprice class with a source.
|
||||
/// </summary>
|
||||
/// <param name="source">The data source for chaining.</param>
|
||||
public Typprice(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 typical price from HLC values.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static double ComputeTypicalPrice(double high, double low, double close)
|
||||
{
|
||||
return Math.FusedMultiplyAdd(high, OneThird, (low + close) * OneThird);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the indicator with a TValue input.
|
||||
/// For TValue input, treats the value as H, L, and C (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, 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 Typical Price value.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TBar bar, bool isNew = true)
|
||||
{
|
||||
return UpdateCore(bar.Time, bar.High, bar.Low, 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 Typical Price 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.HighValues, source.LowValues, 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 high, double low, 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(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);
|
||||
|
||||
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, 0);
|
||||
_ps = _s;
|
||||
Last = default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Typical Price for a bar series (static).
|
||||
/// </summary>
|
||||
public static TSeries Batch(TBarSeries source)
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
return indicator.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch calculation using spans for HLC data.
|
||||
/// </summary>
|
||||
public static void Batch(
|
||||
ReadOnlySpan<double> high,
|
||||
ReadOnlySpan<double> low,
|
||||
ReadOnlySpan<double> close,
|
||||
Span<double> output)
|
||||
{
|
||||
int len = high.Length;
|
||||
if (low.Length != len || close.Length != len)
|
||||
{
|
||||
throw new ArgumentException("All input spans must have the same length", nameof(low));
|
||||
}
|
||||
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] = ComputeTypicalPrice(high[i], low[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.HighValues, source.LowValues, source.CloseValues, output);
|
||||
}
|
||||
|
||||
public static (TSeries Results, Typprice Indicator) Calculate(TBarSeries source)
|
||||
{
|
||||
var indicator = new Typprice();
|
||||
TSeries results = indicator.Update(source);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
# TYPPRICE: Typical Price
|
||||
|
||||
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.
|
||||
|
||||
## 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 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.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### 1. Core Formula
|
||||
|
||||
$$\text{TypPrice}_t = (H_t + L_t + C_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)$$
|
||||
|
||||
The constant $\frac{1}{3}$ is stored as `private const double OneThird = 1.0 / 3.0`, evaluated at compile time. No runtime division occurs.
|
||||
|
||||
### 2. State Management
|
||||
|
||||
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.
|
||||
- **Bar correction**: `isNew=false` rolls back to previous state for same-timestamp rewrites.
|
||||
|
||||
### 3. Complexity
|
||||
|
||||
$O(1)$ per bar. One addition, one FMA. No memory allocation. Always hot after the first bar.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Description | Default | Constraint |
|
||||
|-----------|-------------|---------|------------|
|
||||
| (none) | No user-configurable parameters | | |
|
||||
|
||||
### Why Not Divide by 3?
|
||||
|
||||
Division by a non-power-of-two constant is 4-5x more expensive than multiplication on modern x86 CPUs (~15 cycles vs ~3 cycles). Precomputing $\frac{1}{3}$ as a `const double` and multiplying eliminates the division entirely. The compiler constant-folds `1.0 / 3.0` to the IEEE 754 double `0x3FD5555555555555` at compile time, so the hot path sees only multiply/FMA operations.
|
||||
|
||||
### Pseudo-code
|
||||
|
||||
```
|
||||
function TYPPRICE(bar):
|
||||
const OneThird ← 1.0 / 3.0 // compile-time constant
|
||||
|
||||
h, l, c ← bar.High, bar.Low, bar.Close
|
||||
|
||||
// Substitute last-valid for non-finite inputs
|
||||
if !finite(h): h ← lastValidHigh
|
||||
if !finite(l): l ← lastValidLow
|
||||
if !finite(c): c ← lastValidClose
|
||||
|
||||
result ← FMA(h, OneThird, (l + c) × OneThird)
|
||||
return result
|
||||
```
|
||||
|
||||
### Output Interpretation
|
||||
|
||||
| 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) |
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| 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 |
|
||||
| **Total (hot)** | **3** | | **~8 cycles** |
|
||||
|
||||
### Batch Mode (SIMD Analysis)
|
||||
|
||||
| Aspect | Assessment |
|
||||
|--------|------------|
|
||||
| SIMD vectorizable | Yes: element-wise arithmetic, no inter-bar dependency |
|
||||
| Optimal strategy | `Vector<double>` over H/L/C 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.
|
||||
@@ -0,0 +1,13 @@
|
||||
// TYPPRICE: Typical Price
|
||||
// (High + Low + Close) / 3
|
||||
// TA-Lib compatible — equivalent to TBar.HLC3
|
||||
|
||||
//@version=6
|
||||
indicator("TYPPRICE: Typical Price", overlay=true)
|
||||
|
||||
typprice(float h, float l, float c) =>
|
||||
(h + l + c) / 3.0
|
||||
|
||||
result = typprice(high, low, close)
|
||||
|
||||
plot(result, "TypPrice", color.new(color.green, 0), 2)
|
||||
Reference in New Issue
Block a user