mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 13:08:04 +00:00
Add Yang-Zhang Volatility (YZV) Indicator Implementation
- Introduced YZV class for calculating Yang-Zhang Volatility, a comprehensive volatility measure that incorporates overnight, open-to-close, and high-low components. - Implemented calculation methods, including batch processing for TBarSeries and spans. - Added documentation for YZV, detailing its mathematical foundation, performance profile, and trading applications. - Updated volume index documentation to reflect changes in file paths. - Refactored VWMA calculation method to use a more generic source parameter instead of price.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BiasIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void BiasIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new BiasIndicator();
|
||||
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("BIAS - Price Deviation from SMA", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_MinHistoryDepths_EqualsZero()
|
||||
{
|
||||
var indicator = new BiasIndicator();
|
||||
|
||||
Assert.Equal(0, BiasIndicator.MinHistoryDepths);
|
||||
Assert.Equal(0, ((IWatchlistIndicator)indicator).MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 20 };
|
||||
|
||||
Assert.Contains("BIAS", indicator.ShortName, StringComparison.Ordinal);
|
||||
Assert.Contains("20", indicator.ShortName, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_Initialize_CreatesInternalBias()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_MultipleUpdates_ProducesCorrectBiasSequence()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 100, 100, 110, 100 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 5, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_CalculatesBiasCorrectly()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Add 3 bars with close = 100, then one with close = 110
|
||||
// SMA(3) of [100, 100, 100] = 100
|
||||
// BIAS when price = 100, SMA = 100 → 0%
|
||||
indicator.HistoricalData.AddBar(now, 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
|
||||
// Now add a bar with close = 110
|
||||
// SMA(3) of [100, 100, 110] = 310/3 ≈ 103.333
|
||||
// BIAS = (110 / 103.333) - 1 ≈ 0.0645 (6.45%)
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 110, 110, 110, 110);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
double expectedSma = (100.0 + 100.0 + 110.0) / 3.0;
|
||||
double expectedBias = (110.0 / expectedSma) - 1.0;
|
||||
Assert.Equal(expectedBias, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_ConstantPrice_ZeroBias()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 5 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// All bars at same price should produce zero bias
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100, 100, 100, 100);
|
||||
indicator.ProcessUpdate(i == 0 ? new UpdateArgs(UpdateReason.HistoricalBar) : new UpdateArgs(UpdateReason.NewBar));
|
||||
Assert.Equal(0.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_UpTrend_PositiveBias()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 101, 102, 103, 104, 105 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closes[i], closes[i], closes[i], closes[i]);
|
||||
indicator.ProcessUpdate(i == 0 ? new UpdateArgs(UpdateReason.HistoricalBar) : new UpdateArgs(UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// In uptrend, price should be above SMA, so bias > 0
|
||||
Assert.True(indicator.LinesSeries[0].GetValue(0) > 0, "Bias should be positive in uptrend");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_DownTrend_NegativeBias()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 105, 104, 103, 102, 101, 100 };
|
||||
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), closes[i], closes[i], closes[i], closes[i]);
|
||||
indicator.ProcessUpdate(i == 0 ? new UpdateArgs(UpdateReason.HistoricalBar) : new UpdateArgs(UpdateReason.NewBar));
|
||||
}
|
||||
|
||||
// In downtrend, price should be below SMA, so bias < 0
|
||||
Assert.True(indicator.LinesSeries[0].GetValue(0) < 0, "Bias should be negative in downtrend");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BiasIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new BiasIndicator { Period = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.Period);
|
||||
|
||||
indicator.Period = 100;
|
||||
Assert.Equal(100, indicator.Period);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Drawing;
|
||||
using System.Runtime.CompilerServices;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
[SkipLocalsInit]
|
||||
public sealed class BiasIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 0, minimum: 1, maximum: 10000)]
|
||||
public int Period { get; set; } = 20;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Bias _bias = null!;
|
||||
private readonly LineSeries _series;
|
||||
private string _sourceName = null!;
|
||||
private Func<IHistoryItem, double> _priceSelector = null!;
|
||||
|
||||
public static int MinHistoryDepths => 0;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"BIAS({Period}):{_sourceName}";
|
||||
|
||||
public BiasIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
Name = "BIAS - Price Deviation from SMA";
|
||||
Description = "Measures the percentage difference between price and its Simple Moving Average";
|
||||
_series = new LineSeries(name: "BIAS", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(_series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_priceSelector = Source.GetPriceSelector();
|
||||
_sourceName = Source.ToString();
|
||||
_bias = new Bias(Period);
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
bool isNew = args.IsNewBar();
|
||||
var item = HistoricalData[Count - 1, SeekOriginHistory.Begin];
|
||||
double value = _bias.Update(new TValue(item.TimeLeft.Ticks, _priceSelector(item)), isNew).Value;
|
||||
_series.SetValue(value, _bias.IsHot, ShowColdValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class BiasTests
|
||||
{
|
||||
[Fact]
|
||||
public void Bias_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Bias(0));
|
||||
Assert.Throws<ArgumentException>(() => new Bias(-1));
|
||||
|
||||
var bias = new Bias(10);
|
||||
Assert.NotNull(bias);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Calc_ReturnsValue()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
Assert.Equal(0, bias.Last.Value);
|
||||
|
||||
TValue result = bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(double.IsFinite(result.Value));
|
||||
Assert.Equal(result.Value, bias.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_FirstValue_ReturnsZero()
|
||||
{
|
||||
// Bias = (Price - SMA) / SMA = (100 - 100) / 100 = 0
|
||||
var bias = new Bias(10);
|
||||
|
||||
TValue result = bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(0.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = bias.Last.Value;
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = bias.Last.Value;
|
||||
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = bias.Last.Value;
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = bias.Last.Value;
|
||||
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Reset_ClearsState()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = bias.Last.Value;
|
||||
|
||||
bias.Reset();
|
||||
|
||||
Assert.Equal(0, bias.Last.Value);
|
||||
Assert.False(bias.IsHot);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(0, bias.Last.Value); // First value, Bias = 0
|
||||
Assert.NotEqual(valueBefore, bias.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Properties_Accessible()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
Assert.Equal(0, bias.Last.Value);
|
||||
Assert.False(bias.IsHot);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(0, bias.Last.Value); // First value, Bias = 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
Assert.False(bias.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(bias.IsHot);
|
||||
}
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(bias.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_CalculatesCorrectBias()
|
||||
{
|
||||
// Bias = (Price - SMA) / SMA
|
||||
var bias = new Bias(3);
|
||||
|
||||
// Value 10: SMA = 10, Bias = (10-10)/10 = 0
|
||||
bias.Update(new TValue(DateTime.UtcNow, 10));
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
|
||||
// Value 20: SMA = (10+20)/2 = 15, Bias = (20-15)/15 = 1/3
|
||||
bias.Update(new TValue(DateTime.UtcNow, 20));
|
||||
Assert.Equal(1.0 / 3.0, bias.Last.Value, 1e-10);
|
||||
|
||||
// Value 30: SMA = (10+20+30)/3 = 20, Bias = (30-20)/20 = 0.5
|
||||
bias.Update(new TValue(DateTime.UtcNow, 30));
|
||||
Assert.Equal(0.5, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_SlidingWindow_Works()
|
||||
{
|
||||
var bias = new Bias(3);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 10));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 20));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 30));
|
||||
// SMA = 20, Bias = (30-20)/20 = 0.5
|
||||
Assert.Equal(0.5, bias.Last.Value, 1e-10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 40));
|
||||
// SMA = (20+30+40)/3 = 30, Bias = (40-30)/30 = 1/3
|
||||
Assert.Equal(1.0 / 3.0, bias.Last.Value, 1e-10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 50));
|
||||
// SMA = (30+40+50)/3 = 40, Bias = (50-40)/40 = 0.25
|
||||
Assert.Equal(0.25, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
bias.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember state after 10 values
|
||||
double stateAfterTen = bias.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
bias.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalResult = bias.Update(tenthInput, isNew: false);
|
||||
|
||||
// State should match the original state after 10 values
|
||||
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var biasIterative = new Bias(10);
|
||||
var biasBatch = new Bias(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(biasIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = biasBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterNaN = bias.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
var resultAfterPosInf = bias.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
|
||||
var resultAfterNegInf = bias.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_MultipleNaN_ContinuesWithLastValid()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 110));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 120));
|
||||
|
||||
var r1 = bias.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r2 = bias.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
var r3 = bias.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
Assert.True(double.IsFinite(r1.Value));
|
||||
Assert.True(double.IsFinite(r2.Value));
|
||||
Assert.True(double.IsFinite(r3.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_BatchCalc_HandlesNaN()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow.Ticks, 100);
|
||||
series.Add(DateTime.UtcNow.Ticks + 1, 110);
|
||||
series.Add(DateTime.UtcNow.Ticks + 2, double.NaN);
|
||||
series.Add(DateTime.UtcNow.Ticks + 3, 120);
|
||||
series.Add(DateTime.UtcNow.Ticks + 4, double.PositiveInfinity);
|
||||
series.Add(DateTime.UtcNow.Ticks + 5, 130);
|
||||
|
||||
var results = bias.Update(series);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
Assert.True(double.IsFinite(result.Value), $"Expected finite value but got {result.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Reset_ClearsLastValidValue()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
bias.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
bias.Reset();
|
||||
|
||||
var result = bias.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.Equal(0.0, result.Value, 1e-10); // First value, Bias = 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_StaticBatch_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow.Ticks, 10);
|
||||
series.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
series.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
series.Add(DateTime.UtcNow.Ticks + 3, 40);
|
||||
series.Add(DateTime.UtcNow.Ticks + 4, 50);
|
||||
|
||||
var results = Bias.Batch(series, 3);
|
||||
|
||||
Assert.Equal(5, results.Count);
|
||||
// Last value: SMA(3) = (30+40+50)/3 = 40, Bias = (50-40)/40 = 0.25
|
||||
Assert.Equal(0.25, results.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_FlatLine_ReturnsZero()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
}
|
||||
|
||||
// Price = SMA = 100, Bias = (100-100)/100 = 0
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Bias_SpanBatch_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
Assert.Throws<ArgumentException>(() => Bias.Batch(source.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Bias.Batch(source.AsSpan(), output.AsSpan(), -1));
|
||||
Assert.Throws<ArgumentException>(() => Bias.Batch(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_SpanBatch_MatchesTSeriesBatch()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
var tseriesResult = Bias.Batch(series, 10);
|
||||
Bias.Batch(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_SpanBatch_CalculatesCorrectly()
|
||||
{
|
||||
double[] source = [10, 20, 30, 40, 50];
|
||||
double[] output = new double[5];
|
||||
|
||||
Bias.Batch(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
// i=0: SMA=10, Bias=(10-10)/10=0
|
||||
Assert.Equal(0.0, output[0], 1e-10);
|
||||
// i=1: SMA=15, Bias=(20-15)/15=1/3
|
||||
Assert.Equal(1.0 / 3.0, output[1], 1e-10);
|
||||
// i=2: SMA=20, Bias=(30-20)/20=0.5
|
||||
Assert.Equal(0.5, output[2], 1e-10);
|
||||
// i=3: SMA=30, Bias=(40-30)/30=1/3
|
||||
Assert.Equal(1.0 / 3.0, output[3], 1e-10);
|
||||
// i=4: SMA=40, Bias=(50-40)/40=0.25
|
||||
Assert.Equal(0.25, output[4], 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_SpanBatch_ZeroAllocation()
|
||||
{
|
||||
double[] source = new double[10000];
|
||||
double[] output = new double[10000];
|
||||
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 42);
|
||||
for (int i = 0; i < source.Length; i++)
|
||||
{
|
||||
source[i] = gbm.Next().Close;
|
||||
}
|
||||
|
||||
Bias.Batch(source.AsSpan(), output.AsSpan(), 100);
|
||||
|
||||
Assert.True(double.IsFinite(output[^1]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_SpanBatch_HandlesNaN()
|
||||
{
|
||||
double[] source = [100, 110, double.NaN, 120, 130];
|
||||
double[] output = new double[5];
|
||||
|
||||
Bias.Batch(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
foreach (var val in output)
|
||||
{
|
||||
Assert.True(double.IsFinite(val), $"Expected finite value but got {val}");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
const int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Bias.Batch(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Bias.Batch(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Bias(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Bias(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 9);
|
||||
Assert.Equal(expected, eventingResult, precision: 9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Chainability_Works()
|
||||
{
|
||||
var source = new TSeries();
|
||||
var bias = new Bias(source, 10);
|
||||
|
||||
source.Add(new TValue(DateTime.UtcNow, 100));
|
||||
Assert.Equal(0, bias.Last.Value); // First value, Bias = 0
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_WarmupPeriod_IsSetCorrectly()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
Assert.Equal(10, bias.WarmupPeriod);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Prime_SetsStateCorrectly()
|
||||
{
|
||||
var bias = new Bias(5);
|
||||
double[] history = [10, 20, 30, 40, 50];
|
||||
// SMA = 30, Bias = (50-30)/30 = 2/3
|
||||
|
||||
bias.Prime(history);
|
||||
|
||||
Assert.True(bias.IsHot);
|
||||
Assert.Equal(2.0 / 3.0, bias.Last.Value, 1e-10);
|
||||
|
||||
// Verify it continues correctly with sliding window
|
||||
bias.Update(new TValue(DateTime.UtcNow, 60));
|
||||
// SMA = (20+30+40+50+60)/5 = 40, Bias = (60-40)/40 = 0.5
|
||||
Assert.Equal(0.5, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Prime_WithInsufficientHistory_IsNotHot()
|
||||
{
|
||||
var bias = new Bias(10);
|
||||
double[] history = [10, 20, 30, 40, 50];
|
||||
|
||||
bias.Prime(history);
|
||||
|
||||
Assert.False(bias.IsHot);
|
||||
Assert.True(double.IsFinite(bias.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Prime_HandlesNaN_InHistory()
|
||||
{
|
||||
var bias = new Bias(3);
|
||||
double[] history = [10, 20, double.NaN, 40];
|
||||
// Values used: 10, 20, 20 (NaN replaced), 40
|
||||
// Final window (3): 20, 20, 40 - SMA = 26.67
|
||||
|
||||
bias.Prime(history);
|
||||
|
||||
Assert.True(bias.IsHot);
|
||||
Assert.True(double.IsFinite(bias.Last.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Calculate_ReturnsCorrectResultsAndHotIndicator()
|
||||
{
|
||||
var series = new TSeries();
|
||||
for (int i = 1; i <= 10; i++)
|
||||
{
|
||||
series.Add(DateTime.UtcNow, i * 10);
|
||||
}
|
||||
// 10, 20, 30, 40, 50, 60, 70, 80, 90, 100
|
||||
|
||||
var (results, indicator) = Bias.Calculate(series, 5);
|
||||
|
||||
// Check results
|
||||
Assert.Equal(10, results.Count);
|
||||
|
||||
// Check indicator state
|
||||
Assert.True(indicator.IsHot);
|
||||
Assert.Equal(5, indicator.WarmupPeriod);
|
||||
|
||||
// Verify indicator continues correctly
|
||||
indicator.Update(new TValue(DateTime.UtcNow, 110));
|
||||
// SMA = (70+80+90+100+110)/5 = 90, Bias = (110-90)/90 = 2/9
|
||||
Assert.Equal(2.0 / 9.0, indicator.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_Period1_ReturnsPriceMinusSmaOverSma()
|
||||
{
|
||||
var bias = new Bias(1);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100));
|
||||
// SMA(1) = 100, Bias = (100-100)/100 = 0
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 200));
|
||||
// SMA(1) = 200, Bias = (200-200)/200 = 0
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 150));
|
||||
// SMA(1) = 150, Bias = (150-150)/150 = 0
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_NegativePrice_CalculatesCorrectly()
|
||||
{
|
||||
var bias = new Bias(3);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, -10));
|
||||
bias.Update(new TValue(DateTime.UtcNow, -20));
|
||||
bias.Update(new TValue(DateTime.UtcNow, -30));
|
||||
// SMA = -20, Bias = (-30 - (-20)) / (-20) = -10 / -20 = 0.5
|
||||
Assert.Equal(0.5, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bias_ZeroPrice_HandlesGracefully()
|
||||
{
|
||||
var bias = new Bias(3);
|
||||
|
||||
bias.Update(new TValue(DateTime.UtcNow, 0));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 0));
|
||||
bias.Update(new TValue(DateTime.UtcNow, 0));
|
||||
// SMA = 0, Bias = (0-0)/0 = 0/0 -> should return 0 to avoid NaN
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,436 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Validation tests for Bias indicator.
|
||||
/// Validates against mathematical calculations since BIAS = (Price - SMA) / SMA.
|
||||
/// No direct TA-Lib/Tulip/Skender equivalent exists.
|
||||
/// </summary>
|
||||
public sealed class BiasValidationTests : IDisposable
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
private bool _disposed;
|
||||
|
||||
public BiasValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (disposing)
|
||||
{
|
||||
_testData?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Batch()
|
||||
{
|
||||
const int period = 10;
|
||||
var bias = new Bias(period);
|
||||
var qResult = bias.Update(_testData.Data);
|
||||
|
||||
var rawData = _testData.RawData.ToArray();
|
||||
|
||||
for (int i = 0; i < rawData.Length; i++)
|
||||
{
|
||||
// Calculate SMA manually
|
||||
double sum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
int windowSize = i - startIdx + 1;
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
sum += rawData[j];
|
||||
}
|
||||
double sma = sum / windowSize;
|
||||
|
||||
// BIAS = (Price - SMA) / SMA = Price/SMA - 1
|
||||
double expectedBias = sma != 0 ? (rawData[i] / sma) - 1.0 : 0;
|
||||
|
||||
double qValue = qResult[i].Value;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qValue - expectedBias) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qValue:G17}, Expected={expectedBias:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Bias Batch(TSeries) validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Streaming()
|
||||
{
|
||||
int period = 10;
|
||||
var bias = new Bias(period);
|
||||
var qResults = new List<double>();
|
||||
var rawData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
qResults.Add(bias.Update(item).Value);
|
||||
}
|
||||
|
||||
for (int i = 0; i < rawData.Length; i++)
|
||||
{
|
||||
// Calculate SMA manually
|
||||
double sum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
int windowSize = i - startIdx + 1;
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
sum += rawData[j];
|
||||
}
|
||||
double sma = sum / windowSize;
|
||||
|
||||
// BIAS = (Price - SMA) / SMA = Price/SMA - 1
|
||||
double expectedBias = sma != 0 ? (rawData[i] / sma) - 1.0 : 0;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qResults[i] - expectedBias) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qResults[i]:G17}, Expected={expectedBias:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Bias Streaming validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MathematicalCorrectness_Span()
|
||||
{
|
||||
int period = 10;
|
||||
var sourceData = _testData.RawData.ToArray();
|
||||
var qOutput = new double[sourceData.Length];
|
||||
|
||||
Bias.Batch(sourceData.AsSpan(), qOutput.AsSpan(), period);
|
||||
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
// Calculate SMA manually
|
||||
double sum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
int windowSize = i - startIdx + 1;
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
sum += sourceData[j];
|
||||
}
|
||||
double sma = sum / windowSize;
|
||||
|
||||
// BIAS = (Price - SMA) / SMA = Price/SMA - 1
|
||||
double expectedBias = sma != 0 ? (sourceData[i] / sma) - 1.0 : 0;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qOutput[i] - expectedBias) <= ValidationHelper.DefaultTolerance,
|
||||
$"Mismatch at index {i}: QuanTAlib={qOutput[i]:G17}, Expected={expectedBias:G17}");
|
||||
}
|
||||
|
||||
_output.WriteLine("Bias Span validated against manual calculation");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues_UpTrend()
|
||||
{
|
||||
// Steadily increasing prices: bias should be positive after warmup
|
||||
double[] values = [100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110];
|
||||
var bias = new Bias(5);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, values[i]));
|
||||
|
||||
// Calculate expected
|
||||
int startIdx = Math.Max(0, i - 4);
|
||||
double sum = 0;
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
sum += values[j];
|
||||
}
|
||||
double sma = sum / (i - startIdx + 1);
|
||||
double expectedBias = (values[i] / sma) - 1.0;
|
||||
|
||||
Assert.Equal(expectedBias, bias.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// After warmup, bias should be positive (price above SMA)
|
||||
Assert.True(bias.Last.Value > 0, "Bias should be positive in uptrend");
|
||||
_output.WriteLine($"Uptrend bias: {bias.Last.Value:P4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues_DownTrend()
|
||||
{
|
||||
// Steadily decreasing prices: bias should be negative after warmup
|
||||
double[] values = [110, 109, 108, 107, 106, 105, 104, 103, 102, 101, 100];
|
||||
var bias = new Bias(5);
|
||||
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, values[i]));
|
||||
}
|
||||
|
||||
// After warmup, bias should be negative (price below SMA)
|
||||
Assert.True(bias.Last.Value < 0, "Bias should be negative in downtrend");
|
||||
_output.WriteLine($"Downtrend bias: {bias.Last.Value:P4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues_Constant()
|
||||
{
|
||||
// Constant prices: bias should be exactly 0
|
||||
double constant = 100.0;
|
||||
var bias = new Bias(10);
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, constant));
|
||||
}
|
||||
|
||||
// Bias = (Price - SMA) / SMA = (100 - 100) / 100 = 0
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
_output.WriteLine("Constant sequence bias = 0 confirmed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues_SinglePriceSpike()
|
||||
{
|
||||
// 9 values at 100, then one spike to 200
|
||||
var bias = new Bias(10);
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
bias.Update(new TValue(DateTime.UtcNow, 200.0));
|
||||
|
||||
// SMA = (9 * 100 + 200) / 10 = 1100 / 10 = 110
|
||||
// BIAS = (200 / 110) - 1 = 1.8181818... - 1 = 0.8181818...
|
||||
double expectedSma = 110.0;
|
||||
double expectedBias = (200.0 / expectedSma) - 1.0;
|
||||
|
||||
Assert.Equal(expectedBias, bias.Last.Value, 1e-10);
|
||||
_output.WriteLine($"Single spike bias: {bias.Last.Value:P4} (expected {expectedBias:P4})");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_KnownValues_PriceAtSMA()
|
||||
{
|
||||
// When current price equals SMA, bias should be 0
|
||||
// Use sequence where last value equals the average
|
||||
// Values: 90, 110, 90, 110, 100 → SMA(5) = 100, last price = 100 → bias = 0
|
||||
double[] values = [90, 110, 90, 110, 100];
|
||||
var bias = new Bias(5);
|
||||
|
||||
foreach (var val in values)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, val));
|
||||
}
|
||||
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
_output.WriteLine("Price at SMA produces bias = 0 confirmed");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NumericalStability_LargeValues()
|
||||
{
|
||||
// Test numerical stability with large values
|
||||
var bias = new Bias(100);
|
||||
double baseValue = 1e10;
|
||||
|
||||
for (int i = 0; i < 1000; i++)
|
||||
{
|
||||
double value = baseValue + i;
|
||||
bias.Update(new TValue(DateTime.UtcNow, value));
|
||||
|
||||
if (i >= 99) // After warmup
|
||||
{
|
||||
Assert.True(double.IsFinite(bias.Last.Value), $"Bias should be finite at index {i}");
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine($"Large values stability test passed: {bias.Last.Value:G10}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_NumericalStability_SmallValues()
|
||||
{
|
||||
// Test with small values
|
||||
var bias = new Bias(10);
|
||||
double baseValue = 1e-10;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double value = baseValue * (1 + i * 0.01);
|
||||
bias.Update(new TValue(DateTime.UtcNow, value));
|
||||
|
||||
Assert.True(double.IsFinite(bias.Last.Value), $"Bias should be finite at index {i}");
|
||||
}
|
||||
|
||||
_output.WriteLine($"Small values stability test passed: {bias.Last.Value:G10}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AllModes_Consistency()
|
||||
{
|
||||
int period = 20;
|
||||
var sourceData = _testData.RawData.ToArray();
|
||||
|
||||
// Mode 1: TSeries Batch
|
||||
var bias1 = new Bias(period);
|
||||
var batchResult = bias1.Update(_testData.Data);
|
||||
|
||||
// Mode 2: Streaming
|
||||
var bias2 = new Bias(period);
|
||||
var streamingResults = new List<double>();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamingResults.Add(bias2.Update(item).Value);
|
||||
}
|
||||
|
||||
// Mode 3: Span
|
||||
var spanOutput = new double[sourceData.Length];
|
||||
Bias.Batch(sourceData.AsSpan(), spanOutput.AsSpan(), period);
|
||||
|
||||
// Compare all three
|
||||
for (int i = 0; i < sourceData.Length; i++)
|
||||
{
|
||||
double batchVal = batchResult[i].Value;
|
||||
double streamVal = streamingResults[i];
|
||||
double spanVal = spanOutput[i];
|
||||
|
||||
Assert.Equal(batchVal, streamVal, 1e-10);
|
||||
Assert.Equal(batchVal, spanVal, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("All Bias calculation modes produce consistent results");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_MultiplePeriods()
|
||||
{
|
||||
int[] periods = [5, 10, 20, 50, 100];
|
||||
var rawData = _testData.RawData.ToArray();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
var bias = new Bias(period);
|
||||
var qResult = bias.Update(_testData.Data);
|
||||
|
||||
// Verify last 50 values
|
||||
for (int i = rawData.Length - 50; i < rawData.Length; i++)
|
||||
{
|
||||
// Calculate SMA manually
|
||||
double sum = 0;
|
||||
int startIdx = Math.Max(0, i - period + 1);
|
||||
int windowSize = i - startIdx + 1;
|
||||
for (int j = startIdx; j <= i; j++)
|
||||
{
|
||||
sum += rawData[j];
|
||||
}
|
||||
double sma = sum / windowSize;
|
||||
double expectedBias = sma != 0 ? (rawData[i] / sma) - 1.0 : 0;
|
||||
|
||||
Assert.True(
|
||||
Math.Abs(qResult[i].Value - expectedBias) <= ValidationHelper.DefaultTolerance,
|
||||
$"Period {period}, index {i}: QuanTAlib={qResult[i].Value:G17}, Expected={expectedBias:G17}");
|
||||
}
|
||||
}
|
||||
|
||||
_output.WriteLine("Bias validated for multiple periods");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_PercentageInterpretation()
|
||||
{
|
||||
// Bias of 0.05 means price is 5% above SMA
|
||||
// Bias of -0.05 means price is 5% below SMA
|
||||
var bias = new Bias(10);
|
||||
|
||||
// Create scenario where we know the exact bias
|
||||
// SMA will be 100, price will be 105 → bias = 0.05
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
// For 10th value: need SMA = 100 and price = 105
|
||||
// SMA of (9 * 100 + x) / 10 = 100 → x = 100
|
||||
// So we add another 100 first
|
||||
bias.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
Assert.Equal(0.0, bias.Last.Value, 1e-10);
|
||||
|
||||
// Now add one more value at 105 (old 100 drops out, new comes in)
|
||||
bias.Update(new TValue(DateTime.UtcNow, 105.0));
|
||||
// SMA = (9 * 100 + 105) / 10 = 1005 / 10 = 100.5
|
||||
// Bias = (105 / 100.5) - 1 = 1.04477... - 1 ≈ 0.04478
|
||||
double expectedSma = 100.5;
|
||||
double expectedBias = (105.0 / expectedSma) - 1.0;
|
||||
Assert.Equal(expectedBias, bias.Last.Value, 1e-10);
|
||||
|
||||
_output.WriteLine($"Percentage interpretation validated: {bias.Last.Value:P4}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_AgainstSmaIndicator()
|
||||
{
|
||||
// Cross-validate with Sma indicator
|
||||
int period = 20;
|
||||
var bias = new Bias(period);
|
||||
var sma = new Sma(period);
|
||||
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
var biasResult = bias.Update(item);
|
||||
var smaResult = sma.Update(item);
|
||||
|
||||
// BIAS = (Price - SMA) / SMA = Price/SMA - 1
|
||||
double expectedBias = smaResult.Value != 0
|
||||
? (item.Value / smaResult.Value) - 1.0
|
||||
: 0;
|
||||
|
||||
Assert.Equal(expectedBias, biasResult.Value, 1e-10);
|
||||
}
|
||||
|
||||
_output.WriteLine("Bias validated against Sma indicator");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_OscillatingSequence()
|
||||
{
|
||||
// Oscillating around a mean: bias should oscillate around 0
|
||||
var bias = new Bias(10);
|
||||
double mean = 100.0;
|
||||
double amplitude = 10.0;
|
||||
|
||||
var biasValues = new List<double>();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double value = mean + amplitude * Math.Sin(i * 0.5);
|
||||
bias.Update(new TValue(DateTime.UtcNow, value));
|
||||
if (i >= 9) // After warmup
|
||||
{
|
||||
biasValues.Add(bias.Last.Value);
|
||||
}
|
||||
}
|
||||
|
||||
// Average bias should be close to 0 for oscillating sequence
|
||||
double avgBias = biasValues.Average();
|
||||
Assert.True(Math.Abs(avgBias) < 0.01, $"Average bias should be near 0, got {avgBias}");
|
||||
|
||||
// Should have both positive and negative values
|
||||
Assert.True(biasValues.Any(b => b > 0), "Should have positive bias values");
|
||||
Assert.True(biasValues.Any(b => b < 0), "Should have negative bias values");
|
||||
|
||||
_output.WriteLine($"Oscillating sequence: avg bias = {avgBias:F6}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// Bias: Measures the percentage deviation of a price from its moving average.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bias (BIAS) calculates how far the current price deviates from its Simple Moving Average (SMA),
|
||||
/// expressed as a percentage. It's commonly used to identify overbought/oversold conditions.
|
||||
///
|
||||
/// Formula:
|
||||
/// BIAS = (Price - SMA) / SMA = Price/SMA - 1
|
||||
///
|
||||
/// Key Features:
|
||||
/// - O(1) time complexity per update using running sum
|
||||
/// - Zero allocation in hot path
|
||||
/// - Handles division by zero (returns 0 when SMA is 0)
|
||||
/// - NaN/Infinity safe with last-valid-value substitution
|
||||
///
|
||||
/// IsHot:
|
||||
/// Becomes true when the buffer is full (period samples processed).
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Bias : AbstractBase
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly RingBuffer _buffer;
|
||||
private readonly TValuePublishedHandler _handler;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State
|
||||
{
|
||||
public double Sum;
|
||||
public double LastInput;
|
||||
public double LastValidValue;
|
||||
public int TickCount;
|
||||
}
|
||||
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Creates Bias with specified period.
|
||||
/// </summary>
|
||||
/// <param name="period">Number of values for SMA calculation (must be > 0)</param>
|
||||
public Bias(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
_period = period;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Bias({period})";
|
||||
WarmupPeriod = period;
|
||||
_handler = Handle;
|
||||
}
|
||||
|
||||
public Bias(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
}
|
||||
|
||||
public Bias(TSeries source, int period) : this(period)
|
||||
{
|
||||
source.Pub += _handler;
|
||||
Prime(source.Values);
|
||||
if (source.Count > 0)
|
||||
{
|
||||
Last = new TValue(source.LastTime, Last.Value);
|
||||
}
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
private void Handle(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
|
||||
|
||||
/// <summary>
|
||||
/// True if Bias has enough data to produce valid results.
|
||||
/// Bias is "hot" when the buffer is full (has received at least 'period' values).
|
||||
/// </summary>
|
||||
public override bool IsHot => _buffer.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the indicator state using the provided history.
|
||||
/// </summary>
|
||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||
{
|
||||
if (source.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset state
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
|
||||
int warmupLength = Math.Min(source.Length, WarmupPeriod);
|
||||
int startIndex = source.Length - warmupLength;
|
||||
|
||||
// Seed LastValidValue
|
||||
_state.LastValidValue = double.NaN;
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (double.IsNaN(_state.LastValidValue))
|
||||
{
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
if (double.IsFinite(source[i]))
|
||||
{
|
||||
_state.LastValidValue = source[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Feed the buffer and calculate sum
|
||||
for (int i = startIndex; i < source.Length; i++)
|
||||
{
|
||||
double val = GetValidValue(source[i]);
|
||||
_buffer.Add(val);
|
||||
_state.Sum += val;
|
||||
_state.LastInput = val;
|
||||
}
|
||||
|
||||
// Calculate final Bias
|
||||
double sma = _state.Sum / _buffer.Count;
|
||||
double bias = sma != 0 ? (_state.LastInput - sma) / sma : 0;
|
||||
Last = new TValue(DateTime.MinValue, bias);
|
||||
_p_state = _state;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double val)
|
||||
{
|
||||
if (_buffer.Count == _buffer.Capacity)
|
||||
{
|
||||
_state.Sum -= _buffer.Oldest;
|
||||
}
|
||||
|
||||
_buffer.Add(val);
|
||||
_state.Sum += val;
|
||||
|
||||
_state.TickCount++;
|
||||
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.Sum = _buffer.GetSpan().SumSIMD();
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
_p_state = _state;
|
||||
_buffer.Snapshot();
|
||||
|
||||
double val = GetValidValue(input.Value);
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore both scalar state and buffer state
|
||||
_state = _p_state;
|
||||
_buffer.Restore();
|
||||
|
||||
// Use restored LastValidValue for NaN handling without updating it
|
||||
double val = double.IsFinite(input.Value) ? input.Value : _state.LastValidValue;
|
||||
|
||||
// Replicate the same operation as isNew=true: UpdateState
|
||||
// This properly removes oldest and adds newest, maintaining sliding window
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
}
|
||||
|
||||
// Calculate Bias: (Price - SMA) / SMA
|
||||
double sma = _state.Sum / _buffer.Count;
|
||||
double bias = sma != 0 ? (_state.LastInput - sma) / sma : 0;
|
||||
|
||||
Last = new TValue(input.Time, bias);
|
||||
PubEvent(Last, isNew);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public override TSeries Update(TSeries 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.Values, vSpan, _period);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
Prime(source.Values);
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Bias for the entire series using a new instance.
|
||||
/// </summary>
|
||||
public static TSeries Batch(TSeries source, int period)
|
||||
{
|
||||
var bias = new Bias(period);
|
||||
return bias.Update(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates Bias in-place using O(1) running sum.
|
||||
/// Zero-allocation method for maximum performance.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Batch(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
if (source.Length != output.Length)
|
||||
{
|
||||
throw new ArgumentException("Source and output must have the same length", nameof(output));
|
||||
}
|
||||
|
||||
if (period <= 0)
|
||||
{
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
}
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CalculateScalarCore(source, output, period);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs a batch calculation and returns a "Hot" Bias instance.
|
||||
/// </summary>
|
||||
public static (TSeries Results, Bias Indicator) Calculate(TSeries source, int period)
|
||||
{
|
||||
var bias = new Bias(period);
|
||||
TSeries results = bias.Update(source);
|
||||
return (results, bias);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Length;
|
||||
|
||||
const int StackAllocThreshold = 256;
|
||||
double[]? bufferArray = period > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(period) : null;
|
||||
Span<double> buffer = period <= StackAllocThreshold
|
||||
? stackalloc double[period]
|
||||
: bufferArray!.AsSpan(0, period);
|
||||
|
||||
double sum = 0;
|
||||
double lastValid = double.NaN;
|
||||
|
||||
// Find first valid value
|
||||
for (int k = 0; k < len; k++)
|
||||
{
|
||||
if (double.IsFinite(source[k]))
|
||||
{
|
||||
lastValid = source[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
int bufferIndex = 0;
|
||||
int tickCount = 0;
|
||||
|
||||
// Warmup phase
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
for (int i = 0; i < warmupEnd; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
sum += val;
|
||||
buffer[i] = val;
|
||||
|
||||
double n = i + 1;
|
||||
double sma = sum / n;
|
||||
output[i] = sma != 0 ? (val - sma) / sma : 0;
|
||||
}
|
||||
|
||||
// Main phase with sliding window
|
||||
for (int i = period; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
{
|
||||
lastValid = val;
|
||||
}
|
||||
else
|
||||
{
|
||||
val = lastValid;
|
||||
}
|
||||
|
||||
double oldVal = buffer[bufferIndex];
|
||||
sum = sum - oldVal + val;
|
||||
|
||||
buffer[bufferIndex] = val;
|
||||
bufferIndex++;
|
||||
if (bufferIndex >= period)
|
||||
{
|
||||
bufferIndex = 0;
|
||||
}
|
||||
|
||||
double sma = sum / period;
|
||||
output[i] = sma != 0 ? (val - sma) / sma : 0;
|
||||
|
||||
// Periodic resync for long sequences
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
sum = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
sum += buffer[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (bufferArray != null)
|
||||
{
|
||||
ArrayPool<double>.Shared.Return(bufferArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Bias state.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
# Bias: Price Deviation from Moving Average
|
||||
|
||||
> "When traders ask 'how overbought is it?', they're really asking how far price has strayed from its anchor. Bias answers that question in percentage terms, telling you whether the current price is 5% above or 10% below its moving average. It's the market's stretch marks made visible."
|
||||
|
||||
The Bias indicator measures the percentage difference between the current price and its Simple Moving Average (SMA). A positive bias indicates price is above the average (potentially overbought), while negative bias suggests price is below average (potentially oversold). This is one of the simplest yet most effective tools for identifying mean-reversion opportunities.
|
||||
|
||||
## Historical Context
|
||||
|
||||
Bias (also known as "Rate of Change from MA" or "Price Oscillator Percentage") has been used by traders since the early days of technical analysis. The concept is straightforward: prices tend to oscillate around their moving averages, and extreme deviations often precede reversions.
|
||||
|
||||
In TradingView's PineScript, this indicator is known simply as "Bias" and represents the normalized distance between price and its moving average. Unlike momentum oscillators that measure price change over time, Bias measures price deviation from a smoothed reference point.
|
||||
|
||||
## Architecture & Physics
|
||||
|
||||
### The Mean-Reversion Foundation
|
||||
|
||||
Markets exhibit mean-reversion tendencies across multiple timeframes. When price deviates significantly from its moving average, several forces conspire to pull it back:
|
||||
|
||||
1. **Value Seekers**: Buyers appear when price falls too far below average
|
||||
2. **Profit Taking**: Sellers emerge when price rises too far above average
|
||||
3. **Statistical Gravity**: Extreme deviations are by definition unsustainable
|
||||
|
||||
Bias quantifies this deviation in normalized (percentage) terms, making it comparable across different price scales.
|
||||
|
||||
### Calculation Formula
|
||||
|
||||
The Bias is computed as:
|
||||
|
||||
$$\text{Bias} = \frac{P - \text{SMA}}{\text{SMA}} = \frac{P}{\text{SMA}} - 1$$
|
||||
|
||||
Where:
|
||||
|
||||
* $P$ = Current price
|
||||
* $\text{SMA}$ = Simple Moving Average over the specified period
|
||||
|
||||
This formula normalizes the deviation as a ratio, expressing it as a decimal. A Bias of 0.05 means price is 5% above the SMA; a Bias of -0.10 means price is 10% below.
|
||||
|
||||
### O(1) Streaming Implementation
|
||||
|
||||
Rather than recalculating the SMA from scratch each tick, this implementation maintains a running sum with RingBuffer:
|
||||
|
||||
```csharp
|
||||
// Efficient sliding window sum
|
||||
_sum = _sum - oldValue + newValue;
|
||||
double sma = _sum / Period;
|
||||
double bias = (currentPrice / sma) - 1.0;
|
||||
```
|
||||
|
||||
This achieves constant-time updates regardless of period length.
|
||||
|
||||
## Mathematical Foundation
|
||||
|
||||
### 1. Simple Moving Average
|
||||
|
||||
$$\text{SMA}_t = \frac{1}{n} \sum_{i=0}^{n-1} P_{t-i}$$
|
||||
|
||||
Where $n$ is the period and $P_t$ is the price at time $t$.
|
||||
|
||||
### 2. Bias Calculation
|
||||
|
||||
$$\text{Bias}_t = \frac{P_t - \text{SMA}_t}{\text{SMA}_t}$$
|
||||
|
||||
Equivalently:
|
||||
|
||||
$$\text{Bias}_t = \frac{P_t}{\text{SMA}_t} - 1$$
|
||||
|
||||
### 3. Relationship to Price
|
||||
|
||||
Given the Bias value, you can recover the implied SMA:
|
||||
|
||||
$$\text{SMA}_t = \frac{P_t}{1 + \text{Bias}_t}$$
|
||||
|
||||
### 4. Boundary Behavior
|
||||
|
||||
* When $P_t = \text{SMA}_t$: $\text{Bias} = 0$
|
||||
* When $P_t > \text{SMA}_t$: $\text{Bias} > 0$
|
||||
* When $P_t < \text{SMA}_t$: $\text{Bias} < 0$
|
||||
* When $\text{SMA}_t = 0$: $\text{Bias} = 0$ (division guard)
|
||||
|
||||
## Performance Profile
|
||||
|
||||
### Operation Count (Streaming Mode)
|
||||
|
||||
| Operation | Count | Cost (cycles) | Subtotal |
|
||||
| :--- | :---: | :---: | :---: |
|
||||
| ADD/SUB | 3 | 1 | 3 |
|
||||
| MUL | 0 | 3 | 0 |
|
||||
| DIV | 2 | 15 | 30 |
|
||||
| Buffer Access | 2 | 3 | 6 |
|
||||
| **Total** | **7** | — | **~39 cycles** |
|
||||
|
||||
Division dominates the cost (two divisions: one for SMA, one for bias ratio).
|
||||
|
||||
### Quality Metrics
|
||||
|
||||
| Metric | Score | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **Accuracy** | 10/10 | Exact formula, no approximation |
|
||||
| **Timeliness** | 7/10 | Inherits SMA lag |
|
||||
| **Smoothness** | 8/10 | SMA provides inherent smoothing |
|
||||
| **Interpretability** | 10/10 | Direct percentage meaning |
|
||||
|
||||
## Validation
|
||||
|
||||
| Library | Status | Notes |
|
||||
| :--- | :---: | :--- |
|
||||
| **TA-Lib** | N/A | No direct equivalent |
|
||||
| **Skender** | N/A | No direct equivalent |
|
||||
| **Tulip** | N/A | No direct equivalent |
|
||||
| **TradingView** | ✅ | Matches PineScript Bias indicator |
|
||||
| **Mathematical** | ✅ | Validated against manual calculation |
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Mean-Reversion Trading
|
||||
|
||||
* **Long Entry**: Bias < -0.05 (price 5%+ below SMA)
|
||||
* **Short Entry**: Bias > 0.05 (price 5%+ above SMA)
|
||||
* **Exit**: Bias returns toward zero
|
||||
|
||||
### 2. Trend Confirmation
|
||||
|
||||
Persistent positive Bias confirms uptrend; persistent negative confirms downtrend.
|
||||
|
||||
### 3. Overbought/Oversold Detection
|
||||
|
||||
Extreme Bias values (e.g., ±10%) suggest extended conditions ripe for reversal.
|
||||
|
||||
### 4. Multi-Timeframe Analysis
|
||||
|
||||
Compare Bias across different periods to identify nested mean-reversion setups.
|
||||
|
||||
## API Usage
|
||||
|
||||
### Streaming Mode
|
||||
|
||||
```csharp
|
||||
var bias = new Bias(period: 20);
|
||||
foreach (var price in prices)
|
||||
{
|
||||
var result = bias.Update(new TValue(DateTime.UtcNow, price));
|
||||
Console.WriteLine($"Bias: {result.Value:P2}"); // e.g., "Bias: 3.45%"
|
||||
}
|
||||
```
|
||||
|
||||
### Batch Mode
|
||||
|
||||
```csharp
|
||||
var series = new TSeries();
|
||||
// ... populate series ...
|
||||
var results = Bias.Batch(series, period: 20);
|
||||
```
|
||||
|
||||
### Span Mode (Zero Allocation)
|
||||
|
||||
```csharp
|
||||
double[] input = new double[1000];
|
||||
double[] output = new double[1000];
|
||||
// ... populate input ...
|
||||
Bias.Batch(input.AsSpan(), output.AsSpan(), period: 20);
|
||||
```
|
||||
|
||||
### Event-Driven Mode
|
||||
|
||||
```csharp
|
||||
var source = new TSeries();
|
||||
var bias = new Bias(source, period: 20);
|
||||
// Bias automatically updates when source publishes
|
||||
source.Add(new TValue(DateTime.UtcNow, 100.0));
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **Interpreting Raw Values**: Bias of 0.05 means 5%, not 0.05%. Display as percentage or multiply by 100 for human consumption.
|
||||
|
||||
2. **Period Selection**: Shorter periods (10-20) respond faster but generate more noise. Longer periods (50-200) are smoother but lag more.
|
||||
|
||||
3. **Asymmetric Interpretation**: A +10% Bias doesn't necessarily have the same significance as -10% Bias due to asymmetric price distributions.
|
||||
|
||||
4. **Division by Zero**: When SMA is zero (rare but possible with price data starting at zero), the indicator returns 0 as a safeguard.
|
||||
|
||||
5. **Not a Standalone Signal**: Extreme Bias suggests potential reversal but doesn't guarantee it. Prices can stay overbought/oversold longer than expected.
|
||||
|
||||
6. **Warmup Period**: The indicator needs `period` bars to reach full window. Before that, it uses a growing window for calculation.
|
||||
|
||||
## When to Use Bias
|
||||
|
||||
**Use it when:**
|
||||
|
||||
* You want a simple, interpretable overbought/oversold measure
|
||||
* Mean-reversion strategies are your focus
|
||||
* You need to compare deviation across instruments with different price scales
|
||||
* Quick assessment of price extension from average
|
||||
|
||||
**Skip it when:**
|
||||
|
||||
* You need trend-following signals (use moving average crossovers instead)
|
||||
* Strong trending markets where mean-reversion fails
|
||||
* You prefer momentum-based indicators (RSI, ROC)
|
||||
* Price data starts at or crosses zero
|
||||
|
||||
## References
|
||||
|
||||
* TradingView. "Bias Indicator (PineScript)." *TradingView Documentation*.
|
||||
* Kaufman, P.J. (2013). "Trading Systems and Methods." *Wiley Trading*, 5th Edition.
|
||||
Reference in New Issue
Block a user