using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace QuanTAlib;
///
/// TTM_LRC: TTM Linear Regression Channel
/// John Carter's Linear Regression Channel with ±1σ and ±2σ standard deviation bands.
///
///
/// The TTM LRC provides a clean, statistically-based price channel using linear regression
/// analysis. Unlike Bollinger Bands which measure volatility around a moving average, LRC
/// measures price deviation from the trend line, making it particularly useful for identifying
/// overbought/oversold conditions within a defined trend.
///
/// Calculation:
/// 1. Compute linear regression line: y = mx + b using least squares over N periods
/// 2. Calculate residuals: residual_i = y_i - predicted_i
/// 3. Compute standard deviation of residuals: σ = √(Σ(residual²) / N)
/// 4. Inner bands: ±1σ (68% of prices)
/// 5. Outer bands: ±2σ (95% of prices)
///
/// Key characteristics:
/// - Middle line is the linear regression endpoint (LSMA)
/// - Dual band pairs for statistical significance levels
/// - Slope indicates trend direction and strength
/// - R² indicates trend quality (higher = cleaner trend)
/// - Price at ±2σ suggests extreme deviation from trend
///
/// Sources:
/// John Carter's TTM Indicators
/// https://school.stockcharts.com/doku.php?id=technical_indicators:raff_regression_channel
///
[SkipLocalsInit]
public sealed class TtmLrc : ITValuePublisher
{
private readonly int _period;
// Precomputed constants for linear regression
private readonly double _sumX; // sum of x indices: 0 + 1 + ... + (n-1)
private readonly double _denominator; // n * sumX² - sumX²
// Ring buffer for values
private readonly double[] _buffer;
private double[]? _p_buffer;
[StructLayout(LayoutKind.Auto)]
private record struct State(
int Head,
int Count,
double LastValid,
double Slope,
double StdDev,
double RSquared,
bool IsHot);
private State _state;
private State _p_state;
private readonly TValuePublishedHandler _valueHandler;
public string Name { get; }
public int WarmupPeriod { get; }
///
/// The linear regression line value (trend center)
///
public TValue Midline { get; private set; }
///
/// Upper band at +1 standard deviation
///
public TValue Upper1 { get; private set; }
///
/// Lower band at -1 standard deviation
///
public TValue Lower1 { get; private set; }
///
/// Upper band at +2 standard deviations
///
public TValue Upper2 { get; private set; }
///
/// Lower band at -2 standard deviations
///
public TValue Lower2 { get; private set; }
///
/// Primary output (Midline) for compatibility with AbstractBase
///
public TValue Last => Midline;
public bool IsHot => _state.IsHot;
///
/// The slope of the linear regression line (trend direction)
/// Positive = uptrend, Negative = downtrend
///
public double Slope => _state.Slope;
///
/// The standard deviation of residuals (price dispersion around trend)
///
public double StdDev => _state.StdDev;
///
/// Coefficient of determination (R²) measuring trend quality.
/// Range: 0 to 1. Higher values indicate a cleaner, more reliable trend.
/// R² > 0.8 suggests strong linear trend.
///
public double RSquared => _state.RSquared;
public event TValuePublishedHandler? Pub;
///
/// Initializes a new instance of the TTM Linear Regression Channel indicator.
///
/// Lookback period for regression (default 100, must be > 1)
public TtmLrc(int period = 100)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
_period = period;
_buffer = new double[period];
_p_buffer = new double[period];
WarmupPeriod = period;
Name = $"TtmLrc({period})";
_valueHandler = HandleValue;
// Precompute constants
// sumX = 0 + 1 + ... + (n-1) = n(n-1)/2
_sumX = 0.5 * period * (period - 1);
// sumX² = 0² + 1² + ... + (n-1)² = (n-1)n(2n-1)/6
double sumX2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
// denominator = n * sumX² - sumX²
_denominator = period * sumX2 - _sumX * _sumX;
Reset();
}
public TtmLrc(TSeries source, int period = 100) : this(period)
{
Prime(source);
source.Pub += _valueHandler;
}
private void HandleValue(object? sender, in TValueEventArgs e) => Update(e.Value, e.IsNew);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void PubEvent(TValue value, bool isNew = true) =>
Pub?.Invoke(this, new TValueEventArgs { Value = value, IsNew = isNew });
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_state = new State(0, 0, double.NaN, 0, 0, 0, false);
_p_state = _state;
Array.Fill(_buffer, 0.0);
_p_buffer = (double[])_buffer.Clone();
Midline = default;
Upper1 = default;
Lower1 = default;
Upper2 = default;
Lower2 = default;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValid(double value, bool isNew)
{
if (double.IsFinite(value))
{
// Always update LastValid on finite input (including bar corrections)
_state = _state with { LastValid = value };
return value;
}
return double.IsFinite(_state.LastValid) ? _state.LastValid : 0.0;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
_p_state = _state;
Array.Copy(_buffer, _p_buffer!, _period);
}
else
{
_state = _p_state;
Array.Copy(_p_buffer!, _buffer, _period);
}
double value = GetValid(input.Value, isNew);
// Add to ring buffer
int count = _state.Count;
int head = _state.Head;
if (count < _period)
{
count++;
}
_buffer[head] = value;
int newHead = (head + 1) % _period;
if (isNew)
{
_state = _state with { Head = newHead, Count = count };
}
// Calculate linear regression and std dev of residuals
if (count <= 1)
{
Midline = new TValue(input.Time, value);
Upper1 = new TValue(input.Time, value);
Lower1 = new TValue(input.Time, value);
Upper2 = new TValue(input.Time, value);
Lower2 = new TValue(input.Time, value);
_state = _state with { Slope = 0, StdDev = 0, RSquared = 0 };
PubEvent(Midline, isNew);
return Midline;
}
// Build span of values in chronological order (oldest to newest)
Span values = stackalloc double[count];
int readHead = (newHead - count + _period) % _period;
for (int i = 0; i < count; i++)
{
values[i] = _buffer[(readHead + i) % _period];
}
// Calculate sums for linear regression
double sumY = 0;
double sumXY = 0;
for (int i = 0; i < count; i++)
{
sumY += values[i];
sumXY += i * values[i];
}
double n = count;
double sx = _sumX;
double denom = _denominator;
// Adjust for partial window during warmup
if (count < _period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
// Regression value at current point (x = count - 1)
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Calculate standard deviation of residuals and R²
double sumResiduals2 = 0;
double meanY = sumY / n;
double ssTot = 0;
for (int i = 0; i < count; i++)
{
double predicted = Math.FusedMultiplyAdd(slope, i, intercept);
double residual = values[i] - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
double devFromMean = values[i] - meanY;
ssTot = Math.FusedMultiplyAdd(devFromMean, devFromMean, ssTot);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
// Compute R² (coefficient of determination)
double rSquared = ssTot > 1e-10 ? 1.0 - (sumResiduals2 / ssTot) : 0.0;
rSquared = Math.Clamp(rSquared, 0.0, 1.0);
if (!_state.IsHot && count >= WarmupPeriod)
{
_state = _state with { IsHot = true };
}
_state = _state with { Slope = slope, StdDev = stdDev, RSquared = rSquared };
Midline = new TValue(input.Time, regression);
Upper1 = new TValue(input.Time, regression + stdDev);
Lower1 = new TValue(input.Time, regression - stdDev);
Upper2 = new TValue(input.Time, regression + 2.0 * stdDev);
Lower2 = new TValue(input.Time, regression - 2.0 * stdDev);
PubEvent(Midline, isNew);
return Midline;
}
public (TSeries Midline, TSeries Upper1, TSeries Lower1, TSeries Upper2, TSeries Lower2) Update(TSeries source)
{
if (source.Count == 0)
{
return (new TSeries([], []), new TSeries([], []), new TSeries([], []), new TSeries([], []), new TSeries([], []));
}
int len = source.Count;
var tMid = new List(len);
var vMid = new List(len);
var vU1 = new List(len);
var vL1 = new List(len);
var vU2 = new List(len);
var vL2 = new List(len);
CollectionsMarshal.SetCount(tMid, len);
CollectionsMarshal.SetCount(vMid, len);
CollectionsMarshal.SetCount(vU1, len);
CollectionsMarshal.SetCount(vL1, len);
CollectionsMarshal.SetCount(vU2, len);
CollectionsMarshal.SetCount(vL2, len);
var tSpan = CollectionsMarshal.AsSpan(tMid);
var vMidSpan = CollectionsMarshal.AsSpan(vMid);
var vU1Span = CollectionsMarshal.AsSpan(vU1);
var vL1Span = CollectionsMarshal.AsSpan(vL1);
var vU2Span = CollectionsMarshal.AsSpan(vU2);
var vL2Span = CollectionsMarshal.AsSpan(vL2);
Batch(source.Values, vMidSpan, vU1Span, vL1Span, vU2Span, vL2Span, _period);
source.Times.CopyTo(tSpan);
// Prime internal state for continued streaming
Prime(source);
var lastTime = new DateTime(source.Times[^1], DateTimeKind.Utc);
Midline = new TValue(lastTime, vMidSpan[^1]);
Upper1 = new TValue(lastTime, vU1Span[^1]);
Lower1 = new TValue(lastTime, vL1Span[^1]);
Upper2 = new TValue(lastTime, vU2Span[^1]);
Lower2 = new TValue(lastTime, vL2Span[^1]);
return (
new TSeries(tMid, vMid),
new TSeries(new List(tMid), vU1),
new TSeries(new List(tMid), vL1),
new TSeries(new List(tMid), vU2),
new TSeries(new List(tMid), vL2)
);
}
public void Prime(TSeries source)
{
Reset();
if (source.Count == 0)
{
return;
}
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
///
/// Batch calculation using spans. Outputs midline and all four bands.
///
public static void Batch(
ReadOnlySpan source,
Span midline,
Span upper1,
Span lower1,
Span upper2,
Span lower2,
int period)
{
if (period <= 1)
{
throw new ArgumentOutOfRangeException(nameof(period), "Period must be greater than 1.");
}
if (midline.Length < source.Length ||
upper1.Length < source.Length ||
lower1.Length < source.Length ||
upper2.Length < source.Length ||
lower2.Length < source.Length)
{
throw new ArgumentException("Output spans must be at least as long as input", nameof(midline));
}
int len = source.Length;
if (len == 0)
{
return;
}
// Precompute constants for full period
double sumXFull = 0.5 * period * (period - 1);
double sumX2Full = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
double denomFull = period * sumX2Full - sumXFull * sumXFull;
// Track last valid value for NaN substitution
double lastValid = double.NaN;
for (int i = 0; i < len; i++)
{
// Get valid value with last-valid substitution
double currentValue = source[i];
if (double.IsFinite(currentValue))
{
lastValid = currentValue;
}
else
{
currentValue = lastValid;
}
// If still NaN (no valid value seen yet), output NaN
if (!double.IsFinite(currentValue))
{
midline[i] = double.NaN;
upper1[i] = double.NaN;
lower1[i] = double.NaN;
upper2[i] = double.NaN;
lower2[i] = double.NaN;
continue;
}
int count = Math.Min(i + 1, period);
int start = i - count + 1;
if (count <= 1)
{
midline[i] = currentValue;
upper1[i] = currentValue;
lower1[i] = currentValue;
upper2[i] = currentValue;
lower2[i] = currentValue;
continue;
}
// Calculate sums for linear regression with NaN handling
double sumY = 0;
double sumXY = 0;
double lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
{
lastValidInWindow = rawY;
y = rawY;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
}
sumY += y;
sumXY += j * y;
}
double n = count;
double sx, denom;
if (count < period)
{
sx = 0.5 * n * (n - 1);
double sx2 = (n - 1.0) * n * (2.0 * n - 1.0) / 6.0;
denom = n * sx2 - sx * sx;
}
else
{
sx = sumXFull;
denom = denomFull;
}
double slope, intercept, regression;
if (Math.Abs(denom) < 1e-10)
{
slope = 0;
intercept = sumY / n;
regression = intercept;
}
else
{
slope = (n * sumXY - sx * sumY) / denom;
intercept = (sumY - slope * sx) / n;
regression = Math.FusedMultiplyAdd(slope, count - 1, intercept);
}
// Calculate standard deviation of residuals with NaN handling
double sumResiduals2 = 0;
lastValidInWindow = double.NaN;
for (int j = 0; j < count; j++)
{
double rawY = source[start + j];
double y;
if (double.IsFinite(rawY))
{
lastValidInWindow = rawY;
y = rawY;
}
else
{
y = double.IsFinite(lastValidInWindow) ? lastValidInWindow : 0.0;
}
double predicted = Math.FusedMultiplyAdd(slope, j, intercept);
double residual = y - predicted;
sumResiduals2 = Math.FusedMultiplyAdd(residual, residual, sumResiduals2);
}
double stdDev = Math.Sqrt(sumResiduals2 / n);
midline[i] = regression;
upper1[i] = regression + stdDev;
lower1[i] = regression - stdDev;
upper2[i] = regression + 2.0 * stdDev;
lower2[i] = regression - 2.0 * stdDev;
}
}
public static (TSeries Midline, TSeries Upper1, TSeries Lower1, TSeries Upper2, TSeries Lower2) Batch(TSeries source, int period = 100)
{
int len = source.Count;
var tMid = new List(len);
var vMid = new List(len);
var vU1 = new List(len);
var vL1 = new List(len);
var vU2 = new List(len);
var vL2 = new List(len);
CollectionsMarshal.SetCount(tMid, len);
CollectionsMarshal.SetCount(vMid, len);
CollectionsMarshal.SetCount(vU1, len);
CollectionsMarshal.SetCount(vL1, len);
CollectionsMarshal.SetCount(vU2, len);
CollectionsMarshal.SetCount(vL2, len);
Batch(source.Values,
CollectionsMarshal.AsSpan(vMid),
CollectionsMarshal.AsSpan(vU1),
CollectionsMarshal.AsSpan(vL1),
CollectionsMarshal.AsSpan(vU2),
CollectionsMarshal.AsSpan(vL2),
period);
source.Times.CopyTo(CollectionsMarshal.AsSpan(tMid));
return (
new TSeries(tMid, vMid),
new TSeries(new List(tMid), vU1),
new TSeries(new List(tMid), vL1),
new TSeries(new List(tMid), vU2),
new TSeries(new List(tMid), vL2)
);
}
public static ((TSeries Midline, TSeries Upper1, TSeries Lower1, TSeries Upper2, TSeries Lower2) Results, TtmLrc Indicator) Calculate(TSeries source, int period = 100)
{
var indicator = new TtmLrc(period);
var results = indicator.Update(source);
return (results, indicator);
}
}