normalization of methods

This commit is contained in:
Miha Kralj
2026-02-10 21:33:16 -08:00
parent 915d7a007b
commit 6d6259a47d
527 changed files with 10525 additions and 2123 deletions
@@ -0,0 +1,111 @@
using TradingPlatform.BusinessLayer;
using Xunit;
namespace QuanTAlib.Tests;
public class TtmTrendIndicatorTests
{
[Fact]
public void Constructor_CreatesValidIndicator()
{
var indicator = new TtmTrendIndicator();
Assert.NotNull(indicator);
Assert.Equal("TTM Trend", indicator.Name);
}
[Fact]
public void DefaultPeriod_Is6()
{
var indicator = new TtmTrendIndicator();
Assert.Equal(6, indicator.Period);
}
[Fact]
public void ShortName_IncludesParameters()
{
var indicator = new TtmTrendIndicator { Period = 10 };
Assert.Equal("TTM_TREND(10)", indicator.ShortName);
}
[Fact]
public void MinHistoryDepths_EqualsZero()
{
var indicator = new TtmTrendIndicator { Period = 10 };
Assert.Equal(0, TtmTrendIndicator.MinHistoryDepths);
IWatchlistIndicator watchlistIndicator = indicator;
Assert.Equal(0, watchlistIndicator.MinHistoryDepths);
}
[Fact]
public void SeparateWindow_IsFalse()
{
var indicator = new TtmTrendIndicator();
Assert.False(indicator.SeparateWindow);
}
[Fact]
public void OnBackGround_IsTrue()
{
var indicator = new TtmTrendIndicator();
Assert.True(indicator.OnBackGround);
}
[Fact]
public void CalculationIntegration_ProducesCorrectValues()
{
var ttmCore = new TtmTrend(6);
var time = DateTime.UtcNow;
var bar1 = new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000);
var bar2 = new TBar(time.AddMinutes(1).Ticks, 102.0, 108.0, 100.0, 106.0, 1000);
ttmCore.Update(bar1);
var result = ttmCore.Update(bar2);
// After 2 bars, should be hot and have valid value
Assert.True(ttmCore.IsHot);
Assert.True(double.IsFinite(result.Value));
}
[Fact]
public void TrendDirection_Bullish_WhenRising()
{
var ttmCore = new TtmTrend(6);
var time = DateTime.UtcNow;
ttmCore.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000));
ttmCore.Update(new TBar(time.AddMinutes(1).Ticks, 110.0, 115.0, 108.0, 112.0, 1000));
Assert.Equal(1, ttmCore.Trend);
}
[Fact]
public void TrendDirection_Bearish_WhenFalling()
{
var ttmCore = new TtmTrend(6);
var time = DateTime.UtcNow;
ttmCore.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000));
ttmCore.Update(new TBar(time.AddMinutes(1).Ticks, 90.0, 95.0, 88.0, 92.0, 1000));
Assert.Equal(-1, ttmCore.Trend);
}
[Fact]
public void CoreIndicator_ResetsCorrectly()
{
var ttm = new TtmTrend(6);
var time = DateTime.UtcNow;
ttm.Update(new TBar(time.Ticks, 100.0, 105.0, 98.0, 102.0, 1000));
ttm.Update(new TBar(time.AddMinutes(1).Ticks, 102.0, 108.0, 100.0, 106.0, 1000));
Assert.True(ttm.IsHot);
ttm.Reset();
Assert.False(ttm.IsHot);
Assert.Equal(default, ttm.Last);
Assert.Equal(0, ttm.Trend);
}
}
@@ -0,0 +1,66 @@
using System.Drawing;
using System.Runtime.CompilerServices;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
[SkipLocalsInit]
public sealed class TtmTrendIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 0, 1, 100, 1, 0)]
public int Period { get; set; } = 6;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
public override string ShortName => $"TTM_TREND({Period})";
public static int MinHistoryDepths => 0;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
private TtmTrend _indicator = null!;
private readonly LineSeries _series;
public TtmTrendIndicator()
{
Name = "TTM Trend";
Description = "John Carter's TTM Trend - EMA-based trend indicator with color-coded direction.";
_series = new LineSeries("TTM Trend", Color.Gray, 3, LineStyle.Solid);
AddLineSeries(_series);
SeparateWindow = false;
OnBackGround = true;
}
protected override void OnInit()
{
_indicator = new TtmTrend(Period);
base.OnInit();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override void OnUpdate(UpdateArgs args)
{
bool isNew = args.IsNewBar();
var bar = this.GetInputBar(args);
var result = _indicator.Update(bar, isNew);
_series.SetValue(result.Value, _indicator.IsHot, ShowColdValues);
// Color based on trend direction
if (_indicator.IsHot)
{
Color trendColor = _indicator.Trend switch
{
1 => Color.Green,
-1 => Color.Red,
_ => Color.Gray
};
_series.SetMarker(0, trendColor);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
}
}
+469
View File
@@ -0,0 +1,469 @@
// TTM_TREND Tests - John Carter's TTM Trend Indicator
using Xunit;
namespace QuanTAlib.Tests;
// ═══════════════════════════════════════════════════════════════════════════
// Constructor Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendConstructorTests
{
[Fact]
public void Constructor_DefaultPeriod_Is6()
{
var ttm = new TtmTrend();
Assert.Equal(6, ttm.Period);
}
[Fact]
public void Constructor_CustomPeriod_IsSet()
{
var ttm = new TtmTrend(period: 10);
Assert.Equal(10, ttm.Period);
}
[Theory]
[InlineData(0)]
[InlineData(-1)]
[InlineData(-10)]
public void Constructor_InvalidPeriod_Throws(int period)
{
Assert.Throws<ArgumentException>(() => new TtmTrend(period));
}
[Fact]
public void Constructor_MinPeriod_IsValid()
{
var ttm = new TtmTrend(period: 1);
Assert.Equal(1, ttm.Period);
}
[Fact]
public void Name_ContainsPeriod()
{
var ttm = new TtmTrend(period: 10);
Assert.Contains("10", ttm.Name, StringComparison.Ordinal);
Assert.Contains("TTM_TREND", ttm.Name, StringComparison.Ordinal);
}
[Fact]
public void WarmupPeriod_Is2()
{
Assert.Equal(2, TtmTrend.WarmupPeriod);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Basic Operation Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendBasicTests
{
[Fact]
public void Update_FirstBar_ReturnsValue()
{
var ttm = new TtmTrend();
var result = ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
Assert.Equal(100.0, result.Value);
}
[Fact]
public void Update_SecondBar_CalculatesEma()
{
var ttm = new TtmTrend(period: 6); // alpha = 2/7 ≈ 0.2857
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
var result = ttm.Update(new TValue(time.AddMinutes(1).Ticks, 107.0));
// EMA = alpha * value + (1 - alpha) * prevEMA
// EMA = 0.2857 * 107 + 0.7143 * 100 = 30.57 + 71.43 = 102.0
double alpha = 2.0 / 7.0;
double expected = alpha * 107.0 + (1 - alpha) * 100.0;
Assert.Equal(expected, result.Value, 10);
}
[Fact]
public void IsHot_AfterFirstBar_IsFalse()
{
var ttm = new TtmTrend();
ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
Assert.False(ttm.IsHot);
}
[Fact]
public void IsHot_AfterSecondBar_IsTrue()
{
var ttm = new TtmTrend();
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 101.0));
Assert.True(ttm.IsHot);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Trend Direction Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendDirectionTests
{
[Fact]
public void Trend_RisingValues_IsBullish()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
Assert.Equal(1, ttm.Trend);
}
[Fact]
public void Trend_FallingValues_IsBearish()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 90.0));
Assert.Equal(-1, ttm.Trend);
}
[Fact]
public void Trend_SameValue_IsNeutral()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 100.0));
Assert.Equal(0, ttm.Trend);
}
[Fact]
public void Trend_CanChangeDirection()
{
var ttm = new TtmTrend(period: 2); // Fast EMA
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
Assert.Equal(1, ttm.Trend);
// Drop significantly to reverse trend
ttm.Update(new TValue(time.AddMinutes(2).Ticks, 90.0));
Assert.Equal(-1, ttm.Trend);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Strength Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendStrengthTests
{
[Fact]
public void Strength_IsPositive()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
Assert.True(ttm.Strength > 0);
}
[Fact]
public void Strength_ZeroOnFirstBar()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
Assert.Equal(0, ttm.Strength);
}
[Fact]
public void Strength_LargerMoves_HigherStrength()
{
var ttm1 = new TtmTrend(period: 6);
var ttm2 = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
// Small move
ttm1.Update(new TValue(time.Ticks, 100.0));
ttm1.Update(new TValue(time.AddMinutes(1).Ticks, 101.0));
// Large move
ttm2.Update(new TValue(time.Ticks, 100.0));
ttm2.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
Assert.True(ttm2.Strength > ttm1.Strength);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Bar Input Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendBarInputTests
{
[Fact]
public void Update_Bar_UsesTypicalPrice()
{
var ttm = new TtmTrend(period: 6);
var bar = new TBar(DateTime.UtcNow.Ticks, 100.0, 105.0, 98.0, 102.0, 1000);
var result = ttm.Update(bar);
// Typical price = (H + L + C) / 3 = (105 + 98 + 102) / 3 = 101.67
double typical = (105.0 + 98.0 + 102.0) / 3.0;
Assert.Equal(typical, result.Value, 10);
}
[Fact]
public void Update_BarSeries_ReturnsCorrectLength()
{
var ttm = new TtmTrend(period: 6);
var bars = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0, 105.0, 95.0, 102.0, 1000));
}
var result = ttm.Update(bars);
Assert.Equal(10, result.Count);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Edge Case Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendEdgeCaseTests
{
[Fact]
public void Update_NaN_ReturnsLastValue()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
var result1 = ttm.Update(new TValue(time.Ticks, 100.0));
var result2 = ttm.Update(new TValue(time.AddMinutes(1).Ticks, double.NaN));
Assert.Equal(result1.Value, result2.Value);
}
[Fact]
public void Update_Infinity_ReturnsLastValue()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
var result1 = ttm.Update(new TValue(time.Ticks, 100.0));
var result2 = ttm.Update(new TValue(time.AddMinutes(1).Ticks, double.PositiveInfinity));
Assert.Equal(result1.Value, result2.Value);
}
[Fact]
public void Update_LargeValues_CalculatesCorrectly()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
var result = ttm.Update(new TValue(time.Ticks, 1e10));
Assert.True(double.IsFinite(result.Value));
Assert.Equal(1e10, result.Value);
}
[Fact]
public void Update_SmallValues_CalculatesCorrectly()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
var result = ttm.Update(new TValue(time.Ticks, 1e-10));
Assert.True(double.IsFinite(result.Value));
Assert.Equal(1e-10, result.Value);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Reset Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendResetTests
{
[Fact]
public void Reset_ClearsState()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
Assert.True(ttm.IsHot);
ttm.Reset();
Assert.False(ttm.IsHot);
Assert.Equal(default, ttm.Last);
Assert.Equal(0, ttm.Trend);
Assert.Equal(0, ttm.Strength);
}
[Fact]
public void Reset_CanReuseAfterReset()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0));
ttm.Reset();
var result = ttm.Update(new TValue(time.AddMinutes(2).Ticks, 200.0));
Assert.Equal(200.0, result.Value);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Bar Correction Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendBarCorrectionTests
{
[Fact]
public void Update_IsNewFalse_CorrectsPreviousValue()
{
var ttm = new TtmTrend(period: 6);
var time = DateTime.UtcNow;
ttm.Update(new TValue(time.Ticks, 100.0));
ttm.Update(new TValue(time.AddMinutes(1).Ticks, 110.0), isNew: true);
// Correct the bar with different value
var corrected = ttm.Update(new TValue(time.AddMinutes(1).Ticks, 105.0), isNew: false);
// Should use 105 instead of 110
double alpha = 2.0 / 7.0;
double expected = alpha * 105.0 + (1 - alpha) * 100.0;
Assert.Equal(expected, corrected.Value, 10);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Batch Processing Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendBatchTests
{
[Fact]
public void Batch_ReturnsCorrectResults()
{
var bars = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000));
}
var result = TtmTrend.Batch(bars, period: 6);
Assert.Equal(10, result.Count);
}
[Fact]
public void Calculate_ReturnsIndicatorAndResults()
{
var bars = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000));
}
var (results, indicator) = TtmTrend.Calculate(bars, period: 6);
Assert.Equal(10, results.Count);
Assert.True(indicator.IsHot);
Assert.Equal(6, indicator.Period);
}
[Fact]
public void Update_EmptyBarSeries_ReturnsEmpty()
{
var ttm = new TtmTrend(period: 6);
var bars = new TBarSeries();
var result = ttm.Update(bars);
Assert.True(result.Count == 0);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Event Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendEventTests
{
[Fact]
public void Update_RaisesPubEvent()
{
var ttm = new TtmTrend(period: 6);
var eventRaised = false;
TValue receivedValue = default;
ttm.Pub += (object? sender, in TValueEventArgs args) =>
{
eventRaised = true;
receivedValue = args.Value;
};
var result = ttm.Update(new TValue(DateTime.UtcNow.Ticks, 100.0));
Assert.True(eventRaised);
Assert.Equal(result.Value, receivedValue.Value);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Prime Tests
// ═══════════════════════════════════════════════════════════════════════════
public class TtmTrendPrimeTests
{
[Fact]
public void Prime_WarmUpIndicator()
{
var ttm = new TtmTrend(period: 6);
var bars = new TBarSeries();
var time = DateTime.UtcNow;
for (int i = 0; i < 10; i++)
{
bars.Add(new TBar(time.AddMinutes(i).Ticks, 100.0 + i, 105.0 + i, 95.0 + i, 102.0 + i, 1000));
}
ttm.Prime(bars);
Assert.True(ttm.IsHot);
Assert.NotEqual(default, ttm.Last);
}
}
+270
View File
@@ -0,0 +1,270 @@
// TTM_TREND: John Carter's TTM Trend Indicator
// Color-coded EMA for visual trend identification
// Uses 6-period EMA of HLC/3 (typical price) by default
using System.Runtime.CompilerServices;
namespace QuanTAlib;
/// <summary>
/// TTM_TREND: John Carter's TTM Trend Indicator
/// A fast EMA-based trend indicator with color-coded direction and strength measurement.
///
/// Calculation: EMA(source, period) with trend = sign(EMA - prevEMA)
/// </summary>
/// <remarks>
/// <b>Calculation:</b>
/// <code>
/// alpha = 2 / (period + 1)
/// EMA = alpha * source + (1 - alpha) * prevEMA
/// trend = sign(EMA - prevEMA)
/// strength = |EMA - prevEMA| / prevEMA * 100
/// </code>
///
/// <b>Key characteristics:</b>
/// - O(1) update complexity per bar
/// - Uses EMA for smooth, responsive trend following
/// - Trend direction: +1 (bullish), -1 (bearish), 0 (neutral)
/// - Strength measures percent change between EMA values
/// - Default period of 6 for fast trend detection
/// </remarks>
/// <seealso href="TtmTrend.md">Detailed documentation</seealso>
[SkipLocalsInit]
public sealed class TtmTrend : ITValuePublisher
{
private const int DefaultPeriod = 6;
private readonly int _period;
private readonly double _alpha;
// Current state
private double _ema;
private double _prevEma;
private int _sampleCount;
// Saved state for bar correction
private double _p_ema;
private double _p_prevEma;
private int _p_sampleCount;
/// <summary>
/// Display name for the indicator.
/// </summary>
public string Name { get; }
public event TValuePublishedHandler? Pub;
/// <summary>
/// Current TTM Trend EMA value.
/// </summary>
public TValue Last { get; private set; }
/// <summary>
/// Current trend direction: +1 (bullish), -1 (bearish), 0 (neutral).
/// </summary>
public int Trend { get; private set; }
/// <summary>
/// Current trend strength as percent change between EMA values.
/// </summary>
public double Strength { get; private set; }
/// <summary>
/// True when the indicator has calculated a valid value (after 2 bars).
/// </summary>
public bool IsHot => _sampleCount > 1;
/// <summary>
/// The lookback period parameter.
/// </summary>
public int Period => _period;
/// <summary>
/// The number of bars required for the indicator to warm up.
/// </summary>
public static int WarmupPeriod => 2;
/// <summary>
/// Creates a TTM Trend indicator with specified period.
/// </summary>
/// <param name="period">Lookback period for EMA (must be >= 1, default 6)</param>
public TtmTrend(int period = DefaultPeriod)
{
if (period < 1)
{
throw new ArgumentException("Period must be at least 1", nameof(period));
}
_period = period;
_alpha = 2.0 / (period + 1);
Name = $"TTM_TREND({period})";
}
/// <summary>
/// Resets the indicator state.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Reset()
{
_ema = 0;
_prevEma = 0;
_sampleCount = 0;
_p_ema = 0;
_p_prevEma = 0;
_p_sampleCount = 0;
Trend = 0;
Strength = 0;
Last = default;
}
/// <summary>
/// Updates the TTM Trend indicator with a new value.
/// </summary>
/// <param name="input">Input value (typically HLC/3)</param>
/// <param name="isNew">True for new bar, false for update of current bar</param>
/// <returns>The current TTM Trend EMA value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
double value = input.Value;
// Handle NaN/Infinity inputs
if (!double.IsFinite(value))
{
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
// State management for bar correction
if (isNew)
{
_p_ema = _ema;
_p_prevEma = _prevEma;
_p_sampleCount = _sampleCount;
}
else
{
_ema = _p_ema;
_prevEma = _p_prevEma;
_sampleCount = _p_sampleCount;
}
// EMA calculation
if (_sampleCount == 0)
{
_ema = value;
_prevEma = value;
}
else
{
_prevEma = _ema;
_ema = Math.FusedMultiplyAdd(_alpha, value - _ema, _ema);
}
if (isNew)
{
_sampleCount++;
}
// Calculate trend and strength
double diff = _ema - _prevEma;
Trend = Math.Sign(diff);
Strength = _prevEma > 1e-10 ? Math.Abs(diff) / _prevEma * 100.0 : 0.0;
Last = new TValue(input.Time, _ema);
Pub?.Invoke(this, new TValueEventArgs { Value = Last, IsNew = isNew });
return Last;
}
/// <summary>
/// Updates the TTM Trend indicator with a bar using typical price (HLC/3).
/// </summary>
/// <param name="bar">The price bar</param>
/// <param name="isNew">True for new bar, false for update of current bar</param>
/// <returns>The current TTM Trend EMA value</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TBar bar, bool isNew = true)
{
double typical = (bar.High + bar.Low + bar.Close) / 3.0;
return Update(new TValue(bar.Time, typical), isNew);
}
/// <summary>
/// Updates with a value series.
/// </summary>
public TSeries Update(TSeries source)
{
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var tList = new List<long>(len);
var vList = new List<double>(len);
for (int i = 0; i < len; i++)
{
var result = Update(source[i], isNew: true);
tList.Add(source.Times[i]);
vList.Add(result.Value);
}
return new TSeries(tList, vList);
}
/// <summary>
/// Updates with a bar series.
/// </summary>
public TSeries Update(TBarSeries source)
{
if (source.Count == 0)
{
return new TSeries([], []);
}
int len = source.Count;
var tList = new List<long>(len);
var vList = new List<double>(len);
var times = source.Open.Times;
for (int i = 0; i < len; i++)
{
var result = Update(source[i], isNew: true);
tList.Add(times[i]);
vList.Add(result.Value);
}
return new TSeries(tList, vList);
}
/// <summary>
/// Primes the indicator with historical bar data.
/// </summary>
public void Prime(TBarSeries source)
{
for (int i = 0; i < source.Count; i++)
{
Update(source[i], isNew: true);
}
}
/// <summary>
/// Creates and returns results for a bar series.
/// </summary>
public static TSeries Batch(TBarSeries source, int period = DefaultPeriod)
{
var indicator = new TtmTrend(period);
return indicator.Update(source);
}
/// <summary>
/// Returns the indicator and its results.
/// </summary>
public static (TSeries Results, TtmTrend Indicator) Calculate(TBarSeries source, int period = DefaultPeriod)
{
var indicator = new TtmTrend(period);
var results = indicator.Update(source);
return (results, indicator);
}
}
+113
View File
@@ -0,0 +1,113 @@
# TTM_TREND: TTM Trend Indicator
> John Carter's TTM Trend - A fast EMA-based trend indicator with color-coded direction.
## Historical Context
John Carter developed the TTM (Trade the Markets) Trend indicator as a clean visual tool for identifying short-term trend direction. Popularized through his book *Mastering the Trade* and the thinkorswim platform, it provides a simple but effective way to see trend changes at a glance using color-coded lines.
## Algorithm
### Core Calculation
```
alpha = 2 / (period + 1)
EMA = alpha × source + (1 - alpha) × prevEMA
```
Or equivalently:
```
EMA = alpha × (source - EMA) + EMA
```
### Trend Detection
```
trend = sign(EMA - prevEMA)
+1 = bullish (EMA rising)
-1 = bearish (EMA falling)
0 = neutral (EMA unchanged)
```
### Strength Measurement
```
strength = |EMA - prevEMA| / prevEMA × 100%
```
## Default Parameters
| Parameter | Value | Description |
|:----------|:------|:------------|
| Period | 6 | EMA lookback period (very fast) |
| Source | HLC/3 | Typical price (High + Low + Close) / 3 |
## Outputs
| Output | Type | Description |
|:-------|:-----|:------------|
| Value | double | Current EMA value |
| Trend | int | Trend direction: +1, -1, or 0 |
| Strength | double | Percent change between EMA values |
| IsHot | bool | True after warming up (2 bars) |
## Color Coding
| Color | Condition | Meaning |
|:------|:----------|:--------|
| 🟢 Green | Trend > 0 | EMA rising (bullish) |
| 🔴 Red | Trend < 0 | EMA falling (bearish) |
| ⚫ Gray | Trend = 0 | EMA unchanged (neutral) |
## Performance
| Metric | Value |
|:-------|:------|
| Time complexity | O(1) per bar |
| Space complexity | O(1) |
| Warmup period | 2 bars |
| Allocations | Zero in hot path |
## Usage Examples
### Basic Usage
```csharp
var ttm = new TtmTrend(period: 6);
// Update with typical price
var result = ttm.Update(new TValue(time, typicalPrice));
// Or update with bar (uses HLC/3 automatically)
var result = ttm.Update(bar);
// Access trend direction
if (ttm.Trend > 0) { /* bullish */ }
else if (ttm.Trend < 0) { /* bearish */ }
```
### Batch Processing
```csharp
var results = TtmTrend.Batch(barSeries, period: 6);
```
### With Indicator Instance
```csharp
var (results, indicator) = TtmTrend.Calculate(barSeries, period: 6);
bool isBullish = indicator.Trend > 0;
double strength = indicator.Strength;
```
## Trading Applications
1. **Trend Following**: Trade in the direction of the EMA color
2. **Trend Confirmation**: Use with other TTM indicators (Squeeze, Wave)
3. **Entry Timing**: Enter on color change with confirmation
4. **Exit Signal**: Exit when color changes against position
## Category
**Dynamics** - Measures trend direction and momentum using fast EMA smoothing.
## See Also
- [TTM_SQUEEZE: TTM Squeeze](../ttm_squeeze/TtmSqueeze.md)
- [TTM_WAVE: TTM Wave](../../oscillators/ttm_wave/TtmWave.md)
- [TTM_LRC: TTM Linear Regression Channel](../../channels/ttm_lrc/TtmLrc.md)
- [SUPER: SuperTrend](../super/Super.md)
+59
View File
@@ -0,0 +1,59 @@
// The MIT License (MIT)
// © mihakralj
//@version=6
indicator("TTM Trend", "TTM_TREND", overlay=true)
//@function Calculates TTM Trend using 6-period moving average with color-coded trend
//@doc https://github.com/mihakralj/pinescript/blob/main/indicators/dynamics/ttm_trend.md
//@param source Series to calculate TTM Trend from
//@param period Lookback period for moving average
//@returns Tuple [ttm_line, trend, strength] where trend is -1/0/1 and strength is percentage change
ttm_trend(series float source, simple int period = 6) =>
if period <= 0
runtime.error("Period must be greater than 0")
float alpha = 2.0 / (period + 1)
var float ema = source
var float ema_prev = source
ema := alpha * (source - ema) + ema
float trend = math.sign(ema - ema_prev)
float strength = math.abs(ema - ema_prev) / math.max(ema_prev, 1e-10) * 100
ema_prev := ema
[ema, trend, strength]
// ---------- Main loop ----------
// Inputs
i_period = input.int(6, "Period", minval=1)
i_source = input.source(hlc3, "Source")
i_show_strength = input.bool(true, "Show Trend Strength %")
// Calculation
[ttm_line, trend, strength] = ttm_trend(i_source, i_period)
// Colors
color up_color = color.new(color.green, 0)
color down_color = color.new(color.red, 0)
color neutral_color = color.new(color.gray, 50)
color line_color = trend > 0 ? up_color : trend < 0 ? down_color : neutral_color
// Plot
plot(ttm_line, "TTM Trend", color=line_color, linewidth=3, style=plot.style_line)
// Strength band (optional)
float strength_multiplier = 0.01
float upper_band = i_show_strength ? ttm_line + (ttm_line * strength * strength_multiplier) : na
float lower_band = i_show_strength ? ttm_line - (ttm_line * strength * strength_multiplier) : na
p1 = plot(upper_band, "Upper Strength", color=color.new(color.blue, 80), linewidth=1)
p2 = plot(lower_band, "Lower Strength", color=color.new(color.blue, 80), linewidth=1)
fill(p1, p2, color=color.new(color.blue, 90), title="Strength Band")
// Optional: Plot trend change signals
bool trend_change = trend != nz(trend[1], 0) and bar_index > 0
plotshape(trend_change and trend > 0, "Up", shape.triangleup, location.belowbar, color=up_color, size=size.tiny)
plotshape(trend_change and trend < 0, "Down", shape.triangledown, location.abovebar, color=down_color, size=size.tiny)