mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-14 00:28:05 +00:00
feat: Add Jurik Composite Fractal Behavior (CFB) indicator and tests
This commit is contained in:
@@ -15,6 +15,7 @@ Momentum indicators measure the speed or strength of price movements. This inclu
|
||||
| BBS | Bollinger Band Squeeze | |
|
||||
| BOP | Balance of Power | |
|
||||
| CCI | Commodity Channel Index | |
|
||||
| [CFB](cfb/Cfb.md) | Jurik Composite Fractal Behavior | Trend Duration Index using fractal efficiency. |
|
||||
| CHOP | Choppiness Index | |
|
||||
| CMO | Chande Momentum Oscillator | |
|
||||
| DMX | Jurik Directional Movement Index | |
|
||||
@@ -35,7 +36,7 @@ Momentum indicators measure the speed or strength of price movements. This inclu
|
||||
| ROCP | Rate of Change Percentage | |
|
||||
| ROCR | Rate of Change Ratio | |
|
||||
| RSI | Relative Strength Index | |
|
||||
| [RSX](rsx/Rsx.md) | Relative Strength X (Jurik's RSI Variant) | Noise-free, zero-lag version of RSI |
|
||||
| [RSX](rsx/Rsx.md) | Jurik Relative Strength X | Noise-free, zero-lag version of RSI |
|
||||
| SMI | Stochastic Momentum Index | |
|
||||
| STOCH | Stochastic Oscillator | |
|
||||
| STOCHF | Stochastic Fast | |
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CfbIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void CfbIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new CfbIndicator();
|
||||
|
||||
Assert.Equal(2, indicator.MinLength);
|
||||
Assert.Equal(192, indicator.MaxLength);
|
||||
Assert.Equal(2, indicator.Step);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("CFB - Jurik Composite Fractal Behavior", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_MinHistoryDepths_EqualsMaxLength()
|
||||
{
|
||||
var indicator = new CfbIndicator { MaxLength = 50 };
|
||||
|
||||
Assert.Equal(50, indicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(50, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_ShortName_IncludesParametersAndSource()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 5, MaxLength = 20, Source = SourceType.Close };
|
||||
// Initialize to update SourceName
|
||||
indicator.Initialize();
|
||||
|
||||
Assert.Contains("CFB", indicator.ShortName);
|
||||
Assert.Contains("5-20", indicator.ShortName);
|
||||
Assert.Contains("Close", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new CfbIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Cfb.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_Initialize_CreatesInternalCfb()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 2, MaxLength = 10, Step = 2 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 2, MaxLength = 4, Step = 2 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
// Need enough bars for MaxLength (4)
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(2), 104, 110, 102, 108);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 103, 109, 101, 105);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(4), 105, 112, 103, 110);
|
||||
|
||||
// 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 CfbIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 2, MaxLength = 4, Step = 2 };
|
||||
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.HistoricalData.AddBar(now.AddMinutes(2), 104, 110, 102, 108);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 103, 109, 101, 105);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
// Add new bar
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(4), 105, 112, 103, 110);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 2, MaxLength = 4, Step = 2 };
|
||||
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.HistoricalData.AddBar(now.AddMinutes(2), 104, 110, 102, 108);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(3), 103, 109, 101, 105);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(4), 105, 112, 103, 110);
|
||||
|
||||
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 CfbIndicator_OnPaintChart_DoesNotThrow()
|
||||
{
|
||||
var indicator = new CfbIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var method = indicator.GetType().GetMethod("OnPaintChart");
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal(typeof(CfbIndicator), method.DeclaringType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_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 CfbIndicator { MinLength = 2, MaxLength = 4, Step = 2, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
// Add enough bars
|
||||
for (int i = 0; i < 5; i++)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
|
||||
}
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CfbIndicator_Parameters_CanBeChanged()
|
||||
{
|
||||
var indicator = new CfbIndicator { MinLength = 5, MaxLength = 20, Step = 5 };
|
||||
Assert.Equal(5, indicator.MinLength);
|
||||
Assert.Equal(20, indicator.MaxLength);
|
||||
Assert.Equal(5, indicator.Step);
|
||||
|
||||
indicator.MinLength = 10;
|
||||
indicator.MaxLength = 40;
|
||||
indicator.Step = 10;
|
||||
|
||||
Assert.Equal(10, indicator.MinLength);
|
||||
Assert.Equal(40, indicator.MaxLength);
|
||||
Assert.Equal(10, indicator.Step);
|
||||
Assert.Equal(40, indicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class CfbIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Min Length", sortIndex: 1, 2, 1000, 1, 0)]
|
||||
public int MinLength { get; set; } = 2;
|
||||
|
||||
[InputParameter("Max Length", sortIndex: 2, 2, 1000, 1, 0)]
|
||||
public int MaxLength { get; set; } = 192;
|
||||
|
||||
[InputParameter("Step", sortIndex: 3, 1, 100, 1, 0)]
|
||||
public int Step { get; set; } = 2;
|
||||
|
||||
[IndicatorExtensions.DataSourceInput]
|
||||
public SourceType Source { get; set; } = SourceType.Close;
|
||||
|
||||
[InputParameter("Show cold values", sortIndex: 21)]
|
||||
public bool ShowColdValues { get; set; } = true;
|
||||
|
||||
private Cfb? _cfb;
|
||||
private int _warmupBarIndex = -1;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
|
||||
public int MinHistoryDepths => MaxLength;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"CFB {MinLength}-{MaxLength}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/cfb/Cfb.Quantower.cs";
|
||||
|
||||
public CfbIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
SourceName = Source.ToString();
|
||||
Name = "CFB - Jurik Composite Fractal Behavior";
|
||||
Description = "Trend Duration Index using fractal efficiency";
|
||||
Series = new(name: "CFB", color: IndicatorExtensions.Statistics, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
// Generate lengths array
|
||||
int count = (MaxLength - MinLength) / Step + 1;
|
||||
int[] lengths = new int[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
lengths[i] = MinLength + i * Step;
|
||||
}
|
||||
|
||||
_cfb = new Cfb(lengths);
|
||||
_warmupBarIndex = -1;
|
||||
SourceName = Source.ToString();
|
||||
base.OnInit();
|
||||
}
|
||||
|
||||
protected override void OnUpdate(UpdateArgs args)
|
||||
{
|
||||
TValue input = this.GetInputValue(args, Source);
|
||||
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
|
||||
TValue result = _cfb!.Update(input, isNew);
|
||||
if (_warmupBarIndex < 0 && _cfb!.IsHot)
|
||||
_warmupBarIndex = Count;
|
||||
Series!.SetValue(result.Value);
|
||||
Series!.SetMarker(0, Color.Transparent); //OnPaintChart draws the line, hidden here
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
this.PaintSmoothCurve(args, Series!, _warmupBarIndex, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class CfbTests
|
||||
{
|
||||
[Fact]
|
||||
public void BasicCalculation_DoesNotCrash()
|
||||
{
|
||||
var cfb = new Cfb();
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var data = bars.Close;
|
||||
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
{
|
||||
cfb.Update(new TValue(data.Times[i], data.Values[i]));
|
||||
}
|
||||
|
||||
Assert.True(cfb.Last.Value >= 1.0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PerfectTrend_IncreasesCfb()
|
||||
{
|
||||
// Use small lengths for easier testing
|
||||
int[] lengths = { 4, 8, 12 };
|
||||
var cfb = new Cfb(lengths);
|
||||
|
||||
// Feed a perfect uptrend
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
cfb.Update(new TValue(DateTime.UtcNow, i));
|
||||
}
|
||||
|
||||
Assert.Equal(8.0, cfb.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FlatLine_ReturnsOne()
|
||||
{
|
||||
var cfb = new Cfb();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
cfb.Update(new TValue(DateTime.UtcNow, 100.0));
|
||||
}
|
||||
|
||||
// NetMove is 0. TotalMove is 0.
|
||||
// Ratio = 0/0 -> NaN?
|
||||
// Code handles TotalMove < 1e-12 by skipping.
|
||||
// So no lengths qualify.
|
||||
// Decay logic kicks in.
|
||||
// Should decay to 1.0.
|
||||
|
||||
Assert.Equal(1.0, cfb.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZigZag_ReturnsOne()
|
||||
{
|
||||
var cfb = new Cfb(new int[] { 4, 8 });
|
||||
// 100, 101, 100, 101...
|
||||
// NetMove(4) = Abs(100 - 100) = 0. Ratio = 0.
|
||||
// NetMove(8) = 0. Ratio = 0.
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
double price = 100 + (i % 2);
|
||||
cfb.Update(new TValue(DateTime.UtcNow, price));
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, cfb.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsNew_Consistency()
|
||||
{
|
||||
var cfb = new Cfb();
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var data = new List<TValue>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
data.Add(new TValue(bars.Close.Times[i], bars.Close.Values[i]));
|
||||
}
|
||||
|
||||
// Feed first 99
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
cfb.Update(data[i]);
|
||||
}
|
||||
|
||||
// Update with 100th point (isNew=true)
|
||||
var val1 = cfb.Update(data[99], true);
|
||||
|
||||
// Update with modified 100th point (isNew=false)
|
||||
var modified = new TValue(data[99].Time, data[99].Value + 1.0);
|
||||
var val2 = cfb.Update(modified, false);
|
||||
|
||||
// Create new instance and feed up to modified
|
||||
var cfb2 = new Cfb();
|
||||
for (int i = 0; i < 99; i++)
|
||||
{
|
||||
cfb2.Update(data[i]);
|
||||
}
|
||||
var val3 = cfb2.Update(modified, true);
|
||||
|
||||
Assert.Equal(val3.Value, val2.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticCalculate_Matches_Streaming()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
var cfb = new Cfb();
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults.Add(cfb.Update(new TValue(series.Times[i], series.Values[i])).Value);
|
||||
}
|
||||
|
||||
var staticResults = Cfb.Calculate(series);
|
||||
|
||||
Assert.Equal(streamingResults.Count, staticResults.Count);
|
||||
for (int i = 0; i < streamingResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], staticResults.Values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpanCalculate_Matches_Streaming()
|
||||
{
|
||||
var gbm = new GBM();
|
||||
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
double[] values = bars.Close.Values.ToArray();
|
||||
|
||||
var cfb = new Cfb();
|
||||
var streamingResults = new List<double>();
|
||||
for (int i = 0; i < bars.Count; i++)
|
||||
{
|
||||
streamingResults.Add(cfb.Update(new TValue(bars.Close.Times[i], bars.Close.Values[i])).Value);
|
||||
}
|
||||
|
||||
double[] spanResults = new double[bars.Count];
|
||||
Cfb.Calculate(values, spanResults);
|
||||
|
||||
for (int i = 0; i < streamingResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamingResults[i], spanResults[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class CfbValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public CfbValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Consistency_UpdateVsCalculate()
|
||||
{
|
||||
// Verify that Update(TValue) and Calculate(TSeries) produce identical results
|
||||
var cfb = new Cfb();
|
||||
var streamResult = new TSeries();
|
||||
foreach (var item in _testData.Data)
|
||||
{
|
||||
streamResult.Add(cfb.Update(item));
|
||||
}
|
||||
|
||||
var batchResult = Cfb.Calculate(_testData.Data);
|
||||
|
||||
Assert.Equal(streamResult.Count, batchResult.Count);
|
||||
Assert.NotEmpty(streamResult);
|
||||
for (int i = 0; i < streamResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(streamResult[i].Value, batchResult[i].Value, 1e-9);
|
||||
}
|
||||
_output.WriteLine("CFB Update vs Calculate validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Consistency_SeriesVsSpan()
|
||||
{
|
||||
// Verify that Calculate(TSeries) and Calculate(Span) produce identical results
|
||||
var batchResult = Cfb.Calculate(_testData.Data);
|
||||
|
||||
var spanInput = _testData.Data.Values.ToArray().AsSpan();
|
||||
var spanOutput = new double[spanInput.Length];
|
||||
Cfb.Calculate(spanInput, spanOutput);
|
||||
|
||||
for (int i = 0; i < batchResult.Count; i++)
|
||||
{
|
||||
Assert.Equal(batchResult.Values[i], spanOutput[i], 1e-9);
|
||||
}
|
||||
_output.WriteLine("CFB Series vs Span validated successfully");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Properties()
|
||||
{
|
||||
// CFB should be >= 1.0
|
||||
var result = Cfb.Calculate(_testData.Data);
|
||||
foreach (var val in result.Values)
|
||||
{
|
||||
Assert.True(val >= 1.0, $"CFB value {val} should be >= 1.0");
|
||||
}
|
||||
_output.WriteLine("CFB properties validated successfully");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// CFB: Jurik Composite Fractal Behavior (Trend Duration Index)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// CFB measures the duration of a trend by analyzing fractal efficiency across multiple time scales.
|
||||
/// It calculates a composite index based on which lookback periods show "quality" trending behavior.
|
||||
///
|
||||
/// Key characteristics:
|
||||
/// - Adaptive: Adjusts to market fractal patterns.
|
||||
/// - Granular: Uses a dense array of lookback lengths for smooth transitions.
|
||||
/// - Composite: Weighted average of qualifying trend lengths.
|
||||
/// - Zero-lag: Designed to modulate other indicators with minimal latency.
|
||||
///
|
||||
/// Calculation:
|
||||
/// 1. For each length L:
|
||||
/// Ratio = NetMove(L) / TotalVolatility(L)
|
||||
/// where NetMove = Abs(Price - Price[L ago])
|
||||
/// and TotalVolatility = Sum(Abs(Price[i] - Price[i-1])) over L bars.
|
||||
/// 2. Filter: Only consider lengths where Ratio > Threshold (0.25).
|
||||
/// 3. Composite: Weighted average of qualifying lengths (Weight = Ratio).
|
||||
/// 4. Decay: If no trend found, decay the previous CFB value.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Cfb : ITValuePublisher
|
||||
{
|
||||
private readonly int[] _lengths;
|
||||
private readonly int _maxLen;
|
||||
private readonly RingBuffer _prices;
|
||||
private readonly RingBuffer _volatility;
|
||||
private readonly double[] _runningSums;
|
||||
private readonly double[] _p_runningSums;
|
||||
|
||||
private record struct State(double PrevCfb, double LastPrice, double LastValidValue);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
public string Name { get; }
|
||||
public event Action<TValue>? Pub;
|
||||
public TValue Last { get; private set; }
|
||||
public bool IsHot => _prices.IsFull;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a CFB indicator with specified fractal lengths.
|
||||
/// </summary>
|
||||
/// <param name="lengths">Array of lookback lengths. If null, defaults to 2, 4, ..., 192.</param>
|
||||
public Cfb(int[]? lengths = null)
|
||||
{
|
||||
if (lengths == null || lengths.Length == 0)
|
||||
{
|
||||
// Default dense array: 2, 4, 6, ..., 192
|
||||
_lengths = new int[96];
|
||||
for (int i = 0; i < 96; i++)
|
||||
{
|
||||
_lengths[i] = (i + 1) * 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_lengths = (int[])lengths.Clone();
|
||||
Array.Sort(_lengths);
|
||||
}
|
||||
|
||||
_maxLen = _lengths[^1];
|
||||
|
||||
// We need maxLen + 1 capacity to handle the lookback correctly
|
||||
// _prices stores raw prices
|
||||
// _volatility stores bar-to-bar changes. _volatility[i] = Abs(Price[i] - Price[i-1])
|
||||
_prices = new RingBuffer(_maxLen + 1);
|
||||
_volatility = new RingBuffer(_maxLen + 1);
|
||||
|
||||
_runningSums = new double[_lengths.Length];
|
||||
_p_runningSums = new double[_lengths.Length];
|
||||
|
||||
Name = "Cfb";
|
||||
_state.PrevCfb = 1.0;
|
||||
}
|
||||
|
||||
public Cfb(ITValuePublisher source, int[]? lengths = null) : this(lengths)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
double price = input.Value;
|
||||
if (!double.IsFinite(price))
|
||||
{
|
||||
price = _state.LastValidValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastValidValue = price;
|
||||
}
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// Save state
|
||||
_p_state = _state;
|
||||
Array.Copy(_runningSums, _p_runningSums, _lengths.Length);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Restore state
|
||||
_state = _p_state;
|
||||
Array.Copy(_p_runningSums, _runningSums, _lengths.Length);
|
||||
}
|
||||
|
||||
// Calculate volatility for this step
|
||||
double vol = 0.0;
|
||||
if (_prices.Count > 0)
|
||||
{
|
||||
vol = Math.Abs(price - _state.LastPrice);
|
||||
}
|
||||
|
||||
// Update buffers
|
||||
if (isNew)
|
||||
{
|
||||
_prices.Add(price);
|
||||
_volatility.Add(vol);
|
||||
}
|
||||
else
|
||||
{
|
||||
_prices.UpdateNewest(price);
|
||||
_volatility.UpdateNewest(vol);
|
||||
}
|
||||
_state.LastPrice = price;
|
||||
|
||||
double sumWeightedLen = 0.0;
|
||||
double sumWeights = 0.0;
|
||||
int count = _prices.Count;
|
||||
|
||||
// Update running sums and calculate ratios
|
||||
for (int i = 0; i < _lengths.Length; i++)
|
||||
{
|
||||
int L = _lengths[i];
|
||||
|
||||
// Update running sum of volatility
|
||||
// We always add the new volatility
|
||||
// We only subtract if we have enough history
|
||||
|
||||
double volToRemove = 0.0;
|
||||
if (count > L)
|
||||
{
|
||||
volToRemove = _volatility[count - 1 - L];
|
||||
}
|
||||
|
||||
_runningSums[i] += vol - volToRemove;
|
||||
|
||||
if (count <= L) continue;
|
||||
|
||||
// Safety check for very small volatility
|
||||
if (_runningSums[i] < 1e-12) continue;
|
||||
|
||||
// Net move over L bars
|
||||
// Price at Count-1 is current. Price at Count-1-L is L bars ago.
|
||||
double netMove = Math.Abs(price - _prices[count - 1 - L]);
|
||||
|
||||
double ratio = netMove / _runningSums[i];
|
||||
|
||||
|
||||
if (ratio >= 0.25)
|
||||
{
|
||||
sumWeightedLen += L * ratio;
|
||||
sumWeights += ratio;
|
||||
}
|
||||
}
|
||||
|
||||
double cfb;
|
||||
if (sumWeights > 0.25)
|
||||
{
|
||||
cfb = sumWeightedLen / sumWeights;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Decay
|
||||
cfb = (_state.PrevCfb > 1.0) ? _state.PrevCfb * 0.5 : 1.0;
|
||||
}
|
||||
|
||||
if (cfb < 1.0) cfb = 1.0;
|
||||
|
||||
// Round to nearest integer
|
||||
cfb = Math.Round(cfb);
|
||||
if (cfb < 1.0) cfb = 1.0;
|
||||
|
||||
_state.PrevCfb = cfb;
|
||||
|
||||
Last = new TValue(input.Time, cfb);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public 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);
|
||||
|
||||
Calculate(source.Values, vSpan, _lengths);
|
||||
source.Times.CopyTo(tSpan);
|
||||
|
||||
// Restore state logic would go here if needed for continuity,
|
||||
// but for batch processing we usually just return the result.
|
||||
// To properly support "Update(TValue)" after "Update(TSeries)", we would need to
|
||||
// replay the last MaxLen bars to populate the buffers.
|
||||
|
||||
// Replay last MaxLen bars to restore state
|
||||
int replayStart = Math.Max(0, len - _maxLen - 1);
|
||||
_prices.Clear();
|
||||
_volatility.Clear();
|
||||
Array.Clear(_runningSums);
|
||||
_state = default;
|
||||
_state.PrevCfb = 1.0;
|
||||
|
||||
// We need to re-run the update logic for the replay window to populate running sums correctly
|
||||
// This is expensive but necessary for correct state restoration.
|
||||
// For the purpose of this implementation, we will just ensure the buffers are populated.
|
||||
|
||||
for (int i = replayStart; i < len; i++)
|
||||
{
|
||||
Update(new TValue(source.Times[i], source.Values[i]), true);
|
||||
}
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int[]? lengths = null)
|
||||
{
|
||||
var cfb = new Cfb(lengths);
|
||||
return cfb.Update(source);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int[]? lengths = null)
|
||||
{
|
||||
if (source.Length == 0) return;
|
||||
|
||||
// Setup lengths
|
||||
int[] lens;
|
||||
if (lengths == null || lengths.Length == 0)
|
||||
{
|
||||
lens = new int[96];
|
||||
for (int i = 0; i < 96; i++) lens[i] = (i + 1) * 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
lens = lengths;
|
||||
}
|
||||
int maxLen = 0;
|
||||
for(int i=0; i<lens.Length; i++) if(lens[i] > maxLen) maxLen = lens[i];
|
||||
|
||||
// Pre-calculate volatility for the whole series
|
||||
// vol[i] = Abs(source[i] - source[i-1])
|
||||
// We can use a temporary array for this.
|
||||
int len = source.Length;
|
||||
double[] volArray = new double[len];
|
||||
volArray[0] = 0;
|
||||
for (int i = 1; i < len; i++)
|
||||
{
|
||||
volArray[i] = Math.Abs(source[i] - source[i-1]);
|
||||
}
|
||||
|
||||
// We need running sums for each length.
|
||||
// Since we are processing sequentially, we can maintain the running sums just like in Update.
|
||||
double[] runningSums = new double[lens.Length];
|
||||
double prevCfb = 1.0;
|
||||
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
double price = source[i];
|
||||
double currentVol = volArray[i];
|
||||
|
||||
double sumWeightedLen = 0.0;
|
||||
double sumWeights = 0.0;
|
||||
|
||||
// For very first bars where i < minLen, result is 1
|
||||
if (i < lens[0])
|
||||
{
|
||||
output[i] = 1.0;
|
||||
// Still need to update running sums if possible, but we can't really until we have enough data
|
||||
// Actually we can accumulate volatility.
|
||||
for (int k = 0; k < lens.Length; k++)
|
||||
{
|
||||
runningSums[k] += currentVol;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int k = 0; k < lens.Length; k++)
|
||||
{
|
||||
int L = lens[k];
|
||||
|
||||
// Update running sum
|
||||
runningSums[k] += currentVol;
|
||||
if (i > L)
|
||||
{
|
||||
runningSums[k] -= volArray[i - L];
|
||||
}
|
||||
|
||||
if (i < L) continue;
|
||||
|
||||
double totalMove = runningSums[k];
|
||||
if (totalMove < 1e-12) continue;
|
||||
|
||||
double netMove = Math.Abs(price - source[i - L]);
|
||||
double ratio = netMove / totalMove;
|
||||
|
||||
if (ratio >= 0.25)
|
||||
{
|
||||
sumWeightedLen += L * ratio;
|
||||
sumWeights += ratio;
|
||||
}
|
||||
}
|
||||
|
||||
double cfb;
|
||||
if (sumWeights > 0.25)
|
||||
{
|
||||
cfb = sumWeightedLen / sumWeights;
|
||||
}
|
||||
else
|
||||
{
|
||||
cfb = (prevCfb > 1.0) ? prevCfb * 0.5 : 1.0;
|
||||
}
|
||||
|
||||
if (cfb < 1.0) cfb = 1.0;
|
||||
cfb = Math.Round(cfb);
|
||||
if (cfb < 1.0) cfb = 1.0;
|
||||
|
||||
output[i] = cfb;
|
||||
prevCfb = cfb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
# CFB - Jurik Composite Fractal Behavior
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
Composite Fractal Behavior (CFB) is a sophisticated trend duration index developed by Jurik Research. It measures the "fractal efficiency" of price movements across multiple time scales to determine the quality and duration of a trend. Unlike traditional trend indicators that look at a single period, CFB analyzes a spectrum of lookback periods to create a composite index.
|
||||
|
||||
CFB is designed to answer the question: "How long has the market been trending efficiently?" It is particularly useful for:
|
||||
|
||||
* Adjusting the period of other indicators (adaptive indicators).
|
||||
* Filtering out choppy markets.
|
||||
* Identifying the breakdown of long-term trends.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Fractal Efficiency:** Measures how "straight" the price movement is. A straight line has high efficiency; a choppy path has low efficiency.
|
||||
* **Composite Index:** Instead of relying on a single lookback length, CFB evaluates a wide range of lengths (e.g., 4 to 192 bars) and combines them based on their efficiency.
|
||||
* **Adaptive:** The indicator adapts to the market's current fractal structure, giving more weight to timeframes where trending behavior is evident.
|
||||
* **Trend Duration:** The output value represents the approximate duration (in bars) of the current trend.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function |
|
||||
|-----------|---------|----------|
|
||||
| Lengths | `[2, 4, ..., 192]` | Array of lookback periods to analyze. Default is a dense array from 2 to 192. |
|
||||
| Source | Close | Price data used for calculation. |
|
||||
|
||||
**Pro Tip:** CFB values typically range from 0 to the maximum lookback length. A rising CFB indicates a strengthening trend (either up or down), while a falling CFB suggests the trend is breaking down or the market is entering a consolidation phase.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
The CFB calculation involves several steps for each lookback length $L$ in the provided set:
|
||||
|
||||
1. **Calculate Efficiency Ratio:**
|
||||
For each length $L$, calculate the ratio of the net price movement to the total volatility (path length) over that period.
|
||||
$$Ratio_L = \frac{|Price_t - Price_{t-L}|}{\sum_{i=0}^{L-1} |Price_{t-i} - Price_{t-i-1}|}$$
|
||||
|
||||
2. **Filter:**
|
||||
Only consider lengths where the efficiency ratio exceeds a threshold (typically 0.25). This filters out noise and weak trends.
|
||||
|
||||
3. **Weighted Average:**
|
||||
Calculate the weighted average of the qualifying lengths, using the efficiency ratio as the weight.
|
||||
$$CFB = \frac{\sum (L \cdot Ratio_L)}{\sum Ratio_L}$$
|
||||
where the summation is over all $L$ such that $Ratio_L > 0.25$.
|
||||
|
||||
4. **Decay:**
|
||||
If no lengths qualify (i.e., the market is very choppy), the CFB value decays towards 1.0.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides a high-performance implementation that uses `RingBuffer` for O(1) updates of the volatility sums.
|
||||
|
||||
### Single CFB (`Cfb`)
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with default lengths
|
||||
var cfb = new Cfb();
|
||||
|
||||
// Or specify custom lengths
|
||||
var cfbCustom = new Cfb(new int[] { 10, 20, 30, 40, 50 });
|
||||
|
||||
// Streaming update
|
||||
TValue result = cfb.Update(new TValue(time, price));
|
||||
Console.WriteLine($"Current Trend Duration: {result.Value}");
|
||||
```
|
||||
|
||||
### Zero-Allocation Span API
|
||||
|
||||
For performance-critical scenarios:
|
||||
|
||||
```csharp
|
||||
double[] prices = ...;
|
||||
double[] output = new double[prices.Length];
|
||||
|
||||
// Calculate using default lengths
|
||||
Cfb.Calculate(prices.AsSpan(), output.AsSpan());
|
||||
```
|
||||
|
||||
### Bar Correction (isNew Parameter)
|
||||
|
||||
`Cfb` supports intra-bar updates:
|
||||
|
||||
```csharp
|
||||
// Real-time: receive initial tick for new bar
|
||||
cfb.Update(new TValue(time, 100.5), isNew: true);
|
||||
|
||||
// Real-time: price updates within same bar
|
||||
cfb.Update(new TValue(time, 101.0), isNew: false);
|
||||
```
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
* **High Values:** Indicate a strong, persistent trend. The value roughly corresponds to the number of bars the trend has been in effect.
|
||||
* **Low Values:** Indicate a choppy, non-trending market.
|
||||
* **Rising CFB:** The trend is gaining strength or duration.
|
||||
* **Falling CFB:** The trend is losing consistency or ending.
|
||||
|
||||
CFB is often used as an input to other adaptive indicators (e.g., JMA) to dynamically adjust their smoothing period based on market conditions.
|
||||
|
||||
## References
|
||||
|
||||
* Jurik Research: [CFB - Composite Fractal Behavior](http://jurikres.com/catalog1/ms_cfb.htm)
|
||||
@@ -14,7 +14,7 @@ public class RsxIndicatorTests
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("RSX - Relative Strength X", indicator.Name);
|
||||
Assert.Equal("RSX - Jurik Relative Strength Index", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ public class RsxIndicator : Indicator, IWatchlistIndicator
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
SourceName = Source.ToString();
|
||||
Name = "RSX - Relative Strength X";
|
||||
Description = "Jurik's RSX: A noise-free, zero-lag version of RSI";
|
||||
Name = "RSX - Jurik Relative Strength Index";
|
||||
Description = "Jurik's RSI: A noise-free, zero-lag version of RSI";
|
||||
Series = new(name: $"RSX {Period}", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// RSX: Relative Strength X (Jurik's RSI Variant)
|
||||
/// RSX: Jurik Relative Strength Index (Jurik's RSI Variant)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// RSX is a noise-free version of RSI that eliminates lag and choppiness.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# RSX - Relative Strength X (Jurik's RSI Variant)
|
||||
# RSX - Jurik Relative Strength X
|
||||
|
||||
RSX is a noise-free version of the Relative Strength Index (RSI) developed by Mark Jurik. It eliminates the lag and choppiness associated with standard RSI and its smoothed variants. RSX preserves the 0-100 bounded range and turning points of RSI but provides a much smoother signal, making it easier to identify trends and reversals without false signals from whipsaw movements.
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public class VelIndicatorTests
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("VEL - Jurik's Velocity", indicator.Name);
|
||||
Assert.Equal("VEL - Jurik Velocity", indicator.Name);
|
||||
Assert.True(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class VelIndicator : Indicator, IWatchlistIndicator
|
||||
OnBackGround = true;
|
||||
SeparateWindow = true;
|
||||
SourceName = Source.ToString();
|
||||
Name = "VEL - Jurik's Velocity";
|
||||
Name = "VEL - Jurik Velocity";
|
||||
Description = "Momentum oscillator calculated as PWMA - WMA";
|
||||
Series = new(name: $"VEL {Period}", color: IndicatorExtensions.Momentum, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
|
||||
@@ -5,7 +5,7 @@ using System.Runtime.InteropServices;
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// VEL: Jurik's Velocity
|
||||
/// VEL: Jurik Velocity
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// VEL is a momentum oscillator calculated as the difference between a Parabolic Weighted Moving Average (PWMA)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# VEL - Jurik's Velocity
|
||||
# VEL - Jurik Velocity
|
||||
|
||||
VEL (Jurik's Velocity) is a momentum oscillator that measures the rate of change of price. It is calculated as the difference between a Parabolic Weighted Moving Average (PWMA) and a Weighted Moving Average (WMA) of the same period.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
| HT_TRENDMODE | Ehlers Hilbert Transform Trend Mode | |
|
||||
| HWMA | Holt Weighted MA | |
|
||||
| ICHIMOKU | Ichimoku Cloud | |
|
||||
| [JMA](jma/Jma.md) | Jurik MA | Adaptive moving average that adjusts to market volatility for superior smoothing with minimal lag. |
|
||||
| [JMA](jma/Jma.md) | Jurik Moving Average | Adaptive moving average that adjusts to market volatility for superior smoothing with minimal lag. |
|
||||
| [KAMA](kama/Kama.md) | Kaufman Adaptive MA | Adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio. |
|
||||
| KF | Kalman Filter | |
|
||||
| LOESS | LOESS/LOWESS Smoothing | |
|
||||
|
||||
Reference in New Issue
Block a user