SIMD Refactor: Merge simd-dev into dev (#55)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: aider (openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
Co-authored-by: Warp <agent@warp.dev>
This commit is contained in:
Miha Kralj
2026-01-18 19:02:03 -08:00
committed by GitHub
co-authored by Claude Opus 4.5 aider Warp
parent 5bcdf8d614
commit 86fe32a682
1750 changed files with 198235 additions and 80539 deletions
-70
View File
@@ -1,70 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// AC: Acceleration/Deceleration Oscillator
/// A momentum indicator that measures the acceleration and deceleration of the current driving force.
/// It is derived from the Awesome Oscillator (AO) and helps identify potential trend reversals.
/// </summary>
/// <remarks>
/// The AC calculation process:
/// 1. Calculate the Awesome Oscillator (AO)
/// 2. Calculate a 5-period simple moving average of the AO
/// 3. Subtract the 5-period SMA from the current AO value
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Measures the acceleration/deceleration of market driving force
/// - Positive values indicate increasing momentum
/// - Negative values indicate decreasing momentum
/// - Can be used to identify potential trend reversals
///
/// Formula:
/// AC = AO - SMA(AO, 5)
///
/// Sources:
/// Bill Williams - "Trading Chaos" (1995)
/// https://www.investopedia.com/terms/a/ac.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ac : AbstractBase
{
private readonly Ao _ao;
private readonly Sma _sma5;
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ac(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ac()
{
_ao = new Ao();
_sma5 = new Sma(5);
WarmupPeriod = 39; // AO requires 34 periods + 5 for AC's SMA
Name = "AC";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
var ao = _ao.Calc(BarInput, BarInput.IsNew);
_sma5.Calc(ao, BarInput.IsNew);
return ao - _sma5.Value;
}
}
-70
View File
@@ -1,70 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// AO: Awesome Oscillator
/// A momentum indicator that reflects the precise changes in the market driving force.
/// It is used to affirm trends or to anticipate possible reversals.
/// </summary>
/// <remarks>
/// The AO calculation process:
/// 1. Calculates the 5-period simple moving average of the HL2 (High+Low)/2 values.
/// 2. Calculates the 34-period simple moving average of the HL2 (High+Low)/2 values.
/// 3. Subtracts the 34-period SMA from the 5-period SMA.
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Positive values indicate bullish momentum
/// - Negative values indicate bearish momentum
/// - Crosses above zero suggest buying opportunities
/// - Crosses below zero suggest selling opportunities
///
/// Formula:
/// AO = SMA(HL2, 5) - SMA(HL2, 34)
///
/// Sources:
/// Bill Williams - "Trading Chaos" (1995)
/// https://www.investopedia.com/terms/a/awesomeoscillator.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ao : AbstractBase
{
private readonly Sma _sma5;
private readonly Sma _sma34;
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ao(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Ao()
{
_sma5 = new Sma(5);
_sma34 = new Sma(34);
WarmupPeriod = 34;
Name = "AO";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
_sma5.Calc(BarInput.HL2, BarInput.IsNew);
_sma34.Calc(BarInput.HL2, BarInput.IsNew);
return _sma5.Value - _sma34.Value;
}
}
-119
View File
@@ -1,119 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// AROON: Aroon Oscillator
/// A trend-following indicator that measures the strength of a trend and the likelihood
/// that the trend will continue. It consists of two lines (Aroon Up and Aroon Down) and
/// their difference forms the Aroon Oscillator.
/// </summary>
/// <remarks>
/// The Aroon calculation process:
/// 1. Tracks the number of periods since the last highest high (Aroon Up)
/// 2. Tracks the number of periods since the last lowest low (Aroon Down)
/// 3. Normalizes both values to a 0-100 scale
/// 4. Calculates the difference (Aroon Oscillator)
///
/// Key characteristics:
/// - Oscillates between -100 and +100
/// - Positive values indicate uptrend
/// - Negative values indicate downtrend
/// - Zero line crossovers signal trend changes
/// - Extreme readings suggest strong trends
///
/// Formula:
/// Aroon Up = ((period - days since highest high) / period) × 100
/// Aroon Down = ((period - days since lowest low) / period) × 100
/// Aroon Oscillator = Aroon Up - Aroon Down
///
/// Sources:
/// Tushar Chande - "The New Technical Trader" (1994)
/// https://www.investopedia.com/terms/a/aroonoscillator.asp
///
/// Note: Default period of 25 was recommended by Chande
/// </remarks>
[SkipLocalsInit]
public sealed class Aroon : AbstractBarBase
{
private readonly CircularBuffer _highPrices;
private readonly CircularBuffer _lowPrices;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod = 25;
/// <param name="period">The number of periods used in the Aroon calculation (default 25).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Aroon(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_highPrices = new(period);
_lowPrices = new(period);
_index = 0;
WarmupPeriod = period;
Name = $"AROON({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the Aroon calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Aroon(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_highPrices.Add(Input.High);
_lowPrices.Add(Input.Low);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateAroonLine(int period, int daysSince)
{
return ((period - daysSince) * ScalingFactor) / period;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index < WarmupPeriod)
return double.NaN;
// Find highest high and lowest low positions
int highestIndex = 0;
int lowestIndex = 0;
double highestHigh = _highPrices[0];
double lowestLow = _lowPrices[0];
for (int i = 1; i < _highPrices.Count; i++)
{
if (_highPrices[i] > highestHigh)
{
highestHigh = _highPrices[i];
highestIndex = i;
}
if (_lowPrices[i] < lowestLow)
{
lowestLow = _lowPrices[i];
lowestIndex = i;
}
}
// Calculate Aroon Up and Down
double aroonUp = CalculateAroonLine(_highPrices.Count, highestIndex);
double aroonDown = CalculateAroonLine(_lowPrices.Count, lowestIndex);
// Return Aroon Oscillator
return aroonUp - aroonDown;
}
}
-65
View File
@@ -1,65 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// BOP: Balance of Power
/// A momentum oscillator that measures the strength of buying and selling pressure by comparing
/// closing prices to their corresponding opening prices.
/// </summary>
/// <remarks>
/// The BOP calculation process:
/// 1. Calculate (Close - Open) / (High - Low) for each period
/// 2. A positive BOP indicates buying pressure (bullish)
/// 3. A negative BOP indicates selling pressure (bearish)
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - No upper or lower bounds
/// - Zero line acts as equilibrium between buying and selling pressure
/// - Can be used to identify potential trend reversals and divergences
///
/// Formula:
/// BOP = (Close - Open) / (High - Low)
///
/// Sources:
/// Igor Livshin (1990s)
/// https://www.investopedia.com/terms/b/bop.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Bop : AbstractBase
{
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Bop(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Bop()
{
WarmupPeriod = 1;
Name = "BOP";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
var range = BarInput.High - BarInput.Low;
if (range <= double.Epsilon) return 0;
return (BarInput.Close - BarInput.Open) / range;
}
}
-100
View File
@@ -1,100 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CCI: Commodity Channel Index
/// A momentum oscillator used to identify cyclical trends and measure the deviation of price
/// from its statistical mean.
/// </summary>
/// <remarks>
/// The CCI calculation process:
/// 1. Calculate Typical Price (TP) = (High + Low + Close) / 3
/// 2. Calculate Simple Moving Average of TP
/// 3. Calculate Mean Deviation
/// 4. CCI = (TP - SMA(TP)) / (0.015 * Mean Deviation)
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Typically ranges between +100 and -100
/// - Values above +100 indicate overbought conditions
/// - Values below -100 indicate oversold conditions
/// - Can identify trend strength and reversals
///
/// Formula:
/// CCI = (TypicalPrice - SMA(TypicalPrice, period)) / (0.015 * MeanDeviation)
/// where:
/// - TypicalPrice = (High + Low + Close) / 3
/// - MeanDeviation = Mean(|TP - SMA(TP)|)
///
/// Sources:
/// Donald Lambert (1980)
/// https://www.investopedia.com/terms/c/commoditychannelindex.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Cci : AbstractBase
{
private readonly int _period;
private readonly Sma _sma;
private readonly double[] _typicalPrices;
private readonly double _constant = 0.015;
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The calculation period (default: 20)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cci(object source, int period = 20) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cci(int period = 20)
{
_period = period;
_sma = new Sma(period);
_typicalPrices = new double[period];
WarmupPeriod = period;
Name = "CCI";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateMeanDeviation(double typicalPrice, double smaValue)
{
var sum = 0.0;
var count = System.Math.Min(_period, _index + 1);
for (var i = 0; i < count; i++)
{
sum += System.Math.Abs(_typicalPrices[i] - smaValue);
}
return sum / count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
var typicalPrice = (BarInput.High + BarInput.Low + BarInput.Close) / 3.0;
var idx = _index % _period;
_typicalPrices[idx] = typicalPrice;
var smaValue = _sma.Calc(typicalPrice, BarInput.IsNew);
if (_index < _period - 1) return double.NaN;
var meanDeviation = CalculateMeanDeviation(typicalPrice, smaValue);
if (meanDeviation <= double.Epsilon) return 0;
return (typicalPrice - smaValue) / (_constant * meanDeviation);
}
}
-116
View File
@@ -1,116 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CFO: Chande Forecast Oscillator
/// A momentum oscillator that measures the percentage difference between the actual price
/// and its linear regression forecast value.
/// </summary>
/// <remarks>
/// The CFO calculation process:
/// 1. Calculate linear regression forecast value for the current period
/// 2. Calculate percentage difference between actual price and forecast
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Measures deviation of price from its forecasted value
/// - Positive values indicate price is above forecast (bullish)
/// - Negative values indicate price is below forecast (bearish)
/// - Can identify potential trend reversals and price divergences
///
/// Formula:
/// CFO = ((Price - Forecast) / Price) * 100
/// where:
/// - Price is typically the closing price
/// - Forecast is the linear regression forecast value
///
/// Sources:
/// Tushar Chande (1990s)
/// Technical Analysis of Stocks and Commodities magazine
/// </remarks>
[SkipLocalsInit]
public sealed class Cfo : AbstractBase
{
private readonly int _period;
private readonly double[] _prices;
private double _sumX;
private double _sumY;
private double _sumXY;
private double _sumX2;
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The calculation period (default: 14)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cfo(object source, int period = 14) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cfo(int period = 14)
{
_period = period;
_prices = new double[period];
WarmupPeriod = period;
Name = "CFO";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void UpdateSums(double oldPrice, double newPrice, int oldX, int newX)
{
_sumY -= oldPrice;
_sumY += newPrice;
_sumXY -= oldPrice * oldX;
_sumXY += newPrice * newX;
_sumX -= oldX;
_sumX += newX;
_sumX2 -= oldX * oldX;
_sumX2 += newX * newX;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateForecast()
{
var count = System.Math.Min(_period, _index + 1);
var n = (double)count;
// Calculate linear regression coefficients
var slope = ((n * _sumXY) - (_sumX * _sumY)) / ((n * _sumX2) - (_sumX * _sumX));
var intercept = (_sumY - (slope * _sumX)) / n;
// Calculate forecast for next period
return intercept + (slope * count);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(Input.IsNew);
var price = Input.Value;
var idx = _index % _period;
var oldPrice = _prices[idx];
_prices[idx] = price;
var oldX = idx + 1;
var newX = _index < _period ? idx + 1 : _period;
UpdateSums(oldPrice, price, oldX, newX);
if (_index < _period - 1) return double.NaN;
var forecast = CalculateForecast();
if (price <= double.Epsilon) return 0;
return ((price - forecast) / price) * 100;
}
}
-110
View File
@@ -1,110 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CHOP: Choppiness Index
/// A technical indicator that measures the market's trendiness versus choppiness.
/// It helps determine if the market is trending or moving sideways by comparing
/// the total movement to the net directional movement over a period.
/// </summary>
/// <remarks>
/// The CHOP calculation process:
/// 1. Calculate ATR sum over period
/// 2. Calculate total price range over period
/// 3. Scale result to oscillate between 0 and 100
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - Values above 61.8 indicate choppy market
/// - Values below 38.2 indicate trending market
/// - Based on ATR and price range
/// - Higher values = more choppy/sideways
/// - Lower values = more trending
///
/// Formula:
/// CHOP = 100 * LOG10(SUM(ATR,n)/(HIGH(n)-LOW(n))) / LOG10(n)
/// where:
/// n = period
/// ATR = Average True Range
/// HIGH(n) = Highest high over period n
/// LOW(n) = Lowest low over period n
///
/// Sources:
/// E.W. Dreiss
/// https://www.tradingview.com/support/solutions/43000501980-choppiness-index/
///
/// Note: Default period is 14
/// </remarks>
[SkipLocalsInit]
public sealed class Chop : AbstractBase
{
private readonly Atr _atr;
private readonly CircularBuffer _highs;
private readonly CircularBuffer _lows;
private readonly CircularBuffer _atrValues;
private readonly double _logPeriod;
private const int DefaultPeriod = 14;
private const double ScalingFactor = 100.0;
/// <param name="period">The number of periods used in the CHOP calculation (default 14).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Chop(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_atr = new(period);
_highs = new(period);
_lows = new(period);
_atrValues = new(period);
_logPeriod = Math.Log10(period);
WarmupPeriod = period;
Name = $"CHOP({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the CHOP calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Chop(object source, int period = DefaultPeriod) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_index++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
// Calculate ATR and store it
double atr = _atr.Calc(BarInput);
_atrValues.Add(atr, BarInput.IsNew);
// Store high and low prices
_highs.Add(BarInput.High, BarInput.IsNew);
_lows.Add(BarInput.Low, BarInput.IsNew);
// Calculate highest high and lowest low over period
double highestHigh = _highs.Max();
double lowestLow = _lows.Min();
double range = highestHigh - lowestLow;
// Calculate sum of ATR values
double atrSum = _atrValues.Sum();
// Avoid division by zero
if (range < double.Epsilon || _logPeriod < double.Epsilon)
return 0.0;
// Calculate CHOP
return ScalingFactor * Math.Log10(atrSum / range) / _logPeriod;
}
}
-117
View File
@@ -1,117 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CMO: Chande Momentum Oscillator
/// A technical momentum indicator that measures the difference between upward and
/// downward momentum. CMO helps identify overbought and oversold conditions, as
/// well as trend strength and potential reversals.
/// </summary>
/// <remarks>
/// The CMO calculation process:
/// 1. Calculates price differences from previous period
/// 2. Separates positive (upward) and negative (downward) movements
/// 3. Sums upward and downward movements over period
/// 4. Calculates: 100 * ((sumUp - sumDown) / (sumUp + sumDown))
///
/// Key characteristics:
/// - Oscillates between -100 and +100
/// - Values above +50 indicate overbought
/// - Values below -50 indicate oversold
/// - Zero line crossovers signal trend changes
/// - High absolute values suggest strong trends
///
/// Formula:
/// CMO = 100 * ((ΣUp - ΣDown) / (ΣUp + ΣDown))
/// where:
/// Up = positive price changes
/// Down = absolute negative price changes
///
/// Sources:
/// Tushar Chande - "The New Technical Trader" (1994)
/// https://www.investopedia.com/terms/c/chandemomentumoscillator.asp
///
/// Note: Similar to RSI but with different scaling and calculation method
/// </remarks>
[SkipLocalsInit]
public sealed class Cmo : AbstractBase
{
private readonly CircularBuffer _sumH;
private readonly CircularBuffer _sumL;
private double _prevValue, _p_prevValue;
private const double Epsilon = 1e-10;
private const double ScalingFactor = 100.0;
/// <param name="period">The number of periods used in the CMO calculation.</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmo(int period)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_sumH = new(period);
_sumL = new(period);
WarmupPeriod = period + 1;
Name = $"CMO({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the CMO calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cmo(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevValue = _prevValue;
}
else
{
_prevValue = _p_prevValue;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static (double up, double down) CalculateMovements(double diff)
{
return diff > 0 ? (diff, 0) : (0, -diff);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateCmo(double sumH, double sumL)
{
double divisor = sumH + sumL;
return (Math.Abs(divisor) > Epsilon) ? ScalingFactor * ((sumH - sumL) / divisor) : 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index == 0)
{
_prevValue = Input.Value;
}
// Calculate price difference
double diff = Input.Value - _prevValue;
_prevValue = Input.Value;
// Separate upward and downward movements
var (up, down) = CalculateMovements(diff);
_sumH.Add(up, Input.IsNew);
_sumL.Add(down, Input.IsNew);
// Calculate sums and CMO value
return CalculateCmo(_sumH.Sum(), _sumL.Sum());
}
}
-100
View File
@@ -1,100 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// COG: Ehler's Center of Gravity Oscillator
/// A momentum oscillator that uses the concept of center of gravity from physics
/// to measure price momentum. It calculates a weighted sum where more recent
/// prices have higher weights.
/// </summary>
/// <remarks>
/// The COG calculation process:
/// 1. Calculate weighted sum of prices (numerator)
/// 2. Calculate sum of weights (denominator)
/// 3. Divide to get center of gravity
/// 4. Invert and normalize result
///
/// Key characteristics:
/// - Oscillates around zero
/// - Leading indicator (less lag than traditional momentum)
/// - Positive values indicate upward momentum
/// - Negative values indicate downward momentum
/// - Zero line crossovers signal trend changes
///
/// Formula:
/// COG = -((Σ(Price(i) * i)) / (Σ(Price(i))) - (period + 1)/2)
/// where:
/// i = position in period (1 to period)
/// Price(i) = price at position i
///
/// Sources:
/// John F. Ehlers - "Cybernetic Analysis for Stocks and Futures"
/// https://www.mesasoftware.com/papers/CenterOfGravity.pdf
///
/// Note: Default period is 10
/// </remarks>
[SkipLocalsInit]
public sealed class Cog : AbstractBase
{
private readonly CircularBuffer _prices;
private readonly int _period;
private const int DefaultPeriod = 10;
/// <param name="period">The number of periods used in the COG calculation (default 10).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cog(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_period = period;
_prices = new(period);
WarmupPeriod = period;
Name = $"COG({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the COG calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cog(object source, int period = DefaultPeriod) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
_index++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Add new price to buffer
_prices.Add(Input.Value, Input.IsNew);
double numerator = 0.0;
double denominator = 0.0;
// Calculate weighted sums
for (int i = 0; i < _prices.Count; i++)
{
double price = _prices[i];
double weight = i + 1;
numerator += price * weight;
denominator += price;
}
// Avoid division by zero
if (Math.Abs(denominator) < double.Epsilon)
return 0.0;
// Calculate center of gravity and normalize
return -((numerator / denominator) - ((_period + 1.0) / 2.0));
}
}
-115
View File
@@ -1,115 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// COPPOCK: Coppock Curve
/// A long-term momentum oscillator used to identify major bottoms in the market.
/// It is calculated using a weighted moving average of two different Rate of Change calculations.
/// </summary>
/// <remarks>
/// The Coppock Curve calculation process:
/// 1. Calculate 14-period Rate of Change (ROC)
/// 2. Calculate 11-period Rate of Change (ROC)
/// 3. Sum the two ROC values
/// 4. Apply 10-period Weighted Moving Average (WMA) to the sum
///
/// Key characteristics:
/// - Long-term momentum indicator
/// - Primarily used for monthly data
/// - Buy signals when curve turns up from below zero
/// - Rarely used for sell signals
/// - Designed to identify major bottoms in stock market indices
///
/// Formula:
/// COPPOCK = WMA(10) of (ROC(14) + ROC(11))
/// where:
/// ROC(n) = ((Price - Price[n]) / Price[n]) * 100
/// WMA is weighted moving average
///
/// Sources:
/// Edwin Coppock - Barron's Magazine (October 1962)
/// https://www.investopedia.com/terms/c/coppockcurve.asp
///
/// Note: Originally designed for monthly data with parameters (14,11,10),
/// but can be adapted for other timeframes
/// </remarks>
[SkipLocalsInit]
public sealed class Coppock : AbstractBase
{
private readonly CircularBuffer _values;
private readonly Wma _wma;
private readonly int _roc1Period;
private readonly int _roc2Period;
private const int DefaultRoc1Period = 14;
private const int DefaultRoc2Period = 11;
private const int DefaultWmaPeriod = 10;
/// <param name="roc1Period">The first ROC period (default 14).</param>
/// <param name="roc2Period">The second ROC period (default 11).</param>
/// <param name="wmaPeriod">The WMA smoothing period (default 10).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Coppock(int roc1Period = DefaultRoc1Period, int roc2Period = DefaultRoc2Period, int wmaPeriod = DefaultWmaPeriod)
{
ArgumentOutOfRangeException.ThrowIfLessThan(roc1Period, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(roc2Period, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(wmaPeriod, 1);
_roc1Period = roc1Period;
_roc2Period = roc2Period;
int maxPeriod = Math.Max(roc1Period, roc2Period);
_values = new(maxPeriod + 1);
_wma = new(wmaPeriod);
WarmupPeriod = maxPeriod + wmaPeriod;
Name = $"COPPOCK({roc1Period},{roc2Period},{wmaPeriod})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="roc1Period">The first ROC period.</param>
/// <param name="roc2Period">The second ROC period.</param>
/// <param name="wmaPeriod">The WMA smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Coppock(object source, int roc1Period = DefaultRoc1Period, int roc2Period = DefaultRoc2Period, int wmaPeriod = DefaultWmaPeriod)
: this(roc1Period, roc2Period, wmaPeriod)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_values.Add(Input.Value);
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double CalculateRoc(int period)
{
if (_index <= period) return 0;
double currentValue = _values[0];
double oldValue = _values[period];
return ((currentValue - oldValue) / oldValue) * 100.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Calculate ROC values and their sum
double roc1 = CalculateRoc(_roc1Period);
double roc2 = CalculateRoc(_roc2Period);
double rocSum = roc1 + roc2;
// Not enough data for WMA calculation
if (_index <= Math.Max(_roc1Period, _roc2Period))
return 0;
// Calculate WMA of ROC sums
return _wma.Calc(new TValue(Input.Time, rocSum, Input.IsNew));
}
}
-97
View File
@@ -1,97 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CRSI: Connor RSI
/// A momentum oscillator that combines three different RSI time periods to provide
/// a more comprehensive view of price momentum. It helps identify overbought and
/// oversold conditions with higher accuracy than traditional RSI.
/// </summary>
/// <remarks>
/// The CRSI calculation process:
/// 1. Calculate three RSIs with different periods (3,2,1)
/// 2. Sum the three RSI values
/// 3. Divide by 3 to get the average
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - More responsive than traditional RSI
/// - Combines multiple timeframes
/// - Traditional overbought level at 90
/// - Traditional oversold level at 10
///
/// Formula:
/// CRSI = (RSI(3) + RSI(2) + RSI(1)) / 3
/// where each RSI is calculated using standard RSI formula:
/// RSI = 100 - (100 / (1 + RS))
/// RS = Average Gain / Average Loss
///
/// Sources:
/// Larry Connors - "Short-term Trading Strategies That Work"
/// https://www.tradingview.com/script/cYk1LVpw-Connors-RSI-LazyBear/
///
/// Note: Default periods are 3,2,1 as recommended by Connors
/// </remarks>
[SkipLocalsInit]
public sealed class Crsi : AbstractBase
{
private readonly Rsi _rsi3;
private readonly Rsi _rsi2;
private readonly Rsi _rsi1;
private const int DefaultPeriod1 = 3;
private const int DefaultPeriod2 = 2;
private const int DefaultPeriod3 = 1;
/// <param name="period1">The first RSI period (default 3).</param>
/// <param name="period2">The second RSI period (default 2).</param>
/// <param name="period3">The third RSI period (default 1).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Crsi(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3)
{
if (period1 < 1)
throw new ArgumentOutOfRangeException(nameof(period1), "Period1 must be greater than 0");
if (period2 < 1)
throw new ArgumentOutOfRangeException(nameof(period2), "Period2 must be greater than 0");
if (period3 < 1)
throw new ArgumentOutOfRangeException(nameof(period3), "Period3 must be greater than 0");
_rsi3 = new(period1);
_rsi2 = new(period2);
_rsi1 = new(period3);
WarmupPeriod = Math.Max(Math.Max(period1, period2), period3) + 1;
Name = $"CRSI({period1},{period2},{period3})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period1">The first RSI period.</param>
/// <param name="period2">The second RSI period.</param>
/// <param name="period3">The third RSI period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Crsi(object source, int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3)
: this(period1, period2, period3)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew) _index++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Calculate individual RSIs
double rsi3 = _rsi3.Calc(Input);
double rsi2 = _rsi2.Calc(Input);
double rsi1 = _rsi1.Calc(Input);
// Average the three RSIs
return (rsi3 + rsi2 + rsi1) / 3.0;
}
}
-109
View File
@@ -1,109 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// CTI: Ehler's Correlation Trend Indicator
/// Measures the correlation between price and an ideal trend line.
/// </summary>
/// <remarks>
/// The CTI calculation process:
/// 1. Correlates price curve with an ideal trend line (negative count due to backwards data storage)
/// 2. Uses Spearman's correlation algorithm
/// 3. Returns values between -1 and 1
///
/// Key characteristics:
/// - Oscillates between -1 and 1
/// - Positive values indicate price follows uptrend
/// - Negative values indicate price follows downtrend
///
/// Formula:
/// CTI = (n∑xy - ∑x∑y) / sqrt((n∑x² - (∑x)²)(n∑y² - (∑y)²))
/// where:
/// x = price curve
/// y = -count (ideal trend line)
/// n = period length
///
/// Sources:
/// John Ehlers - "Cybernetic Analysis for Stocks and Futures" (2004)
/// John Ehlers, Correlation Trend Indicator, Stocks & Commodities May-2020
/// </remarks>
[SkipLocalsInit]
public sealed class Cti : AbstractBase
{
private readonly int _period;
private readonly CircularBuffer _priceBuffer;
private readonly double[] _trendLine;
private const int MinimumPoints = 2; // Minimum points needed for correlation
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The calculation period (default: 20)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cti(object source, int period = 20) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Cti(int period = 20)
{
_period = period;
_priceBuffer = new CircularBuffer(period);
// Pre-calculate trend line values since they're static
_trendLine = new double[period];
for (int i = 0; i < period; i++)
{
_trendLine[i] = -i; // negative count for backwards data
}
WarmupPeriod = period;
Name = "CTI";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(Input.IsNew);
_priceBuffer.Add(Input.Value, Input.IsNew);
// Use available points for early calculations
int points = Math.Min(_index + 1, _period);
if (points < MinimumPoints) return 0; // Need at least 2 points for correlation
double sx = 0, sy = 0, sxx = 0, sxy = 0, syy = 0;
// Calculate correlation components using available points
for (int i = 0; i < points; i++)
{
double x = _priceBuffer[i]; // price curve
double y = _trendLine[i]; // pre-calculated trend line
sx += x;
sy += y;
sxx += x * x;
sxy += x * y;
syy += y * y;
}
// Check for numerical stability
double denomX = (points * sxx) - (sx * sx);
double denomY = (points * syy) - (sy * sy);
if (denomX > 0 && denomY > 0)
{
return ((points * sxy) - (sx * sy)) / Math.Sqrt(denomX * denomY);
}
return 0;
}
}
-74
View File
@@ -1,74 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// DOSC: Derivative Oscillator
/// A momentum indicator that combines the Relative Strength Index (RSI) and the Moving Average Convergence Divergence (MACD) to identify potential trend reversals.
/// </summary>
/// <remarks>
/// The DOSC calculation process:
/// 1. Calculate the RSI
/// 2. Calculate the MACD of the RSI
/// 3. Calculate the signal line (SMA) of the MACD
/// 4. Subtract the signal line from the MACD to get the DOSC
///
/// Key characteristics:
/// - Combines RSI and MACD
/// - Oscillates above and below zero
/// - Positive values indicate bullish momentum
/// - Negative values indicate bearish momentum
/// - Crosses above zero suggest buying opportunities
/// - Crosses below zero suggest selling opportunities
///
/// Formula:
/// DOSC = MACD(RSI) - Signal(MACD(RSI))
///
/// Sources:
/// Original development
/// https://www.investopedia.com/terms/d/derivativeoscillator.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Dosc : AbstractBase
{
private readonly Rsi _rsi;
private readonly Macd _macd;
private readonly Sma _signal;
/// <param name="source">The data source object that publishes updates.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Dosc(object source) : this()
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Dosc()
{
_rsi = new Rsi();
_macd = new Macd();
_signal = new Sma(9);
WarmupPeriod = 34; // RSI requires 14 periods + MACD requires 26 periods + 9 for signal line
Name = "DOSC";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
var rsi = _rsi.Calc(BarInput.Close, BarInput.IsNew);
var macd = _macd.Calc(rsi, BarInput.IsNew);
_signal.Calc(macd, BarInput.IsNew);
return macd - _signal.Value;
}
}
-102
View File
@@ -1,102 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// EFI: Elder Ray's Force Index
/// A volume-based oscillator that measures the strength of price movements using volume.
/// It helps identify potential trend reversals and confirm price movements.
/// </summary>
/// <remarks>
/// The EFI calculation process:
/// 1. Calculate the difference between the current close and the previous close
/// 2. Multiply the difference by the current volume
/// 3. Apply an exponential moving average (EMA) to smooth the result
///
/// Key characteristics:
/// - Oscillates above and below zero
/// - Positive values indicate buying pressure
/// - Negative values indicate selling pressure
/// - Crosses above zero suggest buying opportunities
/// - Crosses below zero suggest selling opportunities
///
/// Formula:
/// EFI = EMA((Close - Close[1]) * Volume, period)
///
/// Sources:
/// Alexander Elder - "Trading for a Living" (1993)
/// https://www.investopedia.com/terms/f/force-index.asp
///
/// Note: Default period is 13
/// </remarks>
[SkipLocalsInit]
public sealed class Efi : AbstractBase
{
private readonly Ema _ema;
private double _prevClose;
private double _p_prevClose;
private const int DefaultPeriod = 13;
/// <param name="period">The smoothing period for EMA calculation (default 13).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Efi(int period = DefaultPeriod)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
_ema = new(period);
WarmupPeriod = period + 1;
Name = $"EFI({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The smoothing period for EMA calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Efi(object source, int period = DefaultPeriod) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void Init()
{
base.Init();
_ema.Init();
_prevClose = double.NaN;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevClose = _prevClose;
}
else
{
_prevClose = _p_prevClose;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
if (_index == 1)
{
_prevClose = BarInput.Close;
return 0;
}
// Calculate raw force index
double priceChange = BarInput.Close - _prevClose;
double forceIndex = priceChange * BarInput.Volume;
// Update previous close
_prevClose = BarInput.Close;
// Apply EMA smoothing
return _ema.Calc(forceIndex, BarInput.IsNew);
}
}
-94
View File
@@ -1,94 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// FISHER: Fisher Transform
/// A technical indicator that converts prices into a Gaussian normal distribution.
/// </summary>
/// <remarks>
/// The Fisher Transform calculation process:
/// 1. Calculate the value of the price relative to its high-low range.
/// 2. Apply the Fisher Transform formula to the normalized price.
/// 3. Smooth the result using an exponential moving average.
///
/// Key characteristics:
/// - Oscillates between -1 and 1
/// - Emphasizes price reversals
/// - Can be used to identify overbought and oversold conditions
///
/// Formula:
/// Fisher Transform = 0.5 * log((1 + x) / (1 - x))
/// where:
/// x = 2 * ((price - min) / (max - min) - 0.5)
///
/// Sources:
/// John F. Ehlers - "Rocket Science for Traders" (2001)
/// https://www.investopedia.com/terms/f/fisher-transform.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Fisher : AbstractBase
{
private readonly int _period;
private readonly double[] _prices;
private double _prevFisher;
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The calculation period (default: 10)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Fisher(object source, int period = 10) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Fisher(int period = 10)
{
_period = period;
_prices = new double[period];
WarmupPeriod = period;
Name = "FISHER";
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double NormalizePrice(double price, double min, double max)
{
return 2 * (((price - min) / (max - min)) - 0.5);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double FisherTransform(double value)
{
return 0.5 * System.Math.Log((1 + value) / (1 - value));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override double Calculation()
{
ManageState(Input.IsNew);
var idx = _index % _period;
_prices[idx] = Input.Value;
if (_index < _period - 1) return double.NaN;
var min = _prices.Min();
var max = _prices.Max();
var normalizedPrice = NormalizePrice(Input.Value, min, max);
var fisherValue = FisherTransform(normalizedPrice);
var smoothedFisher = 0.5 * (fisherValue + _prevFisher);
_prevFisher = smoothedFisher;
return smoothedFisher;
}
}
-116
View File
@@ -1,116 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// RSI: Relative Strength Index
/// A momentum oscillator that measures the speed and magnitude of recent price
/// changes to evaluate overbought or oversold conditions. RSI compares the
/// magnitude of recent gains to recent losses.
/// </summary>
/// <remarks>
/// The RSI calculation process:
/// 1. Calculates price changes from previous period
/// 2. Separates gains and losses
/// 3. Calculates average gain and loss using Wilder's smoothing
/// 4. Computes relative strength (avg gain / avg loss)
/// 5. Normalizes to 0-100 scale: 100 - (100 / (1 + RS))
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - Traditional overbought level at 70
/// - Traditional oversold level at 30
/// - Centerline (50) crossovers signal trend changes
/// - Divergences suggest potential reversals
///
/// Formula:
/// RSI = 100 - (100 / (1 + RS))
/// where:
/// RS = Average Gain / Average Loss
/// Average Gain/Loss = Wilder's smoothed average over period
///
/// Sources:
/// J. Welles Wilder Jr. - "New Concepts in Technical Trading Systems" (1978)
/// https://www.investopedia.com/terms/r/rsi.asp
///
/// Note: Default period of 14 was recommended by Wilder
/// </remarks>
[SkipLocalsInit]
public sealed class Rsi : AbstractBase
{
private readonly Rma _avgGain;
private readonly Rma _avgLoss;
private double _prevValue, _p_prevValue;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod = 14;
/// <param name="period">The number of periods used in the RSI calculation (default 14).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rsi(int period = DefaultPeriod)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
_avgGain = new(period, useSma: true);
_avgLoss = new(period, useSma: true);
_index = 0;
WarmupPeriod = period + 1;
Name = $"RSI({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods used in the RSI calculation.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rsi(object source, int period) : this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevValue = _prevValue;
}
else
{
_prevValue = _p_prevValue;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static (double gain, double loss) CalculateGainLoss(double change)
{
return (Math.Max(change, 0), Math.Max(-change, 0));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateRsi(double avgGain, double avgLoss)
{
return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index == 1)
{
_prevValue = Input.Value;
}
// Calculate price change and separate gains/losses
double change = Input.Value - _prevValue;
var (gain, loss) = CalculateGainLoss(change);
_prevValue = Input.Value;
// Calculate smoothed averages using Wilder's method
_avgGain.Calc(gain, Input.IsNew);
_avgLoss.Calc(loss, Input.IsNew);
// Calculate RSI
return CalculateRsi(_avgGain.Value, _avgLoss.Value);
}
}
-131
View File
@@ -1,131 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// RSX: Relative Strength eXtended
/// An enhanced version of RSI developed by Mark Jurik that applies JMA (Jurik Moving
/// Average) smoothing to the RSI calculation. RSX provides smoother signals with
/// less noise while maintaining responsiveness to significant price movements.
/// </summary>
/// <remarks>
/// The RSX calculation process:
/// 1. Calculates traditional RSI values
/// 2. Applies JMA smoothing to RSI output
/// 3. Uses optimized parameters for noise reduction
/// 4. Maintains RSI's 0-100 scale
///
/// Key characteristics:
/// - Smoother than traditional RSI
/// - Better noise reduction
/// - Maintains responsiveness to significant moves
/// - Same interpretation as RSI (0-100 scale)
/// - Fewer false signals than RSI
///
/// Formula:
/// RSX = JMA(RSI(price))
/// where:
/// RSI = standard Relative Strength Index
/// JMA = Jurik Moving Average with optimized parameters
///
/// Sources:
/// Mark Jurik - "The Jurik RSX"
/// https://www.jurikresearch.com/
///
/// Note: Proprietary enhancement of RSI using JMA technology
/// </remarks>
[SkipLocalsInit]
public sealed class Rsx : AbstractBase
{
private readonly Rma _avgGain;
private readonly Rma _avgLoss;
private readonly Jma _rsx;
private double _prevValue, _p_prevValue;
private const double ScalingFactor = 100.0;
private const int DefaultPeriod = 14;
private const int DefaultPhase = 0;
private const double DefaultFactor = 0.55;
private const int JmaPeriod = 8;
private const int JmaPower = 100;
private const double JmaPhase = 0.25;
private const int JmaExtra = 3;
/// <param name="period">The number of periods for RSI calculation (default 14).</param>
/// <param name="phase">The phase parameter for JMA smoothing (default 0).</param>
/// <param name="factor">The factor parameter for smoothing control (default 0.55).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rsx(int period = DefaultPeriod, int phase = DefaultPhase, double factor = DefaultFactor)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_avgGain = new(period);
_avgLoss = new(period);
_rsx = new(JmaPeriod, JmaPower, JmaPhase, JmaExtra);
_index = 0;
WarmupPeriod = period + 1;
Name = $"RSX({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The number of periods for RSI calculation.</param>
/// <param name="phase">The phase parameter for JMA smoothing.</param>
/// <param name="factor">The factor parameter for smoothing control.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Rsx(object source, int period, int phase = DefaultPhase, double factor = DefaultFactor) : this(period, phase, factor)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_index++;
_p_prevValue = _prevValue;
}
else
{
_prevValue = _p_prevValue;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static (double gain, double loss) CalculateGainLoss(double change)
{
return (Math.Max(change, 0), Math.Max(-change, 0));
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateRsi(double avgGain, double avgLoss)
{
return avgLoss > 0 ? ScalingFactor - (ScalingFactor / (1 + (avgGain / avgLoss))) : ScalingFactor;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
if (_index == 1)
{
_prevValue = Input.Value;
}
// Calculate RSI components
double change = Input.Value - _prevValue;
var (gain, loss) = CalculateGainLoss(change);
_prevValue = Input.Value;
// Calculate RSI
_avgGain.Calc(gain, Input.IsNew);
_avgLoss.Calc(loss, Input.IsNew);
double rsi = CalculateRsi(_avgGain.Value, _avgLoss.Value);
// Apply JMA smoothing
_rsx.Calc(rsi, Input.IsNew);
return _rsx.Value;
}
}
-118
View File
@@ -1,118 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// SMI: Stochastic Momentum Index
/// A double-smoothed momentum indicator that shows where the close is relative
/// to the midpoint of the recent high/low range. It helps identify overbought
/// and oversold conditions with higher accuracy than traditional stochastics.
/// </summary>
/// <remarks>
/// The SMI calculation process:
/// 1. Calculate median price distance (Close - (High + Low)/2)
/// 2. Calculate highest high and lowest low over period
/// 3. First smoothing of median distance and range
/// 4. Second smoothing of first smoothed values
/// 5. Scale to percentage (-100 to +100)
///
/// Key characteristics:
/// - Oscillates between -100 and +100
/// - Double smoothing reduces noise
/// - Traditional overbought level at +40
/// - Traditional oversold level at -40
/// - Centerline crossovers signal trend changes
///
/// Formula:
/// D = Close - (High + Low)/2
/// HL = Highest High - Lowest Low
/// First smoothing:
/// SD = EMA(EMA(D, period1), period2)
/// SHL = EMA(EMA(HL, period1), period2)
/// SMI = 100 * (SD / (SHL/2))
///
/// Sources:
/// William Blau - "Momentum, Direction, and Divergence" (1995)
/// https://www.tradingview.com/scripts/stochasticmomentumindex/
///
/// Note: Default periods (10,3,3) are commonly used values
/// </remarks>
[SkipLocalsInit]
public sealed class Smi : AbstractBase
{
private readonly CircularBuffer _highs;
private readonly CircularBuffer _lows;
private readonly Ema _dEma1;
private readonly Ema _dEma2;
private readonly Ema _hlEma1;
private readonly Ema _hlEma2;
private const int DefaultPeriod = 10;
private const int DefaultSmooth1 = 3;
private const int DefaultSmooth2 = 3;
private const double ScalingFactor = 100.0;
/// <param name="period">The lookback period (default 10).</param>
/// <param name="smooth1">First smoothing period (default 3).</param>
/// <param name="smooth2">Second smoothing period (default 3).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Smi(int period = DefaultPeriod, int smooth1 = DefaultSmooth1, int smooth2 = DefaultSmooth2)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smooth1, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smooth2, 1);
_highs = new(period);
_lows = new(period);
_dEma1 = new(smooth1);
_dEma2 = new(smooth2);
_hlEma1 = new(smooth1);
_hlEma2 = new(smooth2);
WarmupPeriod = period + smooth1 + smooth2;
Name = $"SMI({period},{smooth1},{smooth2})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period.</param>
/// <param name="smooth1">First smoothing period.</param>
/// <param name="smooth2">Second smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Smi(object source, int period = DefaultPeriod, int smooth1 = DefaultSmooth1, int smooth2 = DefaultSmooth2)
: this(period, smooth1, smooth2)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_highs.Add(BarInput.High);
_lows.Add(BarInput.Low);
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
// Calculate median price distance and range
double midpoint = (BarInput.High + BarInput.Low) / 2.0;
double distance = BarInput.Close - midpoint;
double range = _highs.Max() - _lows.Min();
// First smoothing
double smoothD1 = _dEma1.Calc(new TValue(BarInput.Time, distance, BarInput.IsNew));
double smoothHL1 = _hlEma1.Calc(new TValue(BarInput.Time, range, BarInput.IsNew));
// Second smoothing
double smoothD2 = _dEma2.Calc(new TValue(BarInput.Time, smoothD1, BarInput.IsNew));
double smoothHL2 = _hlEma2.Calc(new TValue(BarInput.Time, smoothHL1, BarInput.IsNew));
// Calculate SMI
return smoothHL2 >= double.Epsilon ? ScalingFactor * (smoothD2 / (smoothHL2 / 2.0)) : 0;
}
}
-131
View File
@@ -1,131 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// SRSI: Stochastic RSI
/// A momentum oscillator that applies the stochastic formula to RSI values
/// instead of price data. It provides a more sensitive indicator than standard
/// RSI or Stochastic oscillators.
/// </summary>
/// <remarks>
/// The SRSI calculation process:
/// 1. Calculate RSI
/// 2. Apply Stochastic formula to RSI values:
/// - Find highest high and lowest low of RSI over period
/// - Calculate where current RSI is within this range
/// 3. Smooth the result with SMA (signal line)
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - More sensitive than standard RSI
/// - Combines benefits of both RSI and Stochastic
/// - Traditional overbought level at 80
/// - Traditional oversold level at 20
///
/// Formula:
/// SRSI = ((RSI - Lowest RSI) / (Highest RSI - Lowest RSI)) * 100
/// Signal = SMA(SRSI, signalPeriod)
///
/// Sources:
/// Tushar Chande and Stanley Kroll - "The New Technical Trader" (1994)
/// https://www.investopedia.com/terms/s/stochrsi.asp
///
/// Note: Default periods (14,14,3,3) are commonly used values
/// </remarks>
[SkipLocalsInit]
public sealed class Srsi : AbstractBase
{
private readonly Rsi _rsi;
private readonly CircularBuffer _rsiValues;
private readonly CircularBuffer _srsiValues;
private readonly Sma _signal;
private readonly int _rsiPeriod;
private const int DefaultRsiPeriod = 14;
private const int DefaultStochPeriod = 14;
private const int DefaultSmoothK = 3;
private const int DefaultSmoothD = 3;
private const double ScalingFactor = 100.0;
/// <param name="rsiPeriod">The RSI period (default 14).</param>
/// <param name="stochPeriod">The Stochastic period (default 14).</param>
/// <param name="smoothK">K line smoothing period (default 3).</param>
/// <param name="smoothD">D line smoothing period (default 3).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Srsi(int rsiPeriod = DefaultRsiPeriod, int stochPeriod = DefaultStochPeriod,
int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD)
{
ArgumentOutOfRangeException.ThrowIfLessThan(rsiPeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(stochPeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smoothK, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smoothD, 1);
_rsiPeriod = rsiPeriod;
_rsi = new(rsiPeriod);
_rsiValues = new(stochPeriod);
_srsiValues = new(smoothK);
_signal = new(smoothD);
WarmupPeriod = rsiPeriod + stochPeriod + Math.Max(smoothK, smoothD);
Name = $"SRSI({rsiPeriod},{stochPeriod},{smoothK},{smoothD})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="rsiPeriod">The RSI period.</param>
/// <param name="stochPeriod">The Stochastic period.</param>
/// <param name="smoothK">K line smoothing period.</param>
/// <param name="smoothD">D line smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Srsi(object source, int rsiPeriod = DefaultRsiPeriod, int stochPeriod = DefaultStochPeriod,
int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD)
: this(rsiPeriod, stochPeriod, smoothK, smoothD)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew) _index++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Calculate RSI
double rsiValue = _rsi.Calc(Input);
if (Input.IsNew)
_rsiValues.Add(rsiValue);
// Not enough data
if (_index <= _rsiPeriod)
return 0;
// Calculate Stochastic RSI
double highest = _rsiValues.Max();
double lowest = _rsiValues.Min();
double range = highest - lowest;
double srsi = range >= double.Epsilon ? ((rsiValue - lowest) / range) * ScalingFactor : 0;
if (Input.IsNew)
_srsiValues.Add(srsi);
// Calculate signal line
return _signal.Calc(new TValue(Input.Time, srsi, Input.IsNew));
}
/// <summary>
/// Gets the K line value (raw Stochastic RSI)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double K() => _srsiValues[0];
/// <summary>
/// Gets the D line value (signal line)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double D() => Value;
}
-145
View File
@@ -1,145 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// STC: Schaff Trend Cycle
/// A trend-following indicator that combines MACD and stochastic concepts
/// to create a smoother, more responsive indicator with less noise.
/// </summary>
/// <remarks>
/// The STC calculation process:
/// 1. Calculate MACD-style momentum using EMAs
/// 2. Apply double stochastic formula to smooth the momentum
/// 3. Scale result to oscillator range
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - Combines trend and momentum
/// - Double smoothing reduces noise
/// - Traditional overbought level at 75
/// - Traditional oversold level at 25
///
/// Formula:
/// Momentum = EMA1(Close) - EMA2(Close)
/// First Stochastic:
/// %K1 = 100 * (Momentum - Lowest Low) / (Highest High - Lowest Low)
/// %D1 = EMA(%K1)
/// Second Stochastic:
/// %K2 = 100 * (%D1 - Lowest %D1) / (Highest %D1 - Lowest %D1)
/// STC = EMA(%K2)
///
/// Sources:
/// Doug Schaff - "The Schaff Trend Cycle" (1999)
/// https://www.tradingview.com/script/o6tSS6Hn-Schaff-Trend-Cycle/
///
/// Note: Default periods (23,10,3) were recommended by Schaff
/// </remarks>
[SkipLocalsInit]
public sealed class Stc : AbstractBase
{
private readonly Ema _fastEma;
private readonly Ema _slowEma;
private readonly CircularBuffer _macdValues;
private readonly CircularBuffer _k1Values;
private readonly CircularBuffer _d1Values;
private readonly Ema _d1Ema;
private readonly Ema _stcEma;
private const int DefaultCyclePeriod = 10;
private const int DefaultFastPeriod = 23;
private const int DefaultSlowPeriod = 50;
private const int DefaultD1Period = 3;
private const int DefaultStcPeriod = 3;
private const double ScalingFactor = 100.0;
/// <param name="cyclePeriod">The lookback period for highs/lows (default 10).</param>
/// <param name="fastPeriod">Fast EMA period (default 23).</param>
/// <param name="slowPeriod">Slow EMA period (default 50).</param>
/// <param name="d1Period">First %D smoothing period (default 3).</param>
/// <param name="stcPeriod">Final STC smoothing period (default 3).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stc(int cyclePeriod = DefaultCyclePeriod, int fastPeriod = DefaultFastPeriod,
int slowPeriod = DefaultSlowPeriod, int d1Period = DefaultD1Period,
int stcPeriod = DefaultStcPeriod)
{
ArgumentOutOfRangeException.ThrowIfLessThan(cyclePeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(fastPeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(slowPeriod, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(d1Period, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(stcPeriod, 1);
if (fastPeriod >= slowPeriod)
{
throw new ArgumentOutOfRangeException(nameof(fastPeriod), "Fast period must be less than slow period");
}
_fastEma = new(fastPeriod);
_slowEma = new(slowPeriod);
_macdValues = new(cyclePeriod);
_k1Values = new(cyclePeriod);
_d1Values = new(cyclePeriod);
_d1Ema = new(d1Period);
_stcEma = new(stcPeriod);
WarmupPeriod = slowPeriod + cyclePeriod + Math.Max(d1Period, stcPeriod);
Name = $"STC({cyclePeriod},{fastPeriod},{slowPeriod},{d1Period},{stcPeriod})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="cyclePeriod">The lookback period for highs/lows.</param>
/// <param name="fastPeriod">Fast EMA period.</param>
/// <param name="slowPeriod">Slow EMA period.</param>
/// <param name="d1Period">First %D smoothing period.</param>
/// <param name="stcPeriod">Final STC smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stc(object source, int cyclePeriod = DefaultCyclePeriod, int fastPeriod = DefaultFastPeriod,
int slowPeriod = DefaultSlowPeriod, int d1Period = DefaultD1Period,
int stcPeriod = DefaultStcPeriod)
: this(cyclePeriod, fastPeriod, slowPeriod, d1Period, stcPeriod)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew) _index++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateStochastic(double value, double highest, double lowest)
{
double range = highest - lowest;
return range >= double.Epsilon ? ((value - lowest) / range) * ScalingFactor : 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Calculate MACD-style momentum
double fastEma = _fastEma.Calc(Input);
double slowEma = _slowEma.Calc(Input);
double macd = fastEma - slowEma;
if (Input.IsNew)
_macdValues.Add(macd);
// First stochastic
double k1 = CalculateStochastic(macd, _macdValues.Max(), _macdValues.Min());
if (Input.IsNew)
_k1Values.Add(k1);
double d1 = _d1Ema.Calc(new TValue(Input.Time, k1, Input.IsNew));
if (Input.IsNew)
_d1Values.Add(d1);
// Second stochastic
double k2 = CalculateStochastic(d1, _d1Values.Max(), _d1Values.Min());
// Final smoothing
return _stcEma.Calc(new TValue(Input.Time, k2, Input.IsNew));
}
}
-123
View File
@@ -1,123 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// STOCH: Stochastic Oscillator
/// A momentum indicator that shows the location of the close relative to
/// high-low range over a period. Consists of %K (fast) and %D (slow) lines.
/// </summary>
/// <remarks>
/// The Stochastic calculation process:
/// 1. Calculate %K (raw stochastic):
/// - Find highest high and lowest low over period
/// - Calculate where current close is within this range
/// 2. Smooth %K with SMA to get Fast %K
/// 3. Smooth Fast %K with SMA to get %D (signal line)
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - Traditional overbought level at 80
/// - Traditional oversold level at 20
/// - %K/%D crossovers signal momentum shifts
/// - Divergence with price shows potential reversals
///
/// Formula:
/// Raw %K = 100 * (Close - Lowest Low) / (Highest High - Lowest Low)
/// Fast %K = SMA(Raw %K, smoothK)
/// %D = SMA(Fast %K, smoothD)
///
/// Sources:
/// George Lane - "Lane's Stochastics" (1950s)
/// https://www.investopedia.com/terms/s/stochasticoscillator.asp
///
/// Note: Default periods (14,3,3) are commonly used values
/// </remarks>
[SkipLocalsInit]
public sealed class Stoch : AbstractBase
{
private readonly CircularBuffer _highs;
private readonly CircularBuffer _lows;
private readonly Sma _fastK;
private readonly Sma _slowD;
private readonly CircularBuffer _rawK;
private const int DefaultPeriod = 14;
private const int DefaultSmoothK = 3;
private const int DefaultSmoothD = 3;
private const double ScalingFactor = 100.0;
/// <param name="period">The lookback period (default 14).</param>
/// <param name="smoothK">%K smoothing period (default 3).</param>
/// <param name="smoothD">%D smoothing period (default 3).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stoch(int period = DefaultPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smoothK, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(smoothD, 1);
_highs = new(period);
_lows = new(period);
_rawK = new(smoothK);
_fastK = new(smoothK);
_slowD = new(smoothD);
WarmupPeriod = period + Math.Max(smoothK, smoothD);
Name = $"STOCH({period},{smoothK},{smoothD})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period.</param>
/// <param name="smoothK">%K smoothing period.</param>
/// <param name="smoothD">%D smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Stoch(object source, int period = DefaultPeriod, int smoothK = DefaultSmoothK, int smoothD = DefaultSmoothD)
: this(period, smoothK, smoothD)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_highs.Add(BarInput.High);
_lows.Add(BarInput.Low);
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
// Calculate raw %K
double highest = _highs.Max();
double lowest = _lows.Min();
double range = highest - lowest;
double rawK = range >= double.Epsilon ? ((BarInput.Close - lowest) / range) * ScalingFactor : 0;
if (BarInput.IsNew)
_rawK.Add(rawK);
// Calculate Fast %K (first smoothing)
double fastK = _fastK.Calc(new TValue(BarInput.Time, rawK, BarInput.IsNew));
// Calculate %D (second smoothing)
return _slowD.Calc(new TValue(BarInput.Time, fastK, BarInput.IsNew));
}
/// <summary>
/// Gets the %K line value (Fast Stochastic)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double K() => _fastK.Value;
/// <summary>
/// Gets the %D line value (Slow Stochastic)
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public double D() => Value;
}
-111
View File
@@ -1,111 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// TSI: True Strength Index
/// A momentum oscillator that shows both trend direction and overbought/oversold conditions.
/// Uses two EMAs of price change momentum to help identify short-term trends and reversals.
/// </summary>
/// <remarks>
/// The TSI calculation process:
/// 1. Calculate price change (PC): Current close - Previous close
/// 2. Calculate absolute price change (APC): Absolute value of PC
/// 3. First smoothing: EMA1 of PC and EMA1 of APC
/// 4. Second smoothing: EMA2 of EMA1(PC) and EMA2 of EMA1(APC)
/// 5. TSI = 100 * (Double smoothed PC / Double smoothed APC)
///
/// Key characteristics:
/// - Oscillates around zero
/// - Shows momentum and trend direction
/// - Identifies overbought/oversold conditions
/// - Generates signals through centerline/signal line crossovers
/// - Shows momentum divergence with price
///
/// Formula:
/// TSI = 100 * (EMA2(EMA1(PC)) / EMA2(EMA1(APC)))
/// where:
/// PC = Current Price - Previous Price
/// APC = |PC|
/// Default periods: First EMA = 25, Second EMA = 13
///
/// Sources:
/// William Blau - "Momentum, Direction, and Divergence" (1995)
/// https://www.investopedia.com/terms/t/tsi.asp
///
/// Note: Default periods (25,13) were recommended by Blau
/// </remarks>
[SkipLocalsInit]
public sealed class Tsi : AbstractBase
{
private readonly Ema _pcEma1;
private readonly Ema _pcEma2;
private readonly Ema _apcEma1;
private readonly Ema _apcEma2;
private double _prevPrice;
private const int DefaultFirstPeriod = 25;
private const int DefaultSecondPeriod = 13;
private const double ScalingFactor = 100.0;
/// <param name="firstPeriod">The first EMA smoothing period (default 25).</param>
/// <param name="secondPeriod">The second EMA smoothing period (default 13).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Tsi(int firstPeriod = DefaultFirstPeriod, int secondPeriod = DefaultSecondPeriod)
{
if (firstPeriod < 1 || secondPeriod < 1)
throw new ArgumentOutOfRangeException(nameof(firstPeriod), "All periods must be greater than 0");
_pcEma1 = new(firstPeriod);
_pcEma2 = new(secondPeriod);
_apcEma1 = new(firstPeriod);
_apcEma2 = new(secondPeriod);
WarmupPeriod = firstPeriod + secondPeriod;
Name = $"TSI({firstPeriod},{secondPeriod})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="firstPeriod">The first EMA smoothing period.</param>
/// <param name="secondPeriod">The second EMA smoothing period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Tsi(object source, int firstPeriod = DefaultFirstPeriod, int secondPeriod = DefaultSecondPeriod)
: this(firstPeriod, secondPeriod)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new ValueSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
if (_index == 0)
_prevPrice = Input.Value;
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(Input.IsNew);
// Calculate price changes
double priceChange = Input.Value - _prevPrice;
double absPriceChange = Math.Abs(priceChange);
if (Input.IsNew)
_prevPrice = Input.Value;
// First smoothing
double smoothPc = _pcEma1.Calc(new TValue(Input.Time, priceChange, Input.IsNew));
double smoothApc = _apcEma1.Calc(new TValue(Input.Time, absPriceChange, Input.IsNew));
// Second smoothing
double doubleSmoothedPc = _pcEma2.Calc(new TValue(Input.Time, smoothPc, Input.IsNew));
double doubleSmoothedApc = _apcEma2.Calc(new TValue(Input.Time, smoothApc, Input.IsNew));
// Calculate TSI
return doubleSmoothedApc >= double.Epsilon ? ScalingFactor * (doubleSmoothedPc / doubleSmoothedApc) : 0;
}
}
-161
View File
@@ -1,161 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// UO: Ultimate Oscillator
/// A momentum oscillator that uses three different time periods to reduce volatility
/// and false signals. It incorporates a weighted average of three oscillator calculations
/// using different periods.
/// </summary>
/// <remarks>
/// The UO calculation process:
/// 1. Calculate buying pressure (BP): Close - Min(Low, Prior Close)
/// 2. Calculate true range (TR): Max(High, Prior Close) - Min(Low, Prior Close)
/// 3. Calculate average of BP/TR for each period
/// 4. Apply weights to each period's average
/// 5. Scale result to oscillator range
///
/// Key characteristics:
/// - Oscillates between 0 and 100
/// - Uses multiple timeframes to reduce false signals
/// - Weighted sum of three periods
/// - Traditional overbought level at 70
/// - Traditional oversold level at 30
///
/// Formula:
/// UO = 100 * ((4 * Average7) + (2 * Average14) + Average28) / (4 + 2 + 1)
/// where:
/// Average7 = 7-period average of BP/TR
/// Average14 = 14-period average of BP/TR
/// Average28 = 28-period average of BP/TR
///
/// Sources:
/// Larry Williams - "New Trading Dimensions" (1998)
/// https://www.investopedia.com/terms/u/ultimateoscillator.asp
///
/// Note: Default periods (7,14,28) and weights (4,2,1) were recommended by Williams
/// </remarks>
[SkipLocalsInit]
public sealed class Uo : AbstractBase
{
private readonly CircularBuffer _bp1;
private readonly CircularBuffer _tr1;
private readonly CircularBuffer _bp2;
private readonly CircularBuffer _tr2;
private readonly CircularBuffer _bp3;
private readonly CircularBuffer _tr3;
private readonly double _weight1;
private readonly double _weight2;
private readonly double _weight3;
private double _prevClose;
private const int DefaultPeriod1 = 7;
private const int DefaultPeriod2 = 14;
private const int DefaultPeriod3 = 28;
private const double DefaultWeight1 = 4.0;
private const double DefaultWeight2 = 2.0;
private const double DefaultWeight3 = 1.0;
private const double ScalingFactor = 100.0;
/// <param name="period1">The first period (default 7).</param>
/// <param name="period2">The second period (default 14).</param>
/// <param name="period3">The third period (default 28).</param>
/// <param name="weight1">Weight for first period (default 4).</param>
/// <param name="weight2">Weight for second period (default 2).</param>
/// <param name="weight3">Weight for third period (default 1).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when any period is less than 1 or any weight is less than or equal to 0.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Uo(int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3,
double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3)
{
ArgumentOutOfRangeException.ThrowIfLessThan(period1, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(period2, 1);
ArgumentOutOfRangeException.ThrowIfLessThan(period3, 1);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight1, 0);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight2, 0);
ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(weight3, 0);
_weight1 = weight1;
_weight2 = weight2;
_weight3 = weight3;
_bp1 = new(period1);
_tr1 = new(period1);
_bp2 = new(period2);
_tr2 = new(period2);
_bp3 = new(period3);
_tr3 = new(period3);
WarmupPeriod = period3;
Name = $"UO({period1},{period2},{period3})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period1">The first period.</param>
/// <param name="period2">The second period.</param>
/// <param name="period3">The third period.</param>
/// <param name="weight1">Weight for first period.</param>
/// <param name="weight2">Weight for second period.</param>
/// <param name="weight3">Weight for third period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Uo(object source, int period1 = DefaultPeriod1, int period2 = DefaultPeriod2, int period3 = DefaultPeriod3,
double weight1 = DefaultWeight1, double weight2 = DefaultWeight2, double weight3 = DefaultWeight3)
: this(period1, period2, period3, weight1, weight2, weight3)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
if (_index == 0)
_prevClose = BarInput.Close;
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static double CalculateAverage(CircularBuffer bp, CircularBuffer tr)
{
double trSum = tr.Sum();
return trSum >= double.Epsilon ? bp.Sum() / trSum : 0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
// Calculate buying pressure and true range
double minLowPrevClose = Math.Min(BarInput.Low, _prevClose);
double maxHighPrevClose = Math.Max(BarInput.High, _prevClose);
double bp = BarInput.Close - minLowPrevClose;
double tr = maxHighPrevClose - minLowPrevClose;
if (BarInput.IsNew)
{
// Add values to buffers
_bp1.Add(bp);
_tr1.Add(tr);
_bp2.Add(bp);
_tr2.Add(tr);
_bp3.Add(bp);
_tr3.Add(tr);
_prevClose = BarInput.Close;
}
// Not enough data
if (_index <= 1) return 0;
// Calculate averages for each period
double avg1 = CalculateAverage(_bp1, _tr1);
double avg2 = CalculateAverage(_bp2, _tr2);
double avg3 = CalculateAverage(_bp3, _tr3);
// Calculate weighted sum
double weightSum = _weight1 + _weight2 + _weight3;
return ScalingFactor * (((_weight1 * avg1) + (_weight2 * avg2) + (_weight3 * avg3)) / weightSum);
}
}
-86
View File
@@ -1,86 +0,0 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// WILLR: Williams %R
/// A momentum oscillator that measures the level of the close relative to the
/// highest high for a look-back period. Similar to Stochastic Oscillator but
/// with a reversed scale and no smoothing.
/// </summary>
/// <remarks>
/// The Williams %R calculation process:
/// 1. Find highest high and lowest low over period
/// 2. Calculate where current close is within this range
/// 3. Scale result to -100 to 0 range
///
/// Key characteristics:
/// - Oscillates between -100 and 0
/// - Similar to Stochastic but no smoothing
/// - Traditional overbought level at -20
/// - Traditional oversold level at -80
/// - Leading indicator for market tops/bottoms
///
/// Formula:
/// %R = -100 * (Highest High - Close) / (Highest High - Lowest Low)
///
/// Sources:
/// Larry Williams - "How I Made One Million Dollars Last Year Trading Commodities" (1973)
/// https://www.investopedia.com/terms/w/williamsr.asp
///
/// Note: Default period of 14 is commonly used
/// </remarks>
[SkipLocalsInit]
public sealed class Willr : AbstractBase
{
private readonly CircularBuffer _highs;
private readonly CircularBuffer _lows;
private const int DefaultPeriod = 14;
private const double ScalingFactor = -100.0;
/// <param name="period">The lookback period (default 14).</param>
/// <exception cref="ArgumentOutOfRangeException">Thrown when period is less than 1.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Willr(int period = DefaultPeriod)
{
if (period < 1)
throw new ArgumentOutOfRangeException(nameof(period));
_highs = new(period);
_lows = new(period);
WarmupPeriod = period;
Name = $"WILLR({period})";
}
/// <param name="source">The data source object that publishes updates.</param>
/// <param name="period">The lookback period.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Willr(object source, int period = DefaultPeriod)
: this(period)
{
var pubEvent = source.GetType().GetEvent("Pub");
pubEvent?.AddEventHandler(source, new BarSignal(Sub));
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void ManageState(bool isNew)
{
if (isNew)
{
_highs.Add(BarInput.High);
_lows.Add(BarInput.Low);
_index++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
protected override double Calculation()
{
ManageState(BarInput.IsNew);
double highest = _highs.Max();
double lowest = _lows.Min();
double range = highest - lowest;
return range >= double.Epsilon ? ScalingFactor * ((highest - BarInput.Close) / range) : 0;
}
}
+67
View File
@@ -0,0 +1,67 @@
# Oscillators
> "Oscillators tell you when to act, not which direction to trade."  Unknown
Oscillators fluctuate above and below a centerline or within bounded ranges. Useful for identifying overbought/oversold conditions, momentum shifts, and divergences. Best in ranging markets; trend-following indicators work better in trending markets.
## Indicator Status
| Indicator | Full Name | Status | Description |
| :--- | :--- | :---: | :--- |
| AC | Acceleration Oscillator | = | Second derivative of AO. Measures acceleration of market driving force. |
| [AO](lib/oscillators/ao/ao.md) | Awesome Oscillator |  | 5-period SMA minus 34-period SMA of bar midpoint. Bill Williams creation. |
| [APO](lib/oscillators/apo/Apo.md) | Absolute Price Oscillator |  | Raw currency difference between fast and slow EMAs. Unbounded. |
| BBB | Bollinger %B | = | Position within Bollinger Bands. 0=lower band, 1=upper band. || BBI | Bulls Bears Index | ≡ | Measures the relative strength of bulls and bears based on price action. || BBS | Bollinger Band Squeeze | = | BB width < KC width indicates consolidation. Breakout imminent. || BOP | Balance of Power | ≡ | Measures the strength of buyers vs. sellers by relating price change to the trading range. |
| BRAR | BRAR | ≡ | Combines AR (sentiment) and BR (momentum) indicators to gauge market mood. |
| CCI | Commodity Channel Index | ≡ | Measures price deviation from its statistical mean, identifies cyclical turns. |
| COPPOCK | Coppock Curve | ≡ | Long-term momentum oscillator used primarily for identifying major market bottoms. |
| CRSI | Connors RSI | ≡ | Composite indicator combining RSI, Up/Down Streak Length, and Rate-of-Change. |
| CTI | Correlation Trend Indicator | ≡ | Measures the correlation between price and time to determine trend strength. |
| DOSC | Derivative Oscillator | ≡ | Measures the difference between a double-smoothed RSI and its signal line. || CFO | Chande Forecast Oscillator | = | Percentage difference between price and linear regression forecast. |
| DPO | Detrended Price Oscillator | = | Removes trend via displaced SMA. Reveals cycles. || ER | Efficiency Ratio | ≡ | Measures price efficiency by comparing net price movement to total price movement (KAMA component). |
| ERI | Elder Ray Index | ≡ | Measures buying (Bull Power) and selling (Bear Power) pressure relative to an EMA. || FISHER | Fisher Transform | = | Converts prices to Gaussian distribution. Sharp reversals. || FOSC | Forecast Oscillator | ≡ | Plots the percentage difference between a forecast price (e.g., linear regression) and the actual price. || INERTIA | Inertia | = | Trend strength from distance to linear regression line. |
| KDJ | KDJ Indicator | = | Enhanced Stochastic. J = 3K - 2D provides leading signal. || KRI | Kairi Relative Index | ≡ | Measures the deviation of the current price from its simple moving average. |
| KST | KST Oscillator | ≡ | Smoothed, weighted Rate-of-Change oscillator combining multiple timeframes. || PGO | Pretty Good Oscillator | = | Distance from SMA normalized by ATR. Units: ATR multiples. || PSL | Psychological Line | ≡ | Measures percentage of days closing up over a specified period, gauges sentiment. |
| QQE | Quantitative Qualitative Estimation | ≡ | Smoothing technique applied to RSI, providing trade signals via signal line crossovers. |
| RVGI | Relative Vigor Index | ≡ | Compares closing price to trading range. || SMI | Stochastic Momentum Index | = | Distance from range midpoint. More sensitive than classic Stochastic. || SQUEEZE | Squeeze | ≡ | Identifies periods of low volatility (Bollinger Bands inside Keltner Channels) for potential breakouts. || STOCH | Stochastic Oscillator | = | Close position within N-period high-low range. Classic overbought/oversold. |
| STOCHF | Stochastic Fast | = | Unsmoothed Stochastic. Faster but noisier. |
| STOCHRSI | Stochastic RSI | = | Stochastic applied to RSI. More sensitive than either alone. || TD_SEQ | TD Sequential | ≡ | Identifies potential price exhaustion points and reversals based on price bar counting. || TRIX | Triple Exponential Average | = | ROC of triple EMA. Filters noise through three smoothings. |
| [ULTOSC](lib/oscillators/ultosc/ultosc.md) | Ultimate Oscillator |  | Multi-timeframe oscillator. Combines 7, 14, 28 period buying pressure. |
| WILLR | Williams %R | = | Inverse Stochastic. -100 to 0 range. Overbought/oversold. |
**Status Key:**  Implemented | = Planned
## Selection Guide
| Use Case | Recommended | Why |
| :--- | :--- | :--- |
| Momentum confirmation | AO, APO | AO for bar midpoint. APO for close price. Both unbounded. |
| Overbought/oversold | STOCH, WILLR, STOCHRSI | Bounded 0-100 or -100 to 0. Classic mean reversion signals. |
| Multi-timeframe analysis | ULTOSC | Combines three periods. Reduces false signals. |
| Cycle detection | DPO | Removes trend to reveal underlying cycles. |
| Leading signals | KDJ, FISHER | J-line leads K and D. Fisher provides sharp turns. |
| Noise filtering | TRIX | Triple smoothing removes most short-term noise. |
| Volatility-normalized | PGO | ATR normalization makes signals comparable across instruments. |
## Oscillator Types
| Type | Examples | Range | Best For |
| :--- | :--- | :--- | :--- |
| Bounded (0-100) | STOCH, STOCHRSI | 0 to 100 | Overbought/oversold zones |
| Bounded (-100 to 0) | WILLR | -100 to 0 | Mean reversion |
| Bounded (-1 to +1) | FISHER | - to + (practical: 3) | Sharp reversal signals |
| Unbounded | AO, APO, DPO | - to + | Trend momentum |
| Normalized | PGO, CFO | ATR or % units | Cross-market comparison |
## Divergence Analysis
Oscillator divergence signals potential reversals:
| Price Action | Oscillator Action | Signal |
| :--- | :--- | :--- |
| Higher high | Lower high | Bearish divergence. Weakening momentum. |
| Lower low | Higher low | Bullish divergence. Strengthening support. |
| Higher high | Higher high | Confirmation. Trend intact. |
| Lower low | Lower low | Confirmation. Trend intact. |
Divergences work best with bounded oscillators (STOCH, RSI, WILLR) where extremes are well-defined.
-32
View File
@@ -1,32 +0,0 @@
# Oscillators indicators
Done: 24, Todo: 5
✔️ AC - Acceleration Oscillator
✔️ AO - Awesome Oscillator
✔️ AROON - Aroon oscillator (Up, Down)
✔️ BOP - Balance of Power
✔️ CCI - Commodity Channel Index
✔️ CFO - Chande Forcast Oscillator
✔️ CHOP - Choppiness Index
✔️ CMO - Chande Momentum Oscillator
✔️ COG - Ehler's Center of Gravity
✔️ COPPOCK - Coppock Curve
✔️ CRSI - Connor RSI
✔️ CTI - Ehler's Correlation Trend Indicator
✔️ DOSC - Derivative Oscillator
✔️ FISHER - Fisher Transform
✔️ EFI - Elder Ray's Force Index
FOSC - Forecast Oscillator
*GATOR - Williams Alliator Oscillator (Upper Jaw, Lower Jaw, Teeth)
*KDJ - KDJ Indicator (K, D, J lines)
KRI - Kairi Relative Index
✔️ RSI - Relative Strength Index
✔️ RSX - Jurik Trend Strength Index
*RVGI - Relative Vigor Index (RVGI, Signal)
✔️ SMI - Stochastic Momentum Index
✔️ SRSI - Stochastic RSI (SRSI, Signal)
✔️ STC - Schaff Trend Cycle
✔️ STOCH - Stochastic Oscillator (%K, %D)
✔️ TSI - True Strength Index
✔️ UO - Ultimate Oscillator
✔️ WILLR - Larry Williams' %R
+67
View File
@@ -0,0 +1,67 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Accelerator Oscillator (AC)", "AC", overlay=false)
//@function Calculates Bill Williams' Accelerator Oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/ac.md
//@param fastLength Period for fast MA calculation
//@param slowLength Period for slow MA calculation
//@returns AC value measuring acceleration/deceleration of market momentum
ac(simple int fastLength, simple int slowLength) =>
if fastLength <= 0 or slowLength <= 0
runtime.error("Lengths must be greater than 0")
if fastLength >= slowLength
runtime.error("Fast length must be less than slow length")
float mp = (high + low) / 2.0
var array<float> fastBuf = array.new_float(fastLength, na)
var array<float> slowBuf = array.new_float(slowLength, na)
var int fastHead = 0, var int slowHead = 0
var float fastSum = 0.0, var float slowSum = 0.0
var int fastCount = 0, var int slowCount = 0
float fastOldest = array.get(fastBuf, fastHead)
if not na(fastOldest)
fastSum -= fastOldest
fastCount -= 1
if not na(mp)
fastSum += mp
fastCount += 1
array.set(fastBuf, fastHead, mp)
fastHead := (fastHead + 1) % fastLength
float slowOldest = array.get(slowBuf, slowHead)
if not na(slowOldest)
slowSum -= slowOldest
slowCount -= 1
if not na(mp)
slowSum += mp
slowCount += 1
array.set(slowBuf, slowHead, mp)
slowHead := (slowHead + 1) % slowLength
float fastMA = fastCount > 0 ? fastSum / fastCount : na
float slowMA = slowCount > 0 ? slowSum / slowCount : na
float ao = fastMA - slowMA
var array<float> acBuf = array.new_float(5, na)
var int acHead = 0, var float acSum = 0.0, var int acCount = 0
float acOldest = array.get(acBuf, acHead)
if not na(acOldest)
acSum -= acOldest
acCount -= 1
if not na(ao)
acSum += ao
acCount += 1
array.set(acBuf, acHead, ao)
acHead := (acHead + 1) % 5
float aoSMA = acCount > 0 ? acSum / acCount : na
ao - aoSMA
// ---------- Main loop ----------
// Inputs
i_fastLength = input.int(5, "Fast Length", minval=1)
i_slowLength = input.int(34, "Slow Length", minval=1)
// Calculation
ac_value = ac(i_fastLength, i_slowLength)
// Plot
plot(ac_value, "AC", ac_value >= 0 ? color.green : color.red, linewidth=2)
+124
View File
@@ -0,0 +1,124 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class AoIndicatorTests
{
[Fact]
public void AoIndicator_Constructor_SetsDefaults()
{
var indicator = new AoIndicator();
Assert.Equal(5, indicator.FastPeriod);
Assert.Equal(34, indicator.SlowPeriod);
Assert.True(indicator.ShowColdValues);
Assert.Equal("AO - Awesome Oscillator", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void AoIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new AoIndicator { SlowPeriod = 20 };
Assert.Equal(0, AoIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void AoIndicator_ShortName_IncludesParameters()
{
var indicator = new AoIndicator { FastPeriod = 10, SlowPeriod = 40 };
indicator.Initialize();
Assert.Contains("AO", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void AoIndicator_SourceCodeLink_IsValid()
{
var indicator = new AoIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Ao.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void AoIndicator_Initialize_CreatesInternalAo()
{
var indicator = new AoIndicator { FastPeriod = 5, SlowPeriod = 34 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist (Up and Down)
Assert.Equal(2, indicator.LinesSeries.Count);
}
[Fact]
public void AoIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new AoIndicator { FastPeriod = 2, SlowPeriod = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// Need enough bars for Period
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value (either Up or Down)
// One should be NaN, other should be value, or both NaN if cold
double up = indicator.LinesSeries[0].GetValue(0);
double down = indicator.LinesSeries[1].GetValue(0);
Assert.True(double.IsFinite(up) || double.IsFinite(down));
}
[Fact]
public void AoIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new AoIndicator { FastPeriod = 2, SlowPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void AoIndicator_Parameters_CanBeChanged()
{
var indicator = new AoIndicator { FastPeriod = 5, SlowPeriod = 34 };
Assert.Equal(5, indicator.FastPeriod);
Assert.Equal(34, indicator.SlowPeriod);
indicator.FastPeriod = 10;
indicator.SlowPeriod = 40;
Assert.Equal(10, indicator.FastPeriod);
Assert.Equal(40, indicator.SlowPeriod);
Assert.Equal(0, AoIndicator.MinHistoryDepths);
}
}
+79
View File
@@ -0,0 +1,79 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class AoIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Fast Period", sortIndex: 1, 1, 1000, 1, 0)]
public int FastPeriod { get; set; } = 5;
[InputParameter("Slow Period", sortIndex: 2, 1, 1000, 1, 0)]
public int SlowPeriod { get; set; } = 34;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Ao _ao = null!;
private readonly LineSeries _upSeries;
private readonly LineSeries _downSeries;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"AO {FastPeriod}:{SlowPeriod}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/ao/Ao.Quantower.cs";
public AoIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "AO - Awesome Oscillator";
Description = "Momentum indicator measuring market momentum";
_upSeries = new LineSeries(name: "AO Up", color: Color.Green, width: 2, style: LineStyle.Solid);
_downSeries = new LineSeries(name: "AO Down", color: Color.Red, width: 2, style: LineStyle.Solid);
AddLineSeries(_upSeries);
AddLineSeries(_downSeries);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_ao = new Ao(FastPeriod, SlowPeriod);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TValue result = _ao.Update(this.GetInputBar(args), args.IsNewBar());
if (!_ao.IsHot && !ShowColdValues)
return;
double prevAo = double.NaN;
if (Count > 1)
{
prevAo = _upSeries.GetValue(1);
if (double.IsNaN(prevAo))
{
prevAo = _downSeries.GetValue(1);
}
}
if (double.IsNaN(prevAo) || result.Value > prevAo)
{
_upSeries.SetValue(result.Value);
_downSeries.SetValue(double.NaN);
}
else
{
_upSeries.SetValue(double.NaN);
_downSeries.SetValue(result.Value);
}
}
}
+272
View File
@@ -0,0 +1,272 @@
namespace QuanTAlib;
public class AoTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
Assert.True(double.IsFinite(ao.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
ao.Update(bars[i]);
}
// Update with 100th point (isNew=true)
ao.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
var val2 = ao.Update(modifiedBar, false);
// Create new instance and feed up to modified
var ao2 = new Ao(5, 34);
for (int i = 0; i < 99; i++)
{
ao2.Update(bars[i]);
}
var val3 = ao2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
ao.Reset();
Assert.Equal(0, ao.Last.Value);
Assert.False(ao.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
ao.Update(bars[i]);
}
Assert.True(double.IsFinite(ao.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(ao.Update(bars[i]).Value);
}
var ao2 = new Ao(5, 34);
var seriesResults = ao2.Update(bars);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var ao = new Ao(5, 34);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(ao.Update(bars[i]).Value);
}
var staticResults = Ao.Batch(bars, 5, 34);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = ao.Update(bars);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = ao.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ao(0, 34));
Assert.Throws<ArgumentException>(() => new Ao(5, 0));
Assert.Throws<ArgumentException>(() => new Ao(34, 5)); // Fast >= Slow
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var ao = new Ao(5, 34);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 50 new values (more than slow period)
TBar fiftiethInput = default;
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
fiftiethInput = bar;
ao.Update(bar, isNew: true);
}
// Remember state after 50 values
double stateAfterFifty = ao.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
ao.Update(bar, isNew: false);
}
// Feed the remembered 50th input again with isNew=false
TValue finalResult = ao.Update(fiftiethInput, isNew: false);
// State should match the original state after 50 values
Assert.Equal(stateAfterFifty, finalResult.Value, 1e-10);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
Assert.False(ao.IsHot);
// Feed bars until IsHot becomes true
int count = 0;
while (!ao.IsHot && count < 100)
{
var bar = gbm.Next(isNew: true);
ao.Update(bar, isNew: true);
count++;
}
Assert.True(ao.IsHot);
Assert.True(count >= 34); // Should take at least slow period bars
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 40; i++)
{
ao.Update(bars[i]);
}
// Create a bar with NaN values
var nanBar = new TBar(DateTime.UtcNow, double.NaN, double.NaN, double.NaN, double.NaN, double.NaN);
var result = ao.Update(nanBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var ao = new Ao(5, 34);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 40; i++)
{
ao.Update(bars[i]);
}
// Create a bar with Infinity values
var infBar = new TBar(DateTime.UtcNow, double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity, double.PositiveInfinity);
var result = ao.Update(infBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void AllModes_ProduceSameResult()
{
// Arrange
const int fastPeriod = 5;
int slowPeriod = 34;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 1. Batch Mode (static method)
var batchSeries = Ao.Batch(bars, fastPeriod, slowPeriod);
double expected = batchSeries.Last.Value;
// 2. Streaming Mode (instance, one bar at a time)
var streamingInd = new Ao(fastPeriod, slowPeriod);
for (int i = 0; i < bars.Count; i++)
{
streamingInd.Update(bars[i]);
}
double streamingResult = streamingInd.Last.Value;
// 3. Instance Update with TBarSeries
var instanceInd = new Ao(fastPeriod, slowPeriod);
var instanceResult = instanceInd.Update(bars);
double instanceValue = instanceResult.Last.Value;
// Assert all modes produce identical results
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, instanceValue, precision: 9);
}
}
+117
View File
@@ -0,0 +1,117 @@
using Skender.Stock.Indicators;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using QuanTAlib.Tests;
namespace QuanTAlib;
public sealed class AoValidationTests : IDisposable
{
private readonly ValidationTestData _data;
public AoValidationTests()
{
_data = new ValidationTestData();
}
public void Dispose()
{
_data.Dispose();
}
[Fact]
public void MatchesSkender()
{
var ao = new Ao(5, 34);
var results = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = ao.Update(_data.Bars[i]);
results.Add(res.Value);
}
var skenderResults = _data.SkenderQuotes.GetAwesome(5, 34).ToList();
Assert.Equal(_data.Bars.Count, skenderResults.Count);
for (int i = 0; i < _data.Bars.Count; i++)
{
// Skender returns null for warmup
if (skenderResults[i].Oscillator == null)
{
continue;
}
Assert.Equal((double)skenderResults[i].Oscillator!, results[i], ValidationHelper.SkenderTolerance);
}
}
[Fact]
public void MatchesTulip()
{
var ao = new Ao(5, 34);
var results = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = ao.Update(_data.Bars[i]);
results.Add(res.Value);
}
var high = _data.Bars.High.Select(x => x.Value).ToArray();
var low = _data.Bars.Low.Select(x => x.Value).ToArray();
var tulipIndicator = Tulip.Indicators.ao;
double[][] inputs = { high, low };
double[] options = Array.Empty<double>();
const int lookback = 33;
double[][] outputs = [new double[_data.Bars.Count - lookback]];
tulipIndicator.Run(inputs, options, outputs);
var tulipResults = outputs[0];
for (int i = 0; i < tulipResults.Length; i++)
{
Assert.Equal(tulipResults[i], results[i + lookback], ValidationHelper.TulipTolerance);
}
}
[Fact]
public void MatchesOoples()
{
var ao = new Ao(5, 34);
var results = new List<double>();
for (int i = 0; i < _data.Bars.Count; i++)
{
var res = ao.Update(_data.Bars[i]);
results.Add(res.Value);
}
var ooplesData = _data.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Open = (double)q.Open,
High = (double)q.High,
Low = (double)q.Low,
Close = (double)q.Close,
Volume = (double)q.Volume
}).ToList();
var stockData = new StockData(ooplesData);
var oResult = stockData.CalculateAwesomeOscillator(fastLength: 5, slowLength: 34);
var oValues = oResult.OutputValues["Ao"];
Assert.Equal(_data.Bars.Count, oValues.Count);
for (int i = 0; i < _data.Bars.Count; i++)
{
// Ooples might return 0 for warmup
if (i < 33) continue; // Skip warmup
Assert.Equal(oValues[i], results[i], ValidationHelper.OoplesTolerance);
}
}
}
+267
View File
@@ -0,0 +1,267 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// AO: Awesome Oscillator
/// </summary>
/// <remarks>
/// The Awesome Oscillator (AO) is a momentum indicator used to measure market momentum.
/// It calculates the difference between a 5-period and 34-period Simple Moving Average (SMA)
/// of the median prices (High + Low) / 2.
///
/// Calculation:
/// Median Price = (High + Low) / 2
/// AO = SMA(Median Price, 5) - SMA(Median Price, 34)
///
/// Sources:
/// https://www.investopedia.com/terms/a/awesomeoscillator.asp
/// https://www.tradingview.com/support/solutions/43000501826-awesome-oscillator-ao/
/// </remarks>
[SkipLocalsInit]
public sealed class Ao : ITValuePublisher
{
private readonly int _fastPeriod;
private readonly int _slowPeriod;
private readonly Sma _smaFast;
private readonly Sma _smaSlow;
private TValue _p_Last;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current AO value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the AO has enough data to produce valid results.
/// </summary>
public bool IsHot => _smaSlow.IsHot;
/// <summary>
/// The number of bars required to warm up the indicator.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates AO with specified periods.
/// </summary>
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
public Ao(int fastPeriod = 5, int slowPeriod = 34)
{
if (fastPeriod <= 0)
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
if (slowPeriod <= 0)
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
if (fastPeriod >= slowPeriod)
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
_fastPeriod = fastPeriod;
_slowPeriod = slowPeriod;
_smaFast = new Sma(fastPeriod);
_smaSlow = new Sma(slowPeriod);
WarmupPeriod = slowPeriod;
Name = $"Ao({fastPeriod},{slowPeriod})";
}
/// <summary>
/// Resets the AO state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_smaFast.Reset();
_smaSlow.Reset();
Last = default;
_p_Last = default;
}
/// <summary>
/// Updates the AO with a new bar.
/// </summary>
/// <param name="input">The new bar data</param>
/// <param name="isNew">Whether this is a new bar or an update to the last bar</param>
/// <returns>The updated AO value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
double medianPrice = (input.High + input.Low) * 0.5;
var val = new TValue(input.Time, medianPrice);
// Save state for potential rollback
if (isNew)
{
_p_Last = Last;
}
else
{
// Rollback to previous state - SMAs handle their own rollback
Last = _p_Last;
}
var sFast = _smaFast.Update(val, isNew);
var sSlow = _smaSlow.Update(val, isNew);
double ao = sFast.Value - sSlow.Value;
Last = new TValue(input.Time, ao);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates the AO with a new value (assumes value is Median Price).
/// </summary>
/// <param name="input">The new value</param>
/// <param name="isNew">Whether this is a new value or an update to the last value</param>
/// <returns>The updated AO value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
// Guard against non-finite input
if (!double.IsFinite(input.Value))
{
// Keep Last unchanged, publish with IsNew=false to indicate no state change
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = false });
return Last;
}
// Save state for potential rollback
if (isNew)
{
_p_Last = Last;
}
else
{
// Rollback to previous state - SMAs handle their own rollback
Last = _p_Last;
}
var sFast = _smaFast.Update(input, isNew);
var sSlow = _smaSlow.Update(input, isNew);
double ao = sFast.Value - sSlow.Value;
Last = new TValue(input.Time, ao);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates the AO with a series of bars.
/// </summary>
/// <param name="source">The source series of bars</param>
/// <returns>The AO series</returns>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, v, _fastPeriod, _slowPeriod);
// Bulk copy timestamps using CollectionsMarshal
var tList = new List<long>(len);
CollectionsMarshal.SetCount(tList, len);
var tSpan = CollectionsMarshal.AsSpan(tList);
source.Open.Times.CopyTo(tSpan);
var vList = new List<double>(len);
CollectionsMarshal.SetCount(vList, len);
var vSpan = CollectionsMarshal.AsSpan(vList);
v.AsSpan().CopyTo(vSpan);
// Restore streaming state so the instance is hot after batch update
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i], isNew: true);
}
return new TSeries(tList, vList);
}
/// <summary>
/// Calculates AO over OHLC spans into a preallocated output span.
/// Median price is computed as (High + Low) / 2.
/// </summary>
/// <param name="high">High prices</param>
/// <param name="low">Low prices</param>
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
/// <param name="destination">Output AO values</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> high, ReadOnlySpan<double> low, Span<double> destination, int fastPeriod = 5, int slowPeriod = 34)
{
if (high.Length != low.Length || high.Length != destination.Length)
throw new ArgumentException("High, low, and destination spans must have the same length.", nameof(destination));
int len = high.Length;
if (len == 0) return;
// Always use pooled buffer to avoid CS8353 stackalloc escape issues
// For small sizes, ArrayPool overhead is minimal
double[] rentedBuffer = ArrayPool<double>.Shared.Rent(len * 3);
try
{
Span<double> median = rentedBuffer.AsSpan(0, len);
Span<double> fast = rentedBuffer.AsSpan(len, len);
Span<double> slow = rentedBuffer.AsSpan(len * 2, len);
for (int i = 0; i < len; i++)
{
median[i] = (high[i] + low[i]) * 0.5;
}
Sma.Batch(median, fast, fastPeriod);
Sma.Batch(median, slow, slowPeriod);
SimdExtensions.Subtract(fast, slow, destination);
}
finally
{
ArrayPool<double>.Shared.Return(rentedBuffer);
}
}
/// <summary>
/// Calculates AO for the entire series using a stateless batch path.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="fastPeriod">Fast SMA period (default 5)</param>
/// <param name="slowPeriod">Slow SMA period (default 34)</param>
/// <returns>AO series</returns>
public static TSeries Batch(TBarSeries source, int fastPeriod = 5, int slowPeriod = 34)
{
if (source.Count == 0) return new TSeries([], []);
int len = source.Count;
var v = new double[len];
Calculate(source.High.Values, source.Low.Values, v, fastPeriod, slowPeriod);
// Bulk copy timestamps using CollectionsMarshal
var tList = new List<long>(len);
CollectionsMarshal.SetCount(tList, len);
var tSpan = CollectionsMarshal.AsSpan(tList);
source.Open.Times.CopyTo(tSpan);
// Pass values list directly, avoiding spread operator allocation
var vList = new List<double>(len);
CollectionsMarshal.SetCount(vList, len);
var vSpan = CollectionsMarshal.AsSpan(vList);
v.AsSpan().CopyTo(vSpan);
return new TSeries(tList, vList);
}
}
+71
View File
@@ -0,0 +1,71 @@
# AO: Awesome Oscillator
> "Awesome" is a marketing term. The math is just a moving average crossover. But sometimes, simple is all you need.
The Awesome Oscillator (AO) is a momentum indicator that strips away the noise of closing prices to reveal the market's immediate velocity compared to its broader trend. It quantifies the gap between short-term and long-term market consensus using median prices, effectively serving as a non-lagging confirmation of trend direction.
## Historical Context
Bill Williams introduced the AO in *Trading Chaos* (1995). He argued that standard indicators fixated on closing prices missed the volatility that happens *during* the bar. By focusing on the median price, AO attempts to reflect the market's "balance point" rather than just its finish line.
It is a core component of the Williams Trading System, often used in conjunction with the Alligator indicator to confirm trend entries.
## Architecture & Physics
The AO is architecturally simple: it is the difference between two Simple Moving Averages (SMA) of the Median Price.
1. **Median Price**: The midpoint of the trading range is calculated: $(High + Low) / 2$.
2. **Smoothing**: These midpoints are smoothed over two distinct timeframes (Fast and Slow).
3. **Differential**: The slow average is subtracted from the fast average.
### Why Median Price?
Using `(High + Low) / 2` instead of `Close` is a deliberate architectural choice. It filters out the noise of the "last second" trades that determine the close, focusing instead on the center of gravity for the entire period. This makes AO less susceptible to manipulation or anomalies at the bell.
## Mathematical Foundation
The math is elegant in its simplicity.
$$ \text{Median Price}_t = \frac{H_t + L_t}{2} $$
$$ AO_t = SMA(\text{Median Price}, n_{fast}) - SMA(\text{Median Price}, n_{slow}) $$
Where:
* $n_{fast}$ is the fast period (default 5).
* $n_{slow}$ is the slow period (default 34).
## Performance Profile
The AO is lightweight and suitable for high-frequency applications.
### Zero-Allocation Design
The implementation uses `stackalloc` for internal buffers when processing spans, ensuring no heap allocations occur during the calculation. The hot path for streaming updates is purely scalar and allocation-free.
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | 2ns | 2ns / bar (Apple M1 Max). |
| **Allocations** | 0 | Hot path is allocation-free. |
| **Complexity** | O(1) | Constant time updates. |
| **Accuracy** | 10/10 | Matches standard implementations. |
| **Timeliness** | 6/10 | Lags due to SMA smoothing. |
| **Overshoot** | 8/10 | Can overshoot in volatile markets. |
| **Smoothness** | 6/10 | Smoother than raw price, but reactive. |
## Validation
Validation is performed against industry-standard libraries.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Validated. |
| **Skender** | ✅ | Matches `GetAwesome`. |
| **Tulip** | ✅ | Matches `ti.ao`. |
| **Ooples** | ✅ | Matches `CalculateAwesomeOscillator`. |
| **TA-Lib** | N/A | Not implemented in TA-Lib. |
### Common Pitfalls
* **The "Awesome" Misnomer**: Do not let the name fool you. It is a lagging indicator (it uses SMAs). It confirms trends; it does not predict them.
* **Twin Peaks**: The "Twin Peaks" signal is often cited but rarely backtested successfully in isolation. It requires trend confirmation (e.g., via the Alligator).
+121
View File
@@ -0,0 +1,121 @@
using TradingPlatform.BusinessLayer;
using QuanTAlib;
namespace QuanTAlib.Tests;
public class ApoIndicatorTests
{
[Fact]
public void ApoIndicator_Constructor_SetsDefaults()
{
var indicator = new ApoIndicator();
Assert.Equal(12, indicator.FastPeriod);
Assert.Equal(26, indicator.SlowPeriod);
Assert.True(indicator.ShowColdValues);
Assert.Equal("APO - Absolute Price Oscillator", indicator.Name);
Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround);
}
[Fact]
public void ApoIndicator_MinHistoryDepths_EqualsZero()
{
var indicator = new ApoIndicator { SlowPeriod = 20 };
Assert.Equal(0, ApoIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void ApoIndicator_ShortName_IncludesParameters()
{
var indicator = new ApoIndicator { FastPeriod = 10, SlowPeriod = 40 };
indicator.Initialize();
Assert.Contains("APO", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("10", indicator.ShortName, StringComparison.Ordinal);
Assert.Contains("40", indicator.ShortName, StringComparison.Ordinal);
}
[Fact]
public void ApoIndicator_SourceCodeLink_IsValid()
{
var indicator = new ApoIndicator();
Assert.Contains("github.com", indicator.SourceCodeLink, StringComparison.Ordinal);
Assert.Contains("Apo.Quantower.cs", indicator.SourceCodeLink, StringComparison.Ordinal);
}
[Fact]
public void ApoIndicator_Initialize_CreatesInternalApo()
{
var indicator = new ApoIndicator { FastPeriod = 5, SlowPeriod = 34 };
// Initialize should not throw
indicator.Initialize();
// After init, line series should exist
Assert.Single(indicator.LinesSeries);
}
[Fact]
public void ApoIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
{
var indicator = new ApoIndicator { FastPeriod = 2, SlowPeriod = 5 };
indicator.Initialize();
// Add historical data
var now = DateTime.UtcNow;
// Need enough bars for Period
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
// Process update for each bar to simulate history loading
var args = new UpdateArgs(UpdateReason.HistoricalBar);
indicator.ProcessUpdate(args);
}
// Line series should have a value
double val = indicator.LinesSeries[0].GetValue(0);
Assert.True(double.IsFinite(val));
}
[Fact]
public void ApoIndicator_ProcessUpdate_NewBar_ComputesValue()
{
var indicator = new ApoIndicator { FastPeriod = 2, SlowPeriod = 5 };
indicator.Initialize();
var now = DateTime.UtcNow;
for (int i = 0; i < 20; i++)
{
indicator.HistoricalData.AddBar(now.AddMinutes(i), 100 + i, 110 + i, 90 + i, 105 + i);
}
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
// Add new bar
indicator.HistoricalData.AddBar(now.AddMinutes(20), 120, 130, 110, 125);
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
Assert.Equal(2, indicator.LinesSeries[0].Count);
}
[Fact]
public void ApoIndicator_Parameters_CanBeChanged()
{
var indicator = new ApoIndicator { FastPeriod = 5, SlowPeriod = 34 };
Assert.Equal(5, indicator.FastPeriod);
Assert.Equal(34, indicator.SlowPeriod);
indicator.FastPeriod = 10;
indicator.SlowPeriod = 40;
Assert.Equal(10, indicator.FastPeriod);
Assert.Equal(40, indicator.SlowPeriod);
Assert.Equal(0, ApoIndicator.MinHistoryDepths);
}
}
+53
View File
@@ -0,0 +1,53 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class ApoIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Fast Period", sortIndex: 1, 1, 1000, 1, 0)]
public int FastPeriod { get; set; } = 12;
[InputParameter("Slow Period", sortIndex: 2, 1, 1000, 1, 0)]
public int SlowPeriod { get; set; } = 26;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Apo _apo = null!;
private readonly LineSeries _series;
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"APO {FastPeriod}:{SlowPeriod}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/momentum/apo/Apo.Quantower.cs";
public ApoIndicator()
{
OnBackGround = true;
SeparateWindow = true;
Name = "APO - Absolute Price Oscillator";
Description = "Momentum indicator showing the difference between two EMAs";
_series = new LineSeries(name: "APO", color: Color.Orange, width: 2, style: LineStyle.Solid);
AddLineSeries(_series);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnInit()
{
_apo = new Apo(FastPeriod, SlowPeriod);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
TValue result = _apo.Update(this.GetInputBar(args), args.IsNewBar());
_series.SetValue(result.Value, _apo.IsHot, ShowColdValues);
}
}
+273
View File
@@ -0,0 +1,273 @@
namespace QuanTAlib;
public class ApoTests
{
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
apo.Update(bars[i]);
}
Assert.True(double.IsFinite(apo.Last.Value));
}
[Fact]
public void IsNew_Consistency()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
apo.Update(bars[i]);
}
// Update with 100th point (isNew=true)
apo.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 1.0, bars[99].Low - 1.0, bars[99].Close, bars[99].Volume);
var val2 = apo.Update(modifiedBar, false);
// Create new instance and feed up to modified
var apo2 = new Apo(12, 26);
for (int i = 0; i < 99; i++)
{
apo2.Update(bars[i]);
}
var val3 = apo2.Update(modifiedBar, true);
Assert.Equal(val3.Value, val2.Value, 1e-9);
}
[Fact]
public void Reset_Works()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
for (int i = 0; i < bars.Count; i++)
{
apo.Update(bars[i]);
}
apo.Reset();
Assert.Equal(0, apo.Last.Value);
Assert.False(apo.IsHot);
// Feed again
for (int i = 0; i < bars.Count; i++)
{
apo.Update(bars[i]);
}
Assert.True(double.IsFinite(apo.Last.Value));
}
[Fact]
public void TBarSeries_Update_Matches_Streaming()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(apo.Update(bars[i]).Value);
}
var apo2 = new Apo(12, 26);
var seriesResults = apo2.Update(bars.Close);
Assert.Equal(streamingResults.Count, seriesResults.Count);
for (int i = 0; i < seriesResults.Count; i++)
{
Assert.Equal(streamingResults[i], seriesResults.Values[i], 1e-9);
}
}
[Fact]
public void StaticCalculate_Matches_Streaming()
{
var gbm = new GBM();
var bars = gbm.Fetch(200, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var apo = new Apo(12, 26);
var streamingResults = new List<double>();
for (int i = 0; i < bars.Count; i++)
{
streamingResults.Add(apo.Update(bars[i]).Value);
}
var staticResults = Apo.Batch(bars.Close, 12, 26);
Assert.Equal(streamingResults.Count, staticResults.Count);
for (int i = 0; i < staticResults.Count; i++)
{
Assert.Equal(streamingResults[i], staticResults.Values[i], 1e-9);
}
}
[Fact]
public void Chainability_Works()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(10, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Test TBarSeries chain
var result = apo.Update(bars.Close);
Assert.NotNull(result);
Assert.IsType<TSeries>(result);
// Test TBar chain (returns TValue)
var result2 = apo.Update(bars[0]);
Assert.IsType<TValue>(result2);
}
[Fact]
public void Constructor_InvalidParameters_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Apo(0, 26));
Assert.Throws<ArgumentException>(() => new Apo(12, 0));
Assert.Throws<ArgumentException>(() => new Apo(26, 12)); // Fast >= Slow
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var apo = new Apo(12, 26);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
// Feed 50 new values (more than slow period)
TBar fiftiethInput = default;
for (int i = 0; i < 50; i++)
{
var bar = gbm.Next(isNew: true);
fiftiethInput = bar;
apo.Update(bar, isNew: true);
}
// Remember state after 50 values
double stateAfterFifty = apo.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 0; i < 9; i++)
{
var bar = gbm.Next(isNew: false);
apo.Update(bar, isNew: false);
}
// Feed the remembered 50th input again with isNew=false
TValue finalResult = apo.Update(fiftiethInput, isNew: false);
// State should match the original state after 50 values
Assert.Equal(stateAfterFifty, finalResult.Value, 1e-10);
}
[Fact]
public void IsHot_BecomesTrueWhenBufferFull()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
Assert.False(apo.IsHot);
// Feed bars until IsHot becomes true
int count = 0;
while (!apo.IsHot && count < 100)
{
var bar = gbm.Next(isNew: true);
apo.Update(bar, isNew: true);
count++;
}
Assert.True(apo.IsHot);
Assert.True(count >= 26); // Should take at least slow period bars
}
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 40; i++)
{
apo.Update(bars[i]);
}
// Create a bar with NaN close value
var nanBar = new TBar(DateTime.UtcNow, 100, 105, 95, double.NaN, 1000);
var result = apo.Update(nanBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var apo = new Apo(12, 26);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed some valid bars first
for (int i = 0; i < 40; i++)
{
apo.Update(bars[i]);
}
// Create a bar with Infinity close value
var infBar = new TBar(DateTime.UtcNow, 100, 105, 95, double.PositiveInfinity, 1000);
var result = apo.Update(infBar);
// Should not crash and should return a finite value
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void AllModes_ProduceSameResult()
{
// Arrange
const int fastPeriod = 12;
int slowPeriod = 26;
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var closeSeries = bars.Close;
// 1. Batch Mode (static method)
var batchSeries = Apo.Batch(closeSeries, fastPeriod, slowPeriod);
double expected = batchSeries.Last.Value;
// 2. Streaming Mode (instance, one bar at a time)
var streamingInd = new Apo(fastPeriod, slowPeriod);
for (int i = 0; i < bars.Count; i++)
{
streamingInd.Update(bars[i]);
}
double streamingResult = streamingInd.Last.Value;
// 3. Instance Update with TSeries
var instanceInd = new Apo(fastPeriod, slowPeriod);
var instanceResult = instanceInd.Update(closeSeries);
double instanceValue = instanceResult.Last.Value;
// Assert all modes produce identical results
Assert.Equal(expected, streamingResult, precision: 9);
Assert.Equal(expected, instanceValue, precision: 9);
}
}
+149
View File
@@ -0,0 +1,149 @@
using QuanTAlib.Tests;
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using OoplesFinance.StockIndicators.Enums;
namespace QuanTAlib;
public sealed class ApoValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private bool _disposed;
public ApoValidationTests()
{
_testData = new ValidationTestData(); // Default 5000 bars
}
public void Dispose()
{
Dispose(true);
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
_disposed = true;
if (disposing)
{
_testData?.Dispose();
}
}
[Fact]
public void Validate_Against_TALib_Apo()
{
const int fastPeriod = 12;
int slowPeriod = 26;
double[] input = _testData.Data.Values.ToArray();
double[] output = new double[input.Length];
// TA-Lib APO: double[] inReal, int optInFastPeriod, int optInSlowPeriod, int optInMAType
// MAType 1 = EMA
var retCode = TALib.Functions.Apo<double>(input, 0..^0, output, out var outRange, fastPeriod, slowPeriod, TALib.Core.MAType.Ema);
Assert.Equal(TALib.Core.RetCode.Success, retCode);
// 1. Batch Mode
var apo = new Apo(fastPeriod, slowPeriod);
var result = apo.Update(_testData.Data);
ValidationHelper.VerifyData(result, output, outRange, lookback: slowPeriod - 1);
// 2. Streaming Mode
var apoStream = new Apo(fastPeriod, slowPeriod);
var streamResults = new List<double>();
foreach (var item in _testData.Data)
{
streamResults.Add(apoStream.Update(item).Value);
}
ValidationHelper.VerifyData(streamResults, output, outRange, lookback: slowPeriod - 1);
// 3. Span Mode
double[] spanOutput = new double[input.Length];
Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod);
ValidationHelper.VerifyData(spanOutput, output, outRange, lookback: slowPeriod - 1);
}
[Fact]
public void Validate_Against_Tulip_Apo()
{
// Tulip APO uses standard EMA initialization (first value), while QuanTAlib uses
// compensated EMA initialization (zero-based). They converge after sufficient periods.
// With 5000 bars, the tail (last 100) should match closely.
int fastPeriod = 12;
int slowPeriod = 26;
double[] input = _testData.Data.Values.ToArray();
var apoIndicator = Tulip.Indicators.apo;
double[][] inputs = { input };
double[] options = { fastPeriod, slowPeriod };
double[][] outputs = { new double[input.Length - 1] }; // Tulip APO starts at 1
apoIndicator.Run(inputs, options, outputs);
double[] output = outputs[0];
// 1. Batch Mode
var apo = new Apo(fastPeriod, slowPeriod);
var result = apo.Update(_testData.Data);
ValidationHelper.VerifyData(result, output, lookback: 1);
// 2. Streaming Mode
var apoStream = new Apo(fastPeriod, slowPeriod);
var streamResults = new List<double>();
foreach (var item in _testData.Data)
{
streamResults.Add(apoStream.Update(item).Value);
}
ValidationHelper.VerifyData(streamResults, output, lookback: 1);
// 3. Span Mode
double[] spanOutput = new double[input.Length];
Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod);
ValidationHelper.VerifyData(spanOutput, output, lookback: 1);
}
[Fact]
public void Validate_Against_Ooples_Apo()
{
int fastPeriod = 12;
int slowPeriod = 26;
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Open = (double)q.Open,
High = (double)q.High,
Low = (double)q.Low,
Close = (double)q.Close,
Volume = (double)q.Volume
}).ToList();
var stockData = new StockData(ooplesData);
var results = stockData.CalculateAbsolutePriceOscillator(MovingAvgType.ExponentialMovingAverage, fastPeriod, slowPeriod);
var output = results.OutputValues["Apo"].ToArray();
// 1. Batch Mode
var apo = new Apo(fastPeriod, slowPeriod);
var result = apo.Update(_testData.Data);
ValidationHelper.VerifyData(result, output, lookback: 0, tolerance: ValidationHelper.OoplesTolerance);
// 2. Streaming Mode
var apoStream = new Apo(fastPeriod, slowPeriod);
var streamResults = new List<double>();
foreach (var item in _testData.Data)
{
streamResults.Add(apoStream.Update(item).Value);
}
ValidationHelper.VerifyData(streamResults, output, lookback: 0, tolerance: ValidationHelper.OoplesTolerance);
// 3. Span Mode
double[] input = _testData.Data.Values.ToArray();
double[] spanOutput = new double[input.Length];
Apo.Calculate(input.AsSpan(), spanOutput.AsSpan(), fastPeriod, slowPeriod);
ValidationHelper.VerifyData(spanOutput, output, lookback: 0, tolerance: ValidationHelper.OoplesTolerance);
}
}
+187
View File
@@ -0,0 +1,187 @@
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// APO: Absolute Price Oscillator
/// </summary>
/// <remarks>
/// The Absolute Price Oscillator (APO) is a momentum indicator that shows the difference
/// between two Exponential Moving Averages (EMAs) of a security's price.
///
/// Calculation:
/// APO = FastEMA(Price) - SlowEMA(Price)
///
/// Standard Parameters:
/// Fast Period: 12
/// Slow Period: 26
/// Source: Close price
///
/// Sources:
/// https://www.investopedia.com/terms/a/apo.asp
/// https://school.stockcharts.com/doku.php?id=technical_indicators:price_oscillators_ppo
/// </remarks>
[SkipLocalsInit]
public sealed class Apo : ITValuePublisher
{
private readonly Ema _emaFast;
private readonly Ema _emaSlow;
private readonly TValuePublishedHandler _handler;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current APO value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// True if the APO has enough data to produce valid results.
/// </summary>
public bool IsHot => _emaSlow.IsHot;
/// <summary>
/// The number of bars required to warm up the indicator.
/// </summary>
public int WarmupPeriod { get; }
/// <summary>
/// Creates APO with specified periods.
/// </summary>
/// <param name="fastPeriod">Fast EMA period (default 12)</param>
/// <param name="slowPeriod">Slow EMA period (default 26)</param>
public Apo(int fastPeriod = 12, int slowPeriod = 26)
{
if (fastPeriod <= 0)
throw new ArgumentException("Fast period must be greater than 0", nameof(fastPeriod));
if (slowPeriod <= 0)
throw new ArgumentException("Slow period must be greater than 0", nameof(slowPeriod));
if (fastPeriod >= slowPeriod)
throw new ArgumentException("Fast period must be less than slow period", nameof(fastPeriod));
_emaFast = new Ema(fastPeriod);
_emaSlow = new Ema(slowPeriod);
_handler = Handle;
WarmupPeriod = slowPeriod;
Name = $"Apo({fastPeriod},{slowPeriod})";
}
/// <summary>
/// Creates APO with specified source and periods.
/// </summary>
/// <param name="source">Source to subscribe to</param>
/// <param name="fastPeriod">Fast EMA period (default 12)</param>
/// <param name="slowPeriod">Slow EMA period (default 26)</param>
public Apo(ITValuePublisher source, int fastPeriod = 12, int slowPeriod = 26) : this(fastPeriod, slowPeriod)
{
source.Pub += _handler;
}
/// <summary>
/// Resets the APO state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_emaFast.Reset();
_emaSlow.Reset();
Last = default;
}
/// <summary>
/// Updates the APO with a new value.
/// </summary>
/// <param name="input">The new value</param>
/// <param name="isNew">Whether this is a new value or an update to the last value</param>
/// <returns>The updated APO value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
var eFast = _emaFast.Update(input, isNew);
var eSlow = _emaSlow.Update(input, isNew);
double apo = eFast.Value - eSlow.Value;
Last = new TValue(input.Time, apo);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates the APO with a new bar (uses Close price).
/// </summary>
/// <param name="input">The new bar data</param>
/// <param name="isNew">Whether this is a new bar or an update to the last bar</param>
/// <returns>The updated APO value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
return Update(new TValue(input.Time, input.Close), isNew);
}
/// <summary>
/// Updates the APO with a series of values.
/// </summary>
/// <param name="source">The source series of values</param>
/// <returns>The APO series</returns>
public TSeries Update(TSeries source)
{
var t = new List<long>(source.Count);
var v = new List<double>(source.Count);
Reset();
for (int i = 0; i < source.Count; i++)
{
var val = Update(source[i], isNew: true);
t.Add(val.Time);
v.Add(val.Value);
}
return new TSeries(t, v);
}
private void Handle(object? sender, in TValueEventArgs args)
{
Update(args.Value, args.IsNew);
}
/// <summary>
/// Calculates APO for the entire series using a new instance.
/// </summary>
/// <param name="source">Input series</param>
/// <param name="fastPeriod">Fast EMA period (default 12)</param>
/// <param name="slowPeriod">Slow EMA period (default 26)</param>
/// <returns>APO series</returns>
public static TSeries Batch(TSeries source, int fastPeriod = 12, int slowPeriod = 26)
{
var apo = new Apo(fastPeriod, slowPeriod);
return apo.Update(source);
}
/// <summary>
/// Calculates APO for the entire span.
/// </summary>
/// <param name="source">Input span</param>
/// <param name="output">Output span</param>
/// <param name="fastPeriod">Fast EMA period (default 12)</param>
/// <param name="slowPeriod">Slow EMA period (default 26)</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int fastPeriod = 12, int slowPeriod = 26)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output spans must be of the same length.", nameof(output));
Span<double> fastEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Span<double> slowEma = source.Length <= 1024 ? stackalloc double[source.Length] : new double[source.Length];
Ema.Batch(source, fastEma, fastPeriod);
Ema.Batch(source, slowEma, slowPeriod);
SimdExtensions.Subtract(fastEma, slowEma, output);
}
}
+73
View File
@@ -0,0 +1,73 @@
# APO: Absolute Price Oscillator
> Percentages are for analysts. Traders pay bills in cash. APO tells you the cash value of the trend.
The Absolute Price Oscillator (APO) measures the raw currency difference between two exponential moving averages. Unlike its percentage-based cousin (PPO), APO speaks in dollars and cents, making it the preferred tool for spread traders, arbitrageurs, and anyone whose P&L is denominated in currency rather than basis points.
## Historical Context
A \$5 move on a \$100 stock (5%) feels different than a \$5 move on a \$20 stock (25%), but to a spread trader balancing a hedge, \$5 is \$5. Percentage oscillators distort this reality.
APO strips away the normalization. It simply asks: "How far is the fast trend from the slow trend in absolute terms?" This provides a direct read on the cash momentum of the asset.
## Architecture & Physics
APO is built on the foundation of the high-performance QuanTAlib `Ema` kernel. It inherits the $O(1)$ computational complexity and zero-allocation characteristics of the underlying moving averages.
1. **Dual EMA Engine**: Two independent Exponential Moving Averages (Fast and Slow) are maintained.
2. **Differential**: The arithmetic difference between them is computed.
3. **SIMD Acceleration**: For batch processing, hardware intrinsics are used to perform the subtraction across the entire dataset in parallel.
### Computational Efficiency
The EMAs are not recalculated from scratch. The state of both the fast and slow EMAs is maintained, allowing the APO update to be computed in constant time, regardless of the lookback period.
* **Time Complexity**: $O(1)$ per update.
* **Space Complexity**: $O(1)$ (two EMA state structs).
* **Allocations**: 0 bytes on the hot path.
## Mathematical Foundation
The formula is the definition of simplicity.
$$ APO_t = EMA(P, n_{fast}) - EMA(P, n_{slow}) $$
Where:
* $EMA$ is the recursive Exponential Moving Average.
* $n_{fast}$ is the fast period (default 12).
* $n_{slow}$ is the slow period (default 26).
## Performance Profile
APO performance is effectively the sum of two EMA calculations plus a subtraction.
### Zero-Allocation Design
The implementation uses `stackalloc` for internal buffers when processing spans, ensuring no heap allocations occur during the calculation. The hot path for streaming updates is purely scalar and allocation-free.
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | 15ns | 15ns / bar (Apple M1 Max). |
| **Allocations** | 0 | Hot path is allocation-free. |
| **Complexity** | O(1) | Constant time updates. |
| **Accuracy** | 10/10 | Matches TA-Lib to 1e-9. |
| **Timeliness** | 6/10 | Lags due to EMA smoothing. |
| **Overshoot** | 8/10 | Can overshoot in volatile markets. |
| **Smoothness** | 6/10 | Smoother than raw price. |
## Validation
Validation is performed against industry-standard libraries.
| Library | Status | Notes |
| :--- | :--- | :--- |
| **TA-Lib** | ✅ | Matches `APO` with `MAType.Ema`. |
| **Tulip** | ✅ | Matches `ti.apo`. |
| **Ooples** | ✅ | Matches `CalculateAbsolutePriceOscillator`. |
| **Skender** | N/A | Not implemented in Skender. |
### Common Pitfalls
* **Scale Sensitivity**: APO values are not normalized. An APO of 10.0 on Bitcoin is noise; on EUR/USD, it's a catastrophe. Use PPO for cross-asset comparisons.
* **Lag**: As a derivative of moving averages, APO lags price. The lag is a function of the slow period.
+85
View File
@@ -0,0 +1,85 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Bollinger %B", "BBB", overlay=false)
//@function Calculates Bollinger Bands components for %B calculation
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/bbb.md
//@param source Series to calculate from
//@param period Lookback period for SMA and standard deviation
//@param multiplier Standard deviation multiplier for band width
//@returns Bollinger %B value (typically 0-1 range; can overshoot)
//@optimized Uses circular buffer with running sums, O(1) complexity per bar
bbb(series float source, simple int period, simple float multiplier) =>
if period <= 0 or multiplier <= 0.0
runtime.error("Period and multiplier must be greater than 0")
var int p = 0
var int head = 0
var int count = 0
var array<float> buffer = array.new_float(0)
var float sum = 0.0
var float sumSq = 0.0
var string lastSymbol = ""
var string lastTimeframe = ""
string currentSymbol = syminfo.tickerid
string currentTimeframe = timeframe.period
bool needsReset = (p != period) or (currentSymbol != lastSymbol) or (currentTimeframe != lastTimeframe)
if needsReset
p := period
head := 0
count := 0
buffer := array.new_float(p, na)
sum := 0.0
sumSq := 0.0
lastSymbol := currentSymbol
lastTimeframe := currentTimeframe
float result = na
if not na(source)
float oldest = array.get(buffer, head)
if not na(oldest)
sum -= oldest
sumSq -= oldest * oldest
else
count += 1
sum += source
sumSq += source * source
array.set(buffer, head, source)
head := (head + 1) % p
int n = math.max(1, count)
float basis = sum / n
float variance = math.max(0.0, sumSq / n - basis * basis)
float stddev = math.sqrt(variance)
float dev = multiplier * stddev
float upper = basis + dev
float lower = basis - dev
float bandWidth = upper - lower
result := bandWidth > 0 ? (source - lower) / bandWidth : 0.5
result
// ---------- Main loop ----------
// Inputs
i_period = input.int(20, "Period", minval=1)
i_source = input.source(close, "Source")
i_multiplier = input.float(2.0, "StdDev Multiplier", minval=0.001, step=0.1)
// Calculation
result = bbb(i_source, i_period, i_multiplier)
// Plot
plot(result, "Bollinger %B", color=color.yellow, linewidth=2)
hline(1.0, "Upper Band Level", color=color.gray, linestyle=hline.style_dashed)
hline(0.8, "Overbought", color=color.red, linestyle=hline.style_dotted)
hline(0.5, "Midline", color=color.gray, linestyle=hline.style_solid)
hline(0.2, "Oversold", color=color.green, linestyle=hline.style_dotted)
hline(0.0, "Lower Band Level", color=color.gray, linestyle=hline.style_dashed)
+152
View File
@@ -0,0 +1,152 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Bollinger Band Squeeze (BBS)", "BBS", overlay=true)
//@function Calculates Bollinger Bands for squeeze detection
//@param source Series to calculate from
//@param period Lookback period
//@returns tuple with [middle, deviation] values
bbands_calc(series float source, simple int period) =>
var int p = math.max(1, period)
var int head = 0
var int count = 0
var array<float> buffer = array.new_float(p, na)
var float sum = 0.0
var float sumSq = 0.0
float oldest = array.get(buffer, head)
float current_val = nz(source)
if not na(oldest)
sum -= oldest
sumSq -= oldest * oldest
else
count := math.min(count + 1, p)
sum += current_val
sumSq += current_val * current_val
array.set(buffer, head, current_val)
head := (head + 1) % p
int n = math.max(1, count)
float basis = sum / n
float variance = n > 1 ? math.max(0.0, (sumSq / n) - (basis * basis)) : 0.0
float dev = math.sqrt(variance)
[basis, dev]
//@function Calculates Average True Range for Keltner Channels
//@param period Lookback period for ATR calculation
//@returns ATR value with proper warmup
atr_calc(simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
float tr = math.max(high - low, math.max(math.abs(high - nz(close[1])), math.abs(low - nz(close[1]))))
float alpha = 2.0 / (period + 1)
float beta = 1.0 - alpha
var bool warmup = true
var float e = 1.0
var float atr = 0.0
atr := alpha * tr + beta * atr
float result = tr
if warmup
e *= beta
float c = 1.0 / (1.0 - e)
result := c * atr
warmup := e > 1e-10
else
result := atr
result
//@function Calculates Keltner Channels using SMA and ATR
//@param source Series to calculate from
//@param period Lookback period
//@param atr_mult ATR multiplier for channel width
//@param atr_val Pre-calculated ATR value
//@returns tuple with [middle, upper, lower] channel values
keltner_calc(series float source, simple int period, simple float atr_mult, series float atr_val) =>
var int p = math.max(1, period)
var int head = 0
var int count = 0
var array<float> buffer = array.new_float(p, na)
var float sum = 0.0
float oldest = array.get(buffer, head)
float current_val = nz(source)
if not na(oldest)
sum -= oldest
else
count := math.min(count + 1, p)
sum += current_val
array.set(buffer, head, current_val)
head := (head + 1) % p
int n = math.max(1, count)
float middle = sum / n
float offset = atr_mult * atr_val
[middle, middle + offset, middle - offset]
//@function Detects Bollinger Band Squeeze condition
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/bbs.md
//@param source Series to analyze
//@param bb_period Bollinger Band period
//@param bb_mult Bollinger Band standard deviation multiplier
//@param kc_period Keltner Channel period
//@param kc_mult Keltner Channel ATR multiplier
//@returns tuple with [squeeze_on, bandwidth, bands and channels]
bbs(series float source, simple int bb_period, simple float bb_mult, simple int kc_period, simple float kc_mult) =>
if bb_period <= 0 or kc_period <= 0
runtime.error("Periods must be greater than 0")
if bb_mult <= 0.0 or kc_mult <= 0.0
runtime.error("Multipliers must be greater than 0")
[bb_middle, bb_dev] = bbands_calc(source, bb_period)
float bb_upper = bb_middle + (bb_mult * bb_dev)
float bb_lower = bb_middle - (bb_mult * bb_dev)
float atr_val = atr_calc(kc_period)
[kc_middle, kc_upper, kc_lower] = keltner_calc(source, kc_period, kc_mult, atr_val)
bool squeeze_on = bb_upper < kc_upper and bb_lower > kc_lower
float bandwidth = bb_middle == 0 ? 0 : ((bb_upper - bb_lower) / bb_middle) * 100
[squeeze_on, bandwidth, bb_middle, bb_upper, bb_lower, kc_middle, kc_upper, kc_lower]
// ---------- Main loop ----------
// Inputs
i_bb_period = input.int(20, "Bollinger Band Period", minval=1, maxval=500)
i_bb_mult = input.float(2.0, "BB StdDev Multiplier", minval=0.1, maxval=5.0, step=0.1)
i_kc_period = input.int(20, "Keltner Channel Period", minval=1, maxval=500)
i_kc_mult = input.float(1.5, "KC ATR Multiplier", minval=0.1, maxval=5.0, step=0.1)
i_source = input.source(close, "Source")
i_show_bands = input.bool(true, "Show Bands", group="Display Options")
i_show_channels = input.bool(true, "Show Channels", group="Display Options")
// Calculation
[squeeze_on, bandwidth, bb_middle, bb_upper, bb_lower, kc_middle, kc_upper, kc_lower] = bbs(i_source, i_bb_period, i_bb_mult, i_kc_period, i_kc_mult)
// Plot squeeze dots
plotshape(squeeze_on, "Squeeze ON", shape.circle, location.bottom, color=color.red, size=size.tiny)
plotshape(not squeeze_on, "Squeeze OFF", shape.circle, location.bottom, color=color.green, size=size.tiny)
// Optional: Plot bands and channels
plot(i_show_bands ? bb_upper : na, "BB Upper", color=color.new(color.blue, 50), linewidth=1)
plot(i_show_bands ? bb_middle : na, "BB Middle", color=color.new(color.blue, 50), linewidth=1)
plot(i_show_bands ? bb_lower : na, "BB Lower", color=color.new(color.blue, 50), linewidth=1)
plot(i_show_channels ? kc_upper : na, "KC Upper", color=color.new(color.orange, 50), linewidth=1)
plot(i_show_channels ? kc_middle : na, "KC Middle", color=color.new(color.orange, 50), linewidth=1)
plot(i_show_channels ? kc_lower : na, "KC Lower", color=color.new(color.orange, 50), linewidth=1)
// Background color during squeeze
bgcolor(squeeze_on ? color.new(color.red, 95) : na, title="Squeeze Background")
+62
View File
@@ -0,0 +1,62 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Chande Forecast Oscillator", "CFO", overlay=false)
//@function Chande Forecast Oscillator - measures percentage difference between price and forecasted price
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/cfo.md
//@param source Price data to analyze
//@param period Number of bars for linear regression calculation
//@returns Oscillator value showing forecast error percentage
//@optimized O(1) complexity using incremental sumXY maintenance
cfo(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
if period > 5000
runtime.error("Period exceeds maximum of 5000")
var int count = 0
var int head = 0
var float sumY = 0.0
var float sumXY = 0.0
var array<float> buffer = array.new_float(period, na)
if na(source)
na
else
float oldest = array.get(buffer, head)
if not na(oldest)
sumY -= oldest
sumXY -= sumY
sumXY += (period - 1) * source
else
sumXY += count * source
count += 1
sumY += source
array.set(buffer, head, source)
head := (head + 1) % period
if count < period
na
else
float sumX = period * (period - 1) / 2
float sumX2 = period * (period - 1) * (2 * period - 1) / 6
float denomX = period * sumX2 - sumX * sumX
float slope = (period * sumXY - sumX * sumY) / denomX
float intercept = (sumY - slope * sumX) / period
float tsf = intercept + slope * (period - 1)
float result = source == 0.0 ? na : 100.0 * (source - tsf) / source
result
// ---------- Main loop ----------
i_period = input.int(14, "Period", minval=1, maxval=5000)
i_source = input.source(close, "Source")
result = cfo(i_source, i_period)
plot(result, "CFO", color=color.yellow, linewidth=2)
hline(0, "Zero Line", color=color.gray, linestyle=hline.style_dotted)
+38
View File
@@ -0,0 +1,38 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Detrended Price Oscillator (DPO)", "DPO", overlay=false)
//@function Calculates Detrended Price Oscillator (DPO) by removing trend component from price
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/dpo.md
//@param source Series to calculate DPO from
//@param period Period for SMA calculation and displacement
//@returns DPO value (current price - displaced SMA)
dpo(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
int displacement = math.floor(period / 2) + 1
float sum = 0.0
for i = 0 to period - 1
sum += nz(source[i], source)
float sma = sum / period
float currentPrice = source
float displacedSMA = sma[displacement]
float result = na
if not na(displacedSMA)
result := currentPrice - displacedSMA
result
// ---------- Main loop ----------
// Inputs
i_source = input.source(close, "Source")
i_period = input.int(20, "Period", minval=1)
// Calculation
dpo_value = dpo(i_source, i_period)
// Plot
plot(dpo_value, "DPO", color=color.yellow, linewidth=2)
hline(0, "Zero Line", color.gray, hline.style_dotted)
+50
View File
@@ -0,0 +1,50 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Fisher Transform", "FISHER", overlay=false)
//@function Calculates the Fisher Transform oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/fisher.md
//@param source Source price (typically hl2)
//@param period Lookback period for min/max normalization
//@returns [fisher, signal] Fisher Transform value and signal line
fisher(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
if period > 500
runtime.error("Period exceeds maximum of 500")
var float value = 0.0
var float fisher = 0.0
var float signal = 0.0
float highest = ta.highest(source, period)
float lowest = ta.lowest(source, period)
float price_range = highest - lowest
float normalized = price_range > 0 ? (source - lowest) / price_range : 0.5
normalized := 2.0 * normalized - 1.0
float alpha = 0.33
value := alpha * normalized + (1.0 - alpha) * value
value := math.max(-0.999, math.min(0.999, value))
fisher := 0.5 * math.log((1.0 + value) / (1.0 - value))
signal := alpha * fisher + (1.0 - alpha) * signal
[fisher, signal]
// ---------- Main loop ----------
i_period = input.int(10, "Period", minval=1, maxval=500)
i_source = input.source(hl2, "Source")
[fisher_line, signal_line] = fisher(i_source, i_period)
plot(fisher_line, "Fisher", color=color.yellow, linewidth=2)
plot(signal_line, "Signal", color=color.orange, linewidth=1)
hline(0, "Zero", color=color.gray, linestyle=hline.style_dotted)
hline(2, "Overbought", color=color.red, linestyle=hline.style_dashed)
hline(-2, "Oversold", color=color.green, linestyle=hline.style_dashed)
+46
View File
@@ -0,0 +1,46 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Inertia", "INERTIA", overlay=false)
//@function Calculates Inertia oscillator measuring trend strength based on distance from linear regression
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/inertia.md
//@param source Source series to calculate Inertia for
//@param length Period for linear regression calculation
//@returns Inertia value measuring trend strength
inertia(series float source, simple int length) =>
if length <= 0
runtime.error("Length must be positive")
if na(source)
na
else
available_bars = bar_index + 1
effective_length = math.min(length, available_bars)
sum_x = 0.0, sum_y = 0.0, sum_xy = 0.0, sum_x2 = 0.0
for i = 0 to effective_length - 1
x = effective_length - 1 - i
y = nz(source[i])
sum_x += x, sum_y += y
sum_xy += x * y, sum_x2 += x * x
n = effective_length
denominator = n * sum_x2 - sum_x * sum_x
if denominator == 0
0.0
else
slope = (n * sum_xy - sum_x * sum_y) / denominator
intercept = (sum_y - slope * sum_x) / n
regression_value = slope * (effective_length - 1) + intercept
source - regression_value
// ---------- Main loop ----------
// Inputs
i_length = input.int(20, "Length", minval=1, maxval=500, tooltip="Period for linear regression calculation")
i_source = input.source(close, "Source", tooltip="Price series to analyze")
// Calculation
inertia_value = inertia(i_source, i_length)
// Plots
plot(inertia_value, "Inertia", color=color.yellow, linewidth=2)
+127
View File
@@ -0,0 +1,127 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("KDJ", "KDJ", overlay=false)
//@function Calculates KDJ (K, D, J) lines - enhanced Stochastic Oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/kdj.md
//@param high Series of high prices
//@param low Series of low prices
//@param close Series of close prices
//@param length Lookback period for highest/lowest calculation
//@param signal Smoothing period for K and D lines
//@returns Tuple [K line, D line, J line]
//@optimized Uses Wilder's RMA smoothing and deque min/max for highest/lowest, O(n) amortized
kdj(series float high, series float low, series float close, simple int length, simple int signal) =>
if length <= 0 or signal <= 0
runtime.error("Length and signal must be greater than 0")
if length > 500
runtime.error("Length exceeds maximum of 500")
float alpha = 1.0 / signal
float beta = 1.0 - alpha
var bool warmupK = true
var bool warmupD = true
var float eK = 1.0
var float eD = 1.0
var float k = 0.0
var float d = 0.0
var float resultK = 50.0
var float resultD = 50.0
var int lastLength = 0
var array<int> maxDeque = array.new_int(0)
var array<int> minDeque = array.new_int(0)
var array<float> highBuffer = array.new_float(0)
var array<float> lowBuffer = array.new_float(0)
if length != lastLength
lastLength := length
maxDeque := array.new_int(0)
minDeque := array.new_int(0)
highBuffer := array.new_float(length, na)
lowBuffer := array.new_float(length, na)
warmupK := true
warmupD := true
eK := 1.0
eD := 1.0
k := 0.0
d := 0.0
resultK := 50.0
resultD := 50.0
if not na(high) and not na(low) and not na(close)
int currentBar = bar_index
int slot = currentBar % length
array.set(highBuffer, slot, high)
array.set(lowBuffer, slot, low)
while array.size(maxDeque) > 0 and array.get(maxDeque, 0) <= currentBar - length
array.shift(maxDeque)
while array.size(minDeque) > 0 and array.get(minDeque, 0) <= currentBar - length
array.shift(minDeque)
while array.size(maxDeque) > 0 and array.get(highBuffer, array.get(maxDeque, array.size(maxDeque) - 1) % length) <= high
array.pop(maxDeque)
while array.size(minDeque) > 0 and array.get(lowBuffer, array.get(minDeque, array.size(minDeque) - 1) % length) >= low
array.pop(minDeque)
array.push(maxDeque, currentBar)
array.push(minDeque, currentBar)
float highest = high
float lowest = low
if array.size(maxDeque) > 0
highest := array.get(highBuffer, array.get(maxDeque, 0) % length)
if array.size(minDeque) > 0
lowest := array.get(lowBuffer, array.get(minDeque, 0) % length)
float price_range = highest - lowest
float rsv = price_range > 0 ? 100.0 * (close - lowest) / price_range : 50.0
k := alpha * rsv + beta * k
d := alpha * k + beta * d
if warmupK
eK *= beta
float cK = 1.0 / (1.0 - eK)
resultK := math.max(0.0, math.min(100.0, cK * k))
warmupK := eK > 1e-10
else
resultK := math.max(0.0, math.min(100.0, k))
if warmupD
eD *= beta
float cD = 1.0 / (1.0 - eD)
resultD := math.max(0.0, math.min(100.0, cD * d))
warmupD := eD > 1e-10
else
resultD := math.max(0.0, math.min(100.0, d))
float j = 3.0 * resultK - 2.0 * resultD
[resultK, resultD, j]
// ---------- Main loop ----------
// Inputs
i_length = input.int(9, "Length", minval=1, maxval=500)
i_signal = input.int(3, "Signal", minval=1, maxval=50)
// Calculation
[k, d, j] = kdj(high, low, close, i_length, i_signal)
// Plot
plot(k, "K", color=color.blue, linewidth=2)
plot(d, "D", color=color.red, linewidth=2)
plot(j, "J", color=color.yellow, linewidth=2)
hline(80, "Overbought K", color=color.red, linestyle=hline.style_dotted)
hline(70, "Overbought D", color=color.orange, linestyle=hline.style_dotted)
hline(50, "Midline", color=color.gray, linestyle=hline.style_solid)
hline(30, "Oversold D", color=color.lime, linestyle=hline.style_dotted)
hline(20, "Oversold K", color=color.green, linestyle=hline.style_dotted)
+79
View File
@@ -0,0 +1,79 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Pretty Good Oscillator", "PGO", overlay=false)
//@function Calculate Pretty Good Oscillator (PGO)
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/pgo.md
//@param source Price data to analyze
//@param period Number of bars for SMA and ATR calculation
//@returns PGO value normalized by ATR
pgo(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be greater than 0")
if period > 5000
runtime.error("Period exceeds maximum of 5000")
var array<float> sma_buffer = array.new_float(period, na)
var int sma_head = 0
var float sma_sum = 0.0
var int valid_count = 0
float oldest = array.get(sma_buffer, sma_head)
if not na(oldest)
sma_sum -= oldest
valid_count -= 1
if not na(source)
sma_sum += source
valid_count += 1
array.set(sma_buffer, sma_head, source)
sma_head := (sma_head + 1) % period
float sma_value = nz(sma_sum / valid_count, source)
float prevClose = nz(close[1], close)
float tr1 = high - low
float tr2 = math.abs(high - prevClose)
float tr3 = math.abs(low - prevClose)
float tr = math.max(tr1, math.max(tr2, tr3))
float a = 1.0 / float(period)
float beta = 1.0 - a
var bool warmup = true
var float e = 1.0
var float ema = 0.0
var float atr = nz(tr)
ema := a * (nz(tr) - ema) + ema
if warmup
e *= beta
float c = 1.0 / (1.0 - e)
atr := c * ema
warmup := e > 1e-10
else
atr := ema
float pgo_value = atr > 0 ? (source - sma_value) / atr : na
pgo_value
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=1, maxval=500, tooltip="Number of bars for SMA and ATR calculation")
i_source = input.source(close, "Source")
// Calculation
result = pgo(i_source, i_period)
// Plot
plot(result, "PGO", color=color.yellow, linewidth=2)
hline(0, "Zero Line", color=color.gray, linestyle=hline.style_solid)
hline(3, "Overbought", color=color.red, linestyle=hline.style_dashed)
hline(-3, "Oversold", color=color.green, linestyle=hline.style_dashed)
// Background coloring for extreme zones
bgcolor(not na(result) and result > 3 ? color.new(color.red, 85) : not na(result) and result < -3 ? color.new(color.green, 85) : na)
+90
View File
@@ -0,0 +1,90 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Stochastic Momentum Index (SMI)", "SMI", overlay=false)
//@function Calculates Stochastic Momentum Index oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/smi.md
//@param source Source series to calculate SMI for
//@param k_period Lookback period for high/low range calculation
//@param k_smooth First smoothing period for raw SMI values
//@param d_smooth Second smoothing period for signal line
//@param blau Use Blau method (true) or Chande/Kroll method (false)
//@returns [%K, %D] values of Stochastic Momentum Index
smi(series float source, simple int k_period, simple int k_smooth, simple int d_smooth, simple bool blau) =>
if k_period <= 0 or k_smooth <= 0 or d_smooth <= 0
runtime.error("All periods must be positive")
float src_clean = na(source) ? 0 : source
if na(source)
[na, na]
else
var array<float> high_buffer = array.new_float(0), var array<float> low_buffer = array.new_float(0)
array.push(high_buffer, nz(high)), array.push(low_buffer, nz(low))
if array.size(high_buffer) > k_period
array.shift(high_buffer)
if array.size(low_buffer) > k_period
array.shift(low_buffer)
highest_high = array.max(high_buffer), lowest_low = array.min(low_buffer)
midpoint = (highest_high + lowest_low) / 2, range_half = (highest_high - lowest_low) / 2
float a1 = 2.0 / (k_smooth + 1), float a2 = 2.0 / (k_smooth + 1), float a3 = 2.0 / (d_smooth + 1)
var float e1 = 1.0, var float e2 = 1.0, var float e3 = 1.0, var bool warmup = true
var float ema1_raw = 0.0, var float ema2_raw = 0.0, var float ema3_raw = 0.0
var float first_ema = 0.0, var float k_value = 0.0, var float d_value = 0.0
if blau
raw_smi = range_half > 0 ? 100 * (src_clean - midpoint) / range_half : 0
ema1_raw := a1 * (raw_smi - ema1_raw) + ema1_raw
if warmup
e1 *= (1 - a1), e2 *= (1 - a2), e3 *= (1 - a3)
float c1 = 1.0 / (1.0 - e1), float c2 = 1.0 / (1.0 - e2), float c3 = 1.0 / (1.0 - e3)
first_ema := ema1_raw * c1
ema2_raw := a2 * (first_ema - ema2_raw) + ema2_raw
k_value := ema2_raw * c2
ema3_raw := a3 * (k_value - ema3_raw) + ema3_raw
d_value := ema3_raw * c3
warmup := math.max(math.max(e1, e2), e3) > 1e-10
else
first_ema := ema1_raw
ema2_raw := a2 * (first_ema - ema2_raw) + ema2_raw
k_value := ema2_raw
ema3_raw := a3 * (k_value - ema3_raw) + ema3_raw
d_value := ema3_raw
else
var float num_ema1 = 0.0, var float num_ema2 = 0.0, var float den_ema1 = 0.0, var float den_ema2 = 0.0
var float num_first = 0.0, var float den_first = 0.0
numerator = src_clean - midpoint, denominator = range_half
ema1_raw := a1 * (numerator - ema1_raw) + ema1_raw
num_ema1 := a1 * (denominator - num_ema1) + num_ema1
if warmup
e1 *= (1 - a1), e2 *= (1 - a2), e3 *= (1 - a3)
float c1 = 1.0 / (1.0 - e1), float c2 = 1.0 / (1.0 - e2), float c3 = 1.0 / (1.0 - e3)
num_first := ema1_raw * c1, den_first := num_ema1 * c1
num_ema2 := a2 * (num_first - num_ema2) + num_ema2
den_ema2 := a2 * (den_first - den_ema2) + den_ema2
k_value := den_ema2 > 0 ? 100 * (num_ema2 * c2) / (den_ema2 * c2) : 0
ema3_raw := a3 * (k_value - ema3_raw) + ema3_raw
d_value := ema3_raw * c3
warmup := math.max(math.max(e1, e2), e3) > 1e-10
else
num_first := ema1_raw, den_first := num_ema1
num_ema2 := a2 * (num_first - num_ema2) + num_ema2
den_ema2 := a2 * (den_first - den_ema2) + den_ema2
k_value := den_ema2 > 0 ? 100 * num_ema2 / den_ema2 : 0
ema3_raw := a3 * (k_value - ema3_raw) + ema3_raw
d_value := ema3_raw
[k_value, d_value]
// ---------- Main loop ----------
// Inputs
i_k_period = input.int(10, "%K Period", minval=1, maxval=100, tooltip="Lookback period for high/low range calculation")
i_k_smooth = input.int(3, "%K Smooth", minval=1, maxval=20, tooltip="First smoothing period for raw SMI values")
i_d_smooth = input.int(3, "%D Smooth", minval=1, maxval=20, tooltip="Second smoothing period for signal line")
i_source = input.source(close, "Source", tooltip="Price series to analyze")
i_blau = input.bool(true, "Blau Method", tooltip="True: Blau (smooth raw SMI ratio), False: Chande/Kroll (smooth numerator & denominator first)")
// Calculation
[k_value, d_value] = smi(i_source, i_k_period, i_k_smooth, i_d_smooth, i_blau)
// Plots
plot(k_value, "SMI %K", color=color.yellow, linewidth=2)
plot(d_value, "SMI %D", color=color.blue, linewidth=2)
+74
View File
@@ -0,0 +1,74 @@
//@version=6
indicator("Stochastic Oscillator (STOCH)", "Stoch", overlay=false)
//@function Calculates the Stochastic Oscillator (%K and %D). %K = 100 * (close - lowest_low(kLength)) / (highest_high(kLength) - lowest_low(kLength)). %D = SMA(%K, dPeriod). Uses efficient deque implementation for min/max and buffer-based SMA.
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/stoch.md
//@param kLength `simple int` The lookback period for calculating highest high and lowest low.
//@param dPeriod `simple int` The smoothing period for the %D line (SMA of %K).
//@returns `[float, float]` A tuple containing the %K value and the %D value.
stoch(simple int kLength,simple int dPeriod)=>
if kLength<=0 or dPeriod<=0
runtime.error("Both periods must be positive")
var float kVal=0.0, var float dVal=0.0
var int dHead=0, var float dSum=0.0
var array<int>lowestDeque=array.new_int(0)
var array<float>lowestBuffer=array.new_float(kLength,na)
var array<int>highestDeque=array.new_int(0)
var array<float>highestBuffer=array.new_float(kLength,na)
var array<float>dBuffer=array.new_float(dPeriod,0.0)
int idx=bar_index%kLength
float lv=nz(low), float hv=nz(high)
array.set(lowestBuffer,idx,lv)
array.set(highestBuffer,idx,hv)
while array.size(lowestDeque)>0
if array.get(lowestDeque,0)<=bar_index-kLength
array.shift(lowestDeque)
else
break
while array.size(lowestDeque)>0
if array.get(lowestBuffer,array.get(lowestDeque,array.size(lowestDeque)-1)%kLength)>=lv
array.pop(lowestDeque)
else
break
array.push(lowestDeque,bar_index)
while array.size(highestDeque)>0
if array.get(highestDeque,0)<=bar_index-kLength
array.shift(highestDeque)
else
break
while array.size(highestDeque)>0
if array.get(highestBuffer,array.get(highestDeque,array.size(highestDeque)-1)%kLength)<=hv
array.pop(highestDeque)
else
break
array.push(highestDeque,bar_index)
int li=array.get(lowestDeque,0)
int hi=array.get(highestDeque,0)
float lowestLow=array.get(lowestBuffer,li%kLength)
float highestHigh=array.get(highestBuffer,hi%kLength)
float rnge=highestHigh-lowestLow
kVal:=rnge>0?100*(close-lowestLow)/rnge:0.0
if bar_index==0
dSum:=kVal*dPeriod
array.fill(dBuffer,kVal)
else
float oldVal=array.get(dBuffer,dHead)
dSum:=dSum-oldVal+kVal
array.set(dBuffer,dHead,kVal)
dHead:=(dHead+1)%dPeriod
dVal:=dSum/dPeriod
[kVal,dVal]
// ---------- Main loop ----------
// Inputs
kPeriod = input.int(14, "K Length", minval=1)
dPeriod = input.int(3, "D Smooth", minval=1)
// Calculation
[kValue, dValue] = stoch(kPeriod, dPeriod)
// Plot
plot(kValue, "Stochastic %K", color=color.green, linewidth=2)
plot(dValue, "Stochastic %D", color=color.red, linewidth=2)
+73
View File
@@ -0,0 +1,73 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Stochastic Fast (STOCHF)", "STOCHF", overlay=false)
//@function Calculates the Stochastic Fast oscillator (%K and %D)
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/stochf.md
//@param kLength Period for calculating the raw %K line
//@param dLength Smoothing period for the %D signal line
//@returns [%K value, %D value] - fast stochastic oscillator values
stochf(simple int kLength, simple int dLength) =>
if kLength <= 0 or dLength <= 0
runtime.error("Both periods must be positive")
var array<int> lowestDeque = array.new_int(0)
var array<float> lowestBuffer = array.new_float(kLength, na)
var array<int> highestDeque = array.new_int(0)
var array<float> highestBuffer = array.new_float(kLength, na)
var array<float> dBuffer = array.new_float(dLength, na)
var int dHead = 0, var float dSum = 0.0, var int dCount = 0
int idx = bar_index % kLength
lv = nz(low), hv = nz(high), cv = nz(close)
array.set(lowestBuffer, idx, lv)
array.set(highestBuffer, idx, hv)
while array.size(lowestDeque) > 0
if array.get(lowestDeque, 0) <= bar_index - kLength
array.shift(lowestDeque)
else
break
while array.size(lowestDeque) > 0
if array.get(lowestBuffer, array.get(lowestDeque, array.size(lowestDeque) - 1) % kLength) >= lv
array.pop(lowestDeque)
else
break
array.push(lowestDeque, bar_index)
while array.size(highestDeque) > 0
if array.get(highestDeque, 0) <= bar_index - kLength
array.shift(highestDeque)
else
break
while array.size(highestDeque) > 0
if array.get(highestBuffer, array.get(highestDeque, array.size(highestDeque) - 1) % kLength) <= hv
array.pop(highestDeque)
else
break
array.push(highestDeque, bar_index)
li = array.get(lowestDeque, 0), hi = array.get(highestDeque, 0)
lowestLow = array.get(lowestBuffer, li % kLength)
highestHigh = array.get(highestBuffer, hi % kLength)
range_val = highestHigh - lowestLow
kVal = range_val > 0 ? 100 * (cv - lowestLow) / range_val : 0.0
oldest = array.get(dBuffer, dHead)
dSum := not na(oldest) ? dSum - oldest : dSum
dCount := not na(oldest) ? dCount - 1 : dCount
dSum := not na(kVal) ? dSum + kVal : dSum
dCount := not na(kVal) ? dCount + 1 : dCount
array.set(dBuffer, dHead, kVal)
dHead := (dHead + 1) % dLength
dVal = dCount > 0 ? dSum / dCount : kVal
[kVal, dVal]
// ---------- Main loop ----------
// Inputs
kLength = input.int(5, "K Length", minval=1, maxval=100, tooltip="Period for calculating the raw %K line")
dLength = input.int(3, "D Length", minval=1, maxval=50, tooltip="Smoothing period for the %D signal line")
// Calculation
[kValue, dValue] = stochf(kLength, dLength)
// Plots
plot(kValue, "Fast %K", color=color.yellow, linewidth=2)
plot(dValue, "Fast %D", color=color.blue, linewidth=2)
+67
View File
@@ -0,0 +1,67 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Stochastic RSI (STOCHRSI)", "StochRSI", overlay=false)
//@function Calculates Stochastic RSI oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/stochrsi.md
//@param source Source series to calculate STOCHRSI for
//@param rsi_length Period for RSI calculation
//@param stoch_length Lookback period for Stochastic calculation on RSI
//@param k_smooth Smoothing period for %K line
//@param d_smooth Smoothing period for %D line
//@returns [%K, %D] values of Stochastic RSI
stochrsi(series float source, simple int rsi_length, simple int stoch_length, simple int k_smooth, simple int d_smooth) =>
if rsi_length <= 0 or stoch_length <= 0 or k_smooth <= 0 or d_smooth <= 0
runtime.error("All periods must be positive")
float src_clean = na(source) ? 0 : source
float u = math.max(src_clean - nz(src_clean[1]), 0)
float d = math.max(nz(src_clean[1]) - src_clean, 0)
float alpha = 1/rsi_length
var float smoothUp = 0.0, var float smoothDown = 0.0
if bar_index < rsi_length
smoothUp := u
smoothDown := d
else
smoothUp := nz(smoothUp[1]) * (1 - alpha) + u * alpha
smoothDown := nz(smoothDown[1]) * (1 - alpha) + d * alpha
float rs = smoothDown == 0 ? 0 : smoothUp/smoothDown
float rsi_val = smoothDown == 0 ? 100 : 100 - (100 / (1 + rs))
if na(source)
[na, na]
else
var array<float> rsi_buffer = array.new_float(0)
array.push(rsi_buffer, rsi_val)
if array.size(rsi_buffer) > stoch_length
array.shift(rsi_buffer)
highest_rsi = array.max(rsi_buffer)
lowest_rsi = array.min(rsi_buffer)
rsi_range = highest_rsi - lowest_rsi
k_raw = rsi_range > 0 ? 100 * (rsi_val - lowest_rsi) / rsi_range : 50
var array<float> k_buffer = array.new_float(0)
array.push(k_buffer, k_raw)
if array.size(k_buffer) > k_smooth
array.shift(k_buffer)
k_smoothed = array.sum(k_buffer) / array.size(k_buffer)
var array<float> d_buffer = array.new_float(0)
array.push(d_buffer, k_smoothed)
if array.size(d_buffer) > d_smooth
array.shift(d_buffer)
d_smoothed = array.sum(d_buffer) / array.size(d_buffer)
[k_smoothed, d_smoothed]
// ---------- Main loop ----------
// Inputs
i_rsi_length = input.int(14, "RSI Length", minval=1, maxval=100, tooltip="Period for RSI calculation")
i_stoch_length = input.int(14, "Stochastic Length", minval=1, maxval=100, tooltip="Lookback period for Stochastic calculation on RSI")
i_k_smooth = input.int(3, "%K Smooth", minval=1, maxval=20, tooltip="Smoothing period for %K line")
i_d_smooth = input.int(3, "%D Smooth", minval=1, maxval=20, tooltip="Smoothing period for %D line")
i_source = input.source(close, "Source", tooltip="Price series to analyze")
// Calculation
[k_value, d_value] = stochrsi(i_source, i_rsi_length, i_stoch_length, i_k_smooth, i_d_smooth)
// Plots
plot(k_value, "StochRSI %K", color=color.yellow, linewidth=2)
plot(d_value, "StochRSI %D", color=color.blue, linewidth=2)
+51
View File
@@ -0,0 +1,51 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("TRIX", "TRIX", overlay=false)
//@function Calculates TRIX oscillator with compensation
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/trix.md
//@param source Series to calculate TRIX from
//@param period Period for triple exponential smoothing
//@returns TRIX value (percentage rate of change of triple EMA)
trix(series float source, simple int period) =>
if period <= 0
runtime.error("Period must be positive")
float src = na(source) ? source[1] : source
float alpha = 2.0 / (period + 1)
float d = 1 - alpha
var float e1 = 1.0, var float e2 = 1.0, var float e3 = 1.0
var bool warmup = true
var float rema1 = 0, var float rema2 = 0, var float rema3 = 0
var float ema1 = src, var float ema2 = src, var float ema3 = src
var float prev_ema3 = src
rema1 := alpha * (src - rema1) + rema1
if warmup
e1 *= d, e2 *= d, e3 *= d
ema1 := rema1 / (1.0 - e1)
rema2 := alpha * (ema1 - rema2) + rema2
ema2 := rema2 / (1.0 - e2)
rema3 := alpha * (ema2 - rema3) + rema3
ema3 := rema3 / (1.0 - e3)
warmup := e1 > 1e-10
else
ema1 := rema1
rema2 := alpha * (ema1 - rema2) + rema2
ema2 := rema2
rema3 := alpha * (ema2 - rema3) + rema3
ema3 := rema3
trix_value = prev_ema3 != 0 ? 100 * (ema3 - prev_ema3) / prev_ema3 : 0
prev_ema3 := ema3
na(source) ? na : trix_value
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=1, maxval=100, tooltip="Period for triple exponential smoothing")
i_source = input.source(close, "Source", tooltip="Price series to analyze")
// Calculation
trix_value = trix(i_source, i_period)
// Plot
plot(trix_value, "TRIX", color=color.yellow, linewidth=2)
+504
View File
@@ -0,0 +1,504 @@
namespace QuanTAlib.Tests;
public class UltoscTests
{
// ============== Constructor & Parameter Validation ==============
[Fact]
public void Constructor_InvalidPeriod1_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ultosc(0, 14, 28));
Assert.Throws<ArgumentException>(() => new Ultosc(-1, 14, 28));
}
[Fact]
public void Constructor_InvalidPeriod2_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ultosc(7, 0, 28));
Assert.Throws<ArgumentException>(() => new Ultosc(7, -1, 28));
}
[Fact]
public void Constructor_InvalidPeriod3_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ultosc(7, 14, 0));
Assert.Throws<ArgumentException>(() => new Ultosc(7, 14, -1));
}
[Fact]
public void Constructor_Period1NotLessThanPeriod2_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ultosc(14, 14, 28));
Assert.Throws<ArgumentException>(() => new Ultosc(15, 14, 28));
}
[Fact]
public void Constructor_Period2NotLessThanPeriod3_ThrowsArgumentException()
{
Assert.Throws<ArgumentException>(() => new Ultosc(7, 28, 28));
Assert.Throws<ArgumentException>(() => new Ultosc(7, 29, 28));
}
[Fact]
public void Constructor_ValidParameters_Succeeds()
{
var ultosc = new Ultosc(7, 14, 28);
Assert.NotNull(ultosc);
var ultosc2 = new Ultosc(5, 10, 20);
Assert.NotNull(ultosc2);
}
// ============== Basic Functionality ==============
[Fact]
public void BasicCalculation_DoesNotCrash()
{
var ultosc = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
ultosc.Update(bar);
}
Assert.True(double.IsFinite(ultosc.Last.Value));
}
[Fact]
public void Calc_ReturnsValue()
{
var ultosc = new Ultosc(7, 14, 28);
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
Assert.Equal(0, ultosc.Last.Value);
TValue result = ultosc.Update(bar);
Assert.True(result.Value > 0);
Assert.Equal(result.Value, ultosc.Last.Value);
}
[Fact]
public void FirstValue_ReturnsValidOscillator()
{
var ultosc = new Ultosc(7, 14, 28);
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
// First bar: BP = Close - Low = 105 - 90 = 15
// TR = High - Low = 110 - 90 = 20
// Avg = BP/TR = 15/20 = 0.75 for all periods
// UO = 100 * (4*0.75 + 2*0.75 + 0.75) / 7 = 100 * 5.25/7 = 75
TValue result = ultosc.Update(bar);
Assert.Equal(75.0, result.Value, 1e-10);
}
[Fact]
public void Properties_Accessible()
{
var ultosc = new Ultosc(7, 14, 28);
Assert.Equal(0, ultosc.Last.Value);
Assert.False(ultosc.IsHot);
Assert.Contains("Ultosc", ultosc.Name, StringComparison.Ordinal);
Assert.Equal(28, ultosc.WarmupPeriod);
var bar = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
ultosc.Update(bar);
Assert.NotEqual(0, ultosc.Last.Value);
}
// ============== State Management & Bar Correction ==============
[Fact]
public void Calc_IsNew_AcceptsParameter()
{
var ultosc = new Ultosc(7, 14, 28);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
ultosc.Update(bar1, isNew: true);
double value1 = ultosc.Last.Value;
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
ultosc.Update(bar2, isNew: true);
double value2 = ultosc.Last.Value;
Assert.NotEqual(value1, value2);
}
[Fact]
public void Calc_IsNew_False_UpdatesValue()
{
var ultosc = new Ultosc(7, 14, 28);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
ultosc.Update(bar1, isNew: true);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 100, 108, 1000);
ultosc.Update(bar2, isNew: true);
double beforeUpdate = ultosc.Last.Value;
var bar2Modified = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 120, 90, 108, 1000);
ultosc.Update(bar2Modified, isNew: false);
double afterUpdate = ultosc.Last.Value;
Assert.NotEqual(beforeUpdate, afterUpdate);
}
[Fact]
public void IsNew_Consistency()
{
var ultosc = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed first 99
for (int i = 0; i < 99; i++)
{
ultosc.Update(bars[i]);
}
// Update with 100th point (isNew=true)
ultosc.Update(bars[99], true);
// Update with modified 100th point (isNew=false)
var modifiedBar = new TBar(bars[99].Time, bars[99].Open, bars[99].High + 10.0, bars[99].Low - 10.0, bars[99].Close, bars[99].Volume);
double val2 = ultosc.Update(modifiedBar, false).Value;
// Create new instance and feed up to modified
var ultosc2 = new Ultosc(7, 14, 28);
for (int i = 0; i < 99; i++)
{
ultosc2.Update(bars[i]);
}
double val3 = ultosc2.Update(modifiedBar, true).Value;
Assert.Equal(val3, val2, 1e-9);
}
[Fact]
public void IterativeCorrections_RestoreToOriginalState()
{
var ultosc = new Ultosc(3, 5, 7);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
var bars = gbm.Fetch(20, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Feed 10 new values
TBar tenthBar = default;
for (int i = 0; i < 10; i++)
{
tenthBar = bars[i];
ultosc.Update(tenthBar, isNew: true);
}
// Remember state after 10 values
double stateAfterTen = ultosc.Last.Value;
// Generate 9 corrections with isNew=false (different values)
for (int i = 10; i < 19; i++)
{
ultosc.Update(bars[i], isNew: false);
}
// Feed the remembered 10th bar again with isNew=false
TValue finalResult = ultosc.Update(tenthBar, isNew: false);
// State should match the original state after 10 values
Assert.Equal(stateAfterTen, finalResult.Value, 1e-10);
}
[Fact]
public void Reset_Works()
{
var ultosc = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars) ultosc.Update(bar);
double lastVal = ultosc.Last.Value;
Assert.NotEqual(0, lastVal);
ultosc.Reset();
Assert.Equal(0, ultosc.Last.Value);
Assert.False(ultosc.IsHot);
// After reset, should accept new values
ultosc.Update(bars[0]);
Assert.NotEqual(0, ultosc.Last.Value);
}
// ============== Warmup & Convergence ==============
[Fact]
public void IsHot_BecomesTrueAfterWarmup()
{
var ultosc = new Ultosc(3, 5, 7);
Assert.False(ultosc.IsHot);
int steps = 0;
var baseTime = DateTime.UtcNow;
while (!ultosc.IsHot && steps < 100)
{
var bar = new TBar(baseTime.AddMinutes(steps), 100, 110, 90, 100, 1000);
ultosc.Update(bar);
steps++;
}
Assert.True(ultosc.IsHot);
Assert.True(steps > 0);
}
[Fact]
public void WarmupPeriod_IsPositive()
{
var ultosc = new Ultosc(7, 14, 28);
Assert.True(ultosc.WarmupPeriod > 0);
Assert.Equal(28, ultosc.WarmupPeriod);
var ultosc2 = new Ultosc(5, 10, 20);
Assert.Equal(20, ultosc2.WarmupPeriod);
}
// ============== NaN/Infinity Handling ==============
[Fact]
public void NaN_Input_UsesLastValidValue()
{
var ultosc = new Ultosc(3, 5, 7);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
ultosc.Update(bar1);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
ultosc.Update(bar2);
// Feed bar with NaN values
var barWithNaN = new TBar(DateTime.UtcNow.AddMinutes(2), double.NaN, 115, 100, 112, 1000);
var resultAfterNaN = ultosc.Update(barWithNaN);
// Result should be finite
Assert.True(double.IsFinite(resultAfterNaN.Value));
}
[Fact]
public void Infinity_Input_UsesLastValidValue()
{
var ultosc = new Ultosc(3, 5, 7);
var bar1 = new TBar(DateTime.UtcNow, 100, 105, 95, 102, 1000);
ultosc.Update(bar1);
var bar2 = new TBar(DateTime.UtcNow.AddMinutes(1), 102, 110, 98, 108, 1000);
ultosc.Update(bar2);
// Feed bar with Infinity
var barWithInf = new TBar(DateTime.UtcNow.AddMinutes(2), 108, double.PositiveInfinity, 100, 112, 1000);
var resultAfterInf = ultosc.Update(barWithInf);
// Result should be finite or infinity (depending on implementation)
Assert.True(double.IsFinite(resultAfterInf.Value) || double.IsPositiveInfinity(resultAfterInf.Value));
}
// ============== Consistency Tests ==============
[Fact]
public void BatchCalc_MatchesIterativeCalc()
{
var ultoscIterative = new Ultosc(7, 14, 28);
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Calculate iteratively
var iterativeResults = new TSeries();
foreach (var bar in bars)
{
iterativeResults.Add(ultoscIterative.Update(bar));
}
// Calculate batch
var batchResults = Ultosc.Batch(bars, 7, 14, 28);
// 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);
}
}
[Fact]
public void TBarSeries_Update_MatchesStreaming()
{
var ultosc1 = new Ultosc(7, 14, 28);
var ultosc2 = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// Streaming
foreach (var bar in bars)
{
ultosc1.Update(bar);
}
// Batch
ultosc2.Update(bars);
Assert.Equal(ultosc1.Last.Value, ultosc2.Last.Value, 1e-10);
}
[Fact]
public void Chainability_Works()
{
var ultosc = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var result = ultosc.Update(bars);
Assert.Equal(50, result.Count);
Assert.Equal(ultosc.Last.Value, result.Last.Value);
}
// ============== Oscillator Range Tests ==============
[Fact]
public void Oscillator_ReturnsValueBetween0And100()
{
var ultosc = new Ultosc(7, 14, 28);
var gbm = new GBM();
var bars = gbm.Fetch(100, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
foreach (var bar in bars)
{
var result = ultosc.Update(bar);
Assert.InRange(result.Value, 0.0, 100.0);
}
}
[Fact]
public void StrongUptrend_ReturnsHighValues()
{
var ultosc = new Ultosc(3, 5, 7);
var baseTime = DateTime.UtcNow;
// Create strong uptrend bars where Close is always at High
for (int i = 0; i < 20; i++)
{
double basePrice = 100 + (i * 5); // Rising prices
var bar = new TBar(baseTime.AddMinutes(i), basePrice, basePrice + 10, basePrice - 2, basePrice + 10, 1000);
ultosc.Update(bar);
}
// In strong uptrend with Close at High, BP/TR should be high
Assert.True(ultosc.Last.Value > 50);
}
[Fact]
public void StrongDowntrend_ReturnsLowValues()
{
var ultosc = new Ultosc(3, 5, 7);
var baseTime = DateTime.UtcNow;
// Create strong downtrend bars where Close is always at Low
for (int i = 0; i < 20; i++)
{
double basePrice = 200 - (i * 5); // Falling prices
var bar = new TBar(baseTime.AddMinutes(i), basePrice, basePrice + 2, basePrice - 10, basePrice - 10, 1000);
ultosc.Update(bar);
}
// In strong downtrend with Close at Low, BP/TR should be low
Assert.True(ultosc.Last.Value < 50);
}
// ============== Static Batch Method ==============
[Fact]
public void StaticBatch_Works()
{
var gbm = new GBM();
var bars = gbm.Fetch(50, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
var results = Ultosc.Batch(bars, 7, 14, 28);
Assert.Equal(50, results.Count);
Assert.True(double.IsFinite(results.Last.Value));
}
// ============== Edge Cases ==============
[Fact]
public void SingleBar_ReturnsValidResult()
{
var ultosc = new Ultosc(7, 14, 28);
var bar = new TBar(DateTime.UtcNow, 100, 110, 90, 105, 1000);
var result = ultosc.Update(bar);
Assert.True(double.IsFinite(result.Value));
// BP = Close - Low = 105 - 90 = 15
// TR = High - Low = 110 - 90 = 20
// Avg = 15/20 = 0.75
// UO = 100 * (4*0.75 + 2*0.75 + 0.75) / 7 = 75
Assert.Equal(75.0, result.Value, 1e-10);
}
[Fact]
public void FlatBars_ReturnsFifty()
{
var ultosc = new Ultosc(3, 5, 7);
// All bars have same OHLC values (flat market)
for (int i = 0; i < 20; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 100, 100, 100, 1000);
ultosc.Update(bar);
}
// For flat bars: BP = 0, TR = 0, so BP/TR = 0/0 handled as 0.5
// UO = 100 * 0.5 * 7 / 7 = 50
Assert.Equal(50.0, ultosc.Last.Value, 1e-10);
}
[Fact]
public void CloseAtHigh_ReturnsHundred()
{
var ultosc = new Ultosc(3, 5, 7);
// All bars have Close at High
for (int i = 0; i < 20; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 110, 1000);
ultosc.Update(bar);
}
// BP = Close - TrueLow = 110 - 90 = 20
// TR = TrueHigh - TrueLow = 110 - 90 = 20
// Avg = 20/20 = 1.0
// UO = 100 * (4*1 + 2*1 + 1) / 7 = 100
Assert.Equal(100.0, ultosc.Last.Value, 1e-10);
}
[Fact]
public void CloseAtLow_ReturnsZero()
{
var ultosc = new Ultosc(3, 5, 7);
// All bars have Close at Low
for (int i = 0; i < 20; i++)
{
var bar = new TBar(DateTime.UtcNow.AddMinutes(i), 100, 110, 90, 90, 1000);
ultosc.Update(bar);
}
// BP = Close - TrueLow = 90 - 90 = 0
// TR = TrueHigh - TrueLow = 110 - 90 = 20
// Avg = 0/20 = 0.0
// UO = 100 * (4*0 + 2*0 + 0) / 7 = 0
Assert.Equal(0.0, ultosc.Last.Value, 1e-10);
}
}
@@ -0,0 +1,306 @@
using OoplesFinance.StockIndicators;
using OoplesFinance.StockIndicators.Models;
using Skender.Stock.Indicators;
using TALib;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public sealed class UltoscValidationTests : IDisposable
{
private readonly ValidationTestData _testData;
private readonly ITestOutputHelper _output;
private bool _disposed;
public UltoscValidationTests(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_Skender_Batch()
{
int[][] periodSets = { [7, 14, 28] };
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (batch TBarSeries)
var ultosc = new Ultosc(p1, p2, p3);
var qResult = ultosc.Update(_testData.Bars);
// Calculate Skender Ultimate Oscillator
var sResult = _testData.SkenderQuotes.GetUltimate(p1, p2, p3).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s.Ultimate, tolerance: ValidationHelper.SkenderTolerance);
}
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[][] periodSets = { [7, 14, 28] };
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (streaming)
var ultosc = new Ultosc(p1, p2, p3);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(ultosc.Update(item).Value);
}
// Calculate Skender Ultimate Oscillator
var sResult = _testData.SkenderQuotes.GetUltimate(p1, p2, p3).ToList();
// Compare last 100 records
ValidationHelper.VerifyData(qResults, sResult, (s) => s.Ultimate, tolerance: ValidationHelper.SkenderTolerance);
}
_output.WriteLine("Ultosc Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Talib_Batch()
{
int[][] periodSets = { [7, 14, 28] };
// Prepare data for TA-Lib (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
double[] output = new double[hData.Length];
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (batch TBarSeries)
var ultosc = new Ultosc(p1, p2, p3);
var qResult = ultosc.Update(_testData.Bars);
// Calculate TA-Lib UltOsc
var retCode = TALib.Functions.UltOsc(hData, lData, cData, 0..^0, output, out var outRange, p1, p2, p3);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.UltOscLookback(p1, p2, p3);
// Compare last 100 records
ValidationHelper.VerifyData(qResult, output, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
}
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against TA-Lib");
}
[Fact]
public void Validate_Talib_Streaming()
{
int[][] periodSets = { [7, 14, 28] };
// Prepare data for TA-Lib (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
double[] output = new double[hData.Length];
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (streaming)
var ultosc = new Ultosc(p1, p2, p3);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(ultosc.Update(item).Value);
}
// Calculate TA-Lib UltOsc
var retCode = TALib.Functions.UltOsc(hData, lData, cData, 0..^0, output, out var outRange, p1, p2, p3);
Assert.Equal(Core.RetCode.Success, retCode);
int lookback = TALib.Functions.UltOscLookback(p1, p2, p3);
// Compare last 100 records
ValidationHelper.VerifyData(qResults, output, outRange, lookback, tolerance: ValidationHelper.TalibTolerance);
}
_output.WriteLine("Ultosc Streaming validated successfully against TA-Lib");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[][] periodSets = { [7, 14, 28] };
// Prepare data for Tulip (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (batch TBarSeries)
var ultosc = new Ultosc(p1, p2, p3);
var qResult = ultosc.Update(_testData.Bars);
// Calculate Tulip UltOsc
var ultoscIndicator = Tulip.Indicators.ultosc;
double[][] inputs = { hData, lData, cData };
double[] options = { p1, p2, p3 };
// Tulip UltOsc lookback
int lookback = ultoscIndicator.Start(options);
double[][] outputs = { new double[hData.Length - lookback] };
ultoscIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResult, tResult, lookback, tolerance: ValidationHelper.TulipTolerance);
}
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Tulip_Streaming()
{
int[][] periodSets = { [7, 14, 28] };
// Prepare data for Tulip (double[])
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (streaming)
var ultosc = new Ultosc(p1, p2, p3);
var qResults = new List<double>();
foreach (var item in _testData.Bars)
{
qResults.Add(ultosc.Update(item).Value);
}
// Calculate Tulip UltOsc
var ultoscIndicator = Tulip.Indicators.ultosc;
double[][] inputs = { hData, lData, cData };
double[] options = { p1, p2, p3 };
// Tulip UltOsc lookback
int lookback = ultoscIndicator.Start(options);
double[][] outputs = { new double[hData.Length - lookback] };
ultoscIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
ValidationHelper.VerifyData(qResults, tResult, lookback, tolerance: ValidationHelper.TulipTolerance);
}
_output.WriteLine("Ultosc Streaming validated successfully against Tulip");
}
[Fact]
public void Validate_Ooples_Batch()
{
int[][] periodSets = { [7, 14, 28] };
// Prepare data for Ooples (List<TickerData>)
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
{
Date = q.Date,
Close = (double)q.Close,
High = (double)q.High,
Low = (double)q.Low,
Open = (double)q.Open,
Volume = (double)q.Volume
}).ToList();
foreach (var periods in periodSets)
{
int p1 = periods[0];
int p2 = periods[1];
int p3 = periods[2];
// Calculate QuanTAlib Ultosc (batch TBarSeries)
var ultosc = new Ultosc(p1, p2, p3);
var qResult = ultosc.Update(_testData.Bars);
// Calculate Ooples Ultimate Oscillator
var stockData = new StockData(ooplesData);
var sResult = stockData.CalculateUltimateOscillator(p1, p2, p3).OutputValues.Values.First();
// Compare last 100 records
ValidationHelper.VerifyData(qResult, sResult, (s) => s, 100, ValidationHelper.OoplesTolerance);
}
_output.WriteLine("Ultosc Batch(TBarSeries) validated successfully against Ooples");
}
[Fact]
public void Validate_Span_MatchesTBarSeries()
{
const int p1 = 7;
int p2 = 14;
int p3 = 28;
// Prepare data
double[] hData = _testData.Bars.High.Select(x => x.Value).ToArray();
double[] lData = _testData.Bars.Low.Select(x => x.Value).ToArray();
double[] cData = _testData.Bars.Close.Select(x => x.Value).ToArray();
double[] spanOutput = new double[hData.Length];
// Calculate using span method
Ultosc.Calculate(hData, lData, cData, spanOutput, p1, p2, p3);
// Calculate using TBarSeries batch
var ultosc = new Ultosc(p1, p2, p3);
var tbarResult = ultosc.Update(_testData.Bars);
// Compare results
for (int i = 0; i < tbarResult.Count; i++)
{
Assert.Equal(tbarResult[i].Value, spanOutput[i], 1e-10);
}
_output.WriteLine("Ultosc Span calculation matches TBarSeries batch calculation");
}
}
+389
View File
@@ -0,0 +1,389 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
/// <summary>
/// ULTOSC: Ultimate Oscillator
/// </summary>
/// <remarks>
/// The Ultimate Oscillator, developed by Larry Williams in 1976, is a momentum oscillator
/// that uses weighted averages of three different time periods to reduce volatility and
/// false signals inherent in single-period oscillators.
///
/// Calculation:
/// 1. Buying Pressure (BP) = Close - True Low
/// True Low = Min(Low, Previous Close)
/// 2. True Range (TR) = True High - True Low
/// True High = Max(High, Previous Close)
/// 3. Average for each period = Sum(BP) / Sum(TR)
/// 4. Ultimate Oscillator = 100 * (4*Avg7 + 2*Avg14 + Avg28) / (4 + 2 + 1)
///
/// Key Features:
/// - Three time frames reduce false signals
/// - Buying pressure concept measures demand
/// - Weighted average gives priority to shorter-term movements
///
/// Sources:
/// - Larry Williams, "The Ultimate Oscillator" (1985 Stocks & Commodities)
/// - https://www.investopedia.com/terms/u/ultimateoscillator.asp
/// </remarks>
[SkipLocalsInit]
public sealed class Ultosc : AbstractBase
{
private readonly int _period1;
private readonly int _period2;
private readonly int _period3;
private readonly RingBuffer _bp1;
private readonly RingBuffer _bp2;
private readonly RingBuffer _bp3;
private readonly RingBuffer _tr1;
private readonly RingBuffer _tr2;
private readonly RingBuffer _tr3;
private double _prevClose;
private double _p_prevClose;
private int _index;
private int _p_index;
private readonly TBarSeries? _source;
private readonly TBarPublishedHandler? _handler;
// Weights: 4:2:1
private const double Weight1 = 4.0;
private const double Weight2 = 2.0;
private const double Weight3 = 1.0;
private const double WeightSum = Weight1 + Weight2 + Weight3; // 7.0
public override bool IsHot => _index >= _period3;
/// <summary>
/// Creates Ultimate Oscillator with specified periods.
/// </summary>
/// <param name="period1">Short period (default: 7)</param>
/// <param name="period2">Intermediate period (default: 14)</param>
/// <param name="period3">Long period (default: 28)</param>
public Ultosc(int period1 = 7, int period2 = 14, int period3 = 28)
{
if (period1 <= 0)
throw new ArgumentException("Period1 must be greater than 0", nameof(period1));
if (period2 <= 0)
throw new ArgumentException("Period2 must be greater than 0", nameof(period2));
if (period3 <= 0)
throw new ArgumentException("Period3 must be greater than 0", nameof(period3));
if (period1 >= period2)
throw new ArgumentException("Period1 must be less than Period2", nameof(period1));
if (period2 >= period3)
throw new ArgumentException("Period2 must be less than Period3", nameof(period2));
_period1 = period1;
_period2 = period2;
_period3 = period3;
_bp1 = new RingBuffer(period1);
_bp2 = new RingBuffer(period2);
_bp3 = new RingBuffer(period3);
_tr1 = new RingBuffer(period1);
_tr2 = new RingBuffer(period2);
_tr3 = new RingBuffer(period3);
_prevClose = double.NaN;
_p_prevClose = double.NaN;
_index = 0;
_p_index = 0;
Name = $"Ultosc({period1},{period2},{period3})";
WarmupPeriod = period3;
}
/// <summary>
/// Creates Ultimate Oscillator with source subscription and specified periods.
/// </summary>
public Ultosc(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28) : this(period1, period2, period3)
{
_source = source;
_handler = Handle;
source.Pub += _handler;
}
protected override void Dispose(bool disposing)
{
if (disposing && _source != null && _handler != null)
{
_source.Pub -= _handler;
}
base.Dispose(disposing);
}
private void Handle(object? sender, in TBarEventArgs args)
{
Update(args.Value, args.IsNew);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar input, bool isNew = true)
{
if (isNew)
{
_p_prevClose = _prevClose;
_p_index = _index;
}
else
{
_prevClose = _p_prevClose;
_index = _p_index;
}
double high = input.High;
double low = input.Low;
double close = input.Close;
// Handle invalid inputs
if (!double.IsFinite(high) || !double.IsFinite(low) || !double.IsFinite(close))
{
Last = new TValue(input.Time, Last.Value);
PubEvent(Last, isNew);
return Last;
}
double bp, tr;
if (double.IsNaN(_prevClose))
{
// First bar: True Range = High - Low, BP = Close - Low
bp = close - low;
tr = high - low;
}
else
{
// True Low = Min(Low, Previous Close)
double trueLow = Math.Min(low, _prevClose);
// True High = Max(High, Previous Close)
double trueHigh = Math.Max(high, _prevClose);
// Buying Pressure = Close - True Low
bp = close - trueLow;
// True Range = True High - True Low
tr = trueHigh - trueLow;
}
// Add to all three period buffers
_bp1.Add(bp, isNew);
_bp2.Add(bp, isNew);
_bp3.Add(bp, isNew);
_tr1.Add(tr, isNew);
_tr2.Add(tr, isNew);
_tr3.Add(tr, isNew);
if (isNew)
{
_prevClose = close;
_index++;
}
// Calculate sums
double bpSum1 = _bp1.Sum();
double bpSum2 = _bp2.Sum();
double bpSum3 = _bp3.Sum();
double trSum1 = _tr1.Sum();
double trSum2 = _tr2.Sum();
double trSum3 = _tr3.Sum();
// Calculate averages (handle division by zero)
const double epsilon = 1e-10;
double avg1 = trSum1 > epsilon ? bpSum1 / trSum1 : 0.5;
double avg2 = trSum2 > epsilon ? bpSum2 / trSum2 : 0.5;
double avg3 = trSum3 > epsilon ? bpSum3 / trSum3 : 0.5;
// Ultimate Oscillator = 100 * (4*Avg1 + 2*Avg2 + Avg3) / 7
double ultosc = 100.0 * Math.FusedMultiplyAdd(Weight1, avg1, Math.FusedMultiplyAdd(Weight2, avg2, Weight3 * avg3)) / WeightSum;
Last = new TValue(input.Time, ultosc);
PubEvent(Last, isNew);
return Last;
}
/// <summary>
/// Update for TValue input - not recommended for Ultimate Oscillator as it needs OHLC.
/// This method will return 50 (neutral) since proper calculation requires OHLC data.
/// </summary>
public override TValue Update(TValue input, bool isNew = true)
{
// Ultimate Oscillator requires OHLC data
// Return neutral value if called with TValue
Last = new TValue(input.Time, 50.0);
PubEvent(Last, isNew);
return Last;
}
public TSeries Update(TBarSeries source)
{
if (source.Count == 0) return [];
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
// Calculate using span method
Calculate(source.High.Values, source.Low.Values, source.Close.Values,
vSpan, _period1, _period2, _period3);
source.Times.CopyTo(tSpan);
// Restore state for streaming
Reset();
for (int i = 0; i < len; i++)
{
Update(source[i]);
}
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
}
public override TSeries Update(TSeries source)
{
// Cannot properly calculate Ultimate Oscillator from single-value series
// Return series of neutral values
if (source.Count == 0) return [];
var t = new List<long>(source.Count);
var v = new List<double>(source.Count);
for (int i = 0; i < source.Count; i++)
{
t.Add(source.Times[i]);
v.Add(50.0);
}
return new TSeries(t, v);
}
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
{
// Cannot properly prime Ultimate Oscillator from single-value array
// This method is a no-op for OHLC indicators
}
public static TSeries Batch(TBarSeries source, int period1 = 7, int period2 = 14, int period3 = 28)
{
var ultosc = new Ultosc(period1, period2, period3);
return ultosc.Update(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(
ReadOnlySpan<double> high,
ReadOnlySpan<double> low,
ReadOnlySpan<double> close,
Span<double> output,
int period1 = 7,
int period2 = 14,
int period3 = 28)
{
int len = high.Length;
if (len != low.Length || len != close.Length || len != output.Length)
throw new ArgumentException("All arrays must have the same length", nameof(output));
if (period1 <= 0)
throw new ArgumentException("Period1 must be greater than 0", nameof(period1));
if (period2 <= 0)
throw new ArgumentException("Period2 must be greater than 0", nameof(period2));
if (period3 <= 0)
throw new ArgumentException("Period3 must be greater than 0", nameof(period3));
if (period1 >= period2)
throw new ArgumentException("Period1 must be less than Period2", nameof(period1));
if (period2 >= period3)
throw new ArgumentException("Period2 must be less than Period3", nameof(period2));
if (len == 0) return;
// Allocate buffers for BP and TR
double[] bpArray = System.Buffers.ArrayPool<double>.Shared.Rent(len);
double[] trArray = System.Buffers.ArrayPool<double>.Shared.Rent(len);
try
{
Span<double> bp = bpArray.AsSpan(0, len);
Span<double> tr = trArray.AsSpan(0, len);
// First bar
bp[0] = close[0] - low[0];
tr[0] = high[0] - low[0];
// Calculate BP and TR for remaining bars
for (int i = 1; i < len; i++)
{
double h = high[i];
double l = low[i];
double c = close[i];
double prevC = close[i - 1];
double trueLow = Math.Min(l, prevC);
double trueHigh = Math.Max(h, prevC);
bp[i] = c - trueLow;
tr[i] = trueHigh - trueLow;
}
// Calculate running sums and output
double bpSum1 = 0, bpSum2 = 0, bpSum3 = 0;
double trSum1 = 0, trSum2 = 0, trSum3 = 0;
const double epsilon = 1e-10;
for (int i = 0; i < len; i++)
{
// Add current values
bpSum1 += bp[i];
bpSum2 += bp[i];
bpSum3 += bp[i];
trSum1 += tr[i];
trSum2 += tr[i];
trSum3 += tr[i];
// Remove old values for each period window
if (i >= period1)
{
bpSum1 -= bp[i - period1];
trSum1 -= tr[i - period1];
}
if (i >= period2)
{
bpSum2 -= bp[i - period2];
trSum2 -= tr[i - period2];
}
if (i >= period3)
{
bpSum3 -= bp[i - period3];
trSum3 -= tr[i - period3];
}
// Calculate averages
double avg1 = trSum1 > epsilon ? bpSum1 / trSum1 : 0.5;
double avg2 = trSum2 > epsilon ? bpSum2 / trSum2 : 0.5;
double avg3 = trSum3 > epsilon ? bpSum3 / trSum3 : 0.5;
// Ultimate Oscillator
output[i] = 100.0 * Math.FusedMultiplyAdd(Weight1, avg1, Math.FusedMultiplyAdd(Weight2, avg2, Weight3 * avg3)) / WeightSum;
}
}
finally
{
System.Buffers.ArrayPool<double>.Shared.Return(bpArray);
System.Buffers.ArrayPool<double>.Shared.Return(trArray);
}
}
public override void Reset()
{
_bp1.Clear();
_bp2.Clear();
_bp3.Clear();
_tr1.Clear();
_tr2.Clear();
_tr3.Clear();
_prevClose = double.NaN;
_p_prevClose = double.NaN;
_index = 0;
_p_index = 0;
Last = default;
}
}
+134
View File
@@ -0,0 +1,134 @@
# UltOsc: Ultimate Oscillator
> "Why use one timeframe when three can save you from yourself?"
The Ultimate Oscillator is Larry Williams' answer to the fundamental flaw of single-period momentum oscillators: they whipsaw. By combining buying pressure across three distinct timeframes with a weighted average, UltOsc filters out the noise that traps traders who rely on RSI or Stochastics alone.
The indicator oscillates between 0 and 100. Readings above 70 suggest overbought conditions; readings below 30 suggest oversold. But the real power lies in **divergence detection**: when price makes a new high but UltOsc does not, the trend is exhausted.
## Historical Context
Larry Williams introduced the Ultimate Oscillator in his 1985 article for *Technical Analysis of Stocks & Commodities* magazine. Williams, a legendary trader who famously turned \$10,000 into over \$1 million in a single year of trading, designed UltOsc to solve a specific problem.
Single-period oscillators like RSI suffer from two fatal flaws:
1. **False signals during trends**: In a strong uptrend, RSI can stay overbought for weeks, generating endless "sell" signals.
2. **Period sensitivity**: A 7-period RSI behaves differently from a 14-period RSI. Which one is "right"?
Williams' solution was elegant: use three periods (7, 14, 28) and weight them so the shortest period has the most influence (4:2:1). This gives responsiveness to recent price action while still respecting the broader context.
## Architecture & Physics
UltOsc is built on two core concepts: **Buying Pressure (BP)** and **True Range (TR)**.
### Buying Pressure
Buying Pressure measures how much of today's price movement was "bought." It is the distance from the True Low (the lower of today's Low or yesterday's Close) to today's Close.
$$
BP = Close - TrueLow
$$
If the close is at the high of the day, BP is maximized. If the close is at the low, BP is zero.
### True Range
True Range captures the full volatility of the day, including overnight gaps.
$$
TR = TrueHigh - TrueLow
$$
Where:
- $TrueHigh = \max(High, Close_{t-1})$
- $TrueLow = \min(Low, Close_{t-1})$
### The Multi-Timeframe Fusion
For each of the three periods, UltOsc calculates the ratio of accumulated Buying Pressure to accumulated True Range:
$$
Avg_n = \frac{\sum_{i=1}^{n} BP_i}{\sum_{i=1}^{n} TR_i}
$$
This ratio represents the "efficiency" of buying over that period. A value of 1.0 means all volatility was captured by buyers; 0.0 means sellers dominated.
The final oscillator applies a 4:2:1 weighting:
$$
UltOsc = 100 \times \frac{4 \times Avg_7 + 2 \times Avg_{14} + 1 \times Avg_{28}}{4 + 2 + 1}
$$
## Mathematical Foundation
### 1. True Low and True High
$$
TrueLow_t = \min(Low_t, Close_{t-1})
$$
$$
TrueHigh_t = \max(High_t, Close_{t-1})
$$
### 2. Buying Pressure and True Range
$$
BP_t = Close_t - TrueLow_t
$$
$$
TR_t = TrueHigh_t - TrueLow_t
$$
### 3. Period Averages
For periods $n_1 = 7$, $n_2 = 14$, $n_3 = 28$:
$$
Avg_n = \frac{\sum_{i=t-n+1}^{t} BP_i}{\sum_{i=t-n+1}^{t} TR_i}
$$
### 4. Ultimate Oscillator
$$
UltOsc = 100 \times \frac{4 \cdot Avg_7 + 2 \cdot Avg_{14} + 1 \cdot Avg_{28}}{7}
$$
## Performance Profile
| Metric | Score | Notes |
| :--- | :--- | :--- |
| **Throughput** | 8 | Moderate; requires six running sums (BP and TR for each period). |
| **Allocations** | 0 | Zero-allocation in hot paths using ring buffers. |
| **Complexity** | O(1) | Constant time via running sums. |
| **Accuracy** | 10 | Matches TA-Lib and Skender exactly. |
| **Timeliness** | 6 | Balanced; short-period weighting provides responsiveness. |
| **Overshoot** | 2 | Bounded to [0, 100]; minimal overshoot by design. |
| **Smoothness** | 7 | Multi-period averaging provides inherent smoothing. |
## Validation
| Library | Status | Notes |
| :--- | :--- | :--- |
| **QuanTAlib** | ✅ | Validated. |
| **TA-Lib** | ✅ | Matches `TA_ULTOSC` exactly. |
| **Skender** | ✅ | Matches `GetUltimate` exactly. |
| **Tulip** | ✅ | Matches `ultosc` exactly. |
| **Ooples** | ⚠️ | Minor deviations in warmup period handling. |
### Trading Signals
Williams outlined specific rules for trading UltOsc:
1. **Bullish Divergence**: Price makes a lower low, UltOsc makes a higher low (UltOsc < 30).
2. **Breakout Confirmation**: After divergence, UltOsc breaks above the divergence high.
3. **Exit**: UltOsc reaches 70, or price hits target.
### Common Pitfalls
- **Ignoring Divergence**: UltOsc is designed for divergence trading. Using it as a simple overbought/oversold indicator misses the point.
- **Wrong Timeframes**: The default 7/14/28 works for daily charts. For intraday, consider scaling down proportionally.
- **Trending Markets**: Like all oscillators, UltOsc struggles in strong trends. Use trend filters (ADX, moving averages) to avoid fighting the tide.
- **Division by Zero**: If True Range is zero (flat line), the ratio is undefined. QuanTAlib handles this by returning 0.5 (neutral).
+39
View File
@@ -0,0 +1,39 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("Williams %R (WILLR)", "WILLR", overlay=false)
//@function Calculates Williams %R oscillator
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/oscillators/willr.md
//@param period Lookback period for highest high and lowest low calculation
//@returns Williams %R value (-100 to 0 scale)
willr(simple int period) =>
if period <= 0
runtime.error("Period must be positive")
var array<float> high_buffer = array.new_float(period, na)
var array<float> low_buffer = array.new_float(period, na)
var int head = 0, var int count = 0
idx = head % period
old_high = array.get(high_buffer, idx)
if not na(old_high)
count := count - 1
array.set(high_buffer, idx, high)
array.set(low_buffer, idx, low)
count := count + 1
head := head + 1
highest_high = array.max(high_buffer)
lowest_low = array.min(low_buffer)
range_val = highest_high - lowest_low
range_val > 0 ? -100 * (highest_high - close) / range_val : -50
// ---------- Main loop ----------
// Inputs
i_period = input.int(14, "Period", minval=1, maxval=100, tooltip="Lookback period for highest high and lowest low calculation")
// Calculation
willr_value = willr(i_period)
// Plots
plot(willr_value, "Williams %R", color=color.yellow, linewidth=2)