mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-23 04:58:08 +00:00
normalization of methods
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user