Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2808,6 +2808,28 @@ public WilderAccumulativeSwingIndex ASI(Symbol symbol, decimal limitMove, Resolu
return asi;
}

/// <summary>
/// Creates a new WaveTrend Oscillator (WTO) indicator for the symbol.
/// The indicator will be automatically updated on the given resolution.
/// </summary>
/// <param name="symbol">The symbol whose WaveTrend Oscillator we want</param>
/// <param name="channelPeriod">The smoothing period for the typical-price EMA and the deviation EMA</param>
/// <param name="averagePeriod">The EMA period applied to the channel index to produce the WT1 line</param>
/// <param name="signalPeriod">The SMA period applied to WT1 to produce the WT2 signal line</param>
/// <param name="resolution">The resolution</param>
/// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to casting the input value to a TradeBar</param>
/// <returns>The WaveTrendOscillator indicator for the requested symbol</returns>
[DocumentationAttribute(Indicators)]
public WaveTrendOscillator WTO(Symbol symbol, int channelPeriod, int averagePeriod, int signalPeriod,
Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
{
var name = CreateIndicatorName(symbol, $"WTO({channelPeriod},{averagePeriod},{signalPeriod})", resolution);
var waveTrendOscillator = new WaveTrendOscillator(name, channelPeriod, averagePeriod, signalPeriod);
InitializeIndicator(waveTrendOscillator, resolution, selector, symbol);

return waveTrendOscillator;
}

/// <summary>
/// Creates a new Arms Index indicator
/// </summary>
Expand Down
159 changes: 159 additions & 0 deletions Indicators/WaveTrendOscillator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using QuantConnect.Data.Market;

namespace QuantConnect.Indicators
{
/// <summary>
/// The WaveTrend Oscillator (WTO) is a momentum indicator that highlights overbought
/// and oversold conditions by measuring how far the typical price has deviated from a
/// smoothed moving average, normalized by an exponentially smoothed mean absolute
/// deviation. The oscillator's main line (WT1) is an EMA of this normalized channel
/// index, and the signal line (WT2) is an SMA of WT1; crossovers between the two
/// lines are commonly used as entry and exit signals.
///
/// Formula:
/// HLC3 = (High + Low + Close) / 3
/// ESA = EMA(HLC3, channelPeriod)
/// D = EMA(|HLC3 - ESA|, channelPeriod)
/// CI = (HLC3 - ESA) / (0.015 * D)
/// WT1 = EMA(CI, averagePeriod) (the indicator's Current.Value)
/// WT2 = SMA(WT1, signalPeriod) (exposed via <see cref="Signal"/>)
/// </summary>
public class WaveTrendOscillator : BarIndicator, IIndicatorWarmUpPeriodProvider
{
/// <summary>
/// Scaling constant that keeps the channel index roughly within +/-100 most of the time,
/// matching the original Lambert/CCI normalization convention.
/// </summary>
private const decimal NormalizationConstant = 0.015m;

/// <summary>
/// Gets the EMA of the typical price (ESA in the original WaveTrend formulation).
/// </summary>
public IndicatorBase<IndicatorDataPoint> ChannelAverage { get; }

/// <summary>
/// Gets the EMA of the absolute deviation between the typical price and <see cref="ChannelAverage"/>.
/// </summary>
public IndicatorBase<IndicatorDataPoint> ChannelDeviation { get; }

/// <summary>
/// Gets the smoothed channel index (WT1): an EMA of the normalized channel index.
/// </summary>
public IndicatorBase<IndicatorDataPoint> ChannelIndexAverage { get; }

/// <summary>
/// Gets the signal line (WT2): a simple moving average of <see cref="ChannelIndexAverage"/>.
/// </summary>
public IndicatorBase<IndicatorDataPoint> Signal { get; }

/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized.
/// </summary>
public override bool IsReady => Signal.IsReady;

/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod { get; }

/// <summary>
/// Initializes a new instance of the <see cref="WaveTrendOscillator"/> class.
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="channelPeriod">The smoothing period for the typical-price EMA and the deviation EMA (n1)</param>
/// <param name="averagePeriod">The EMA period applied to the channel index to produce WT1 (n2)</param>
/// <param name="signalPeriod">The SMA period applied to WT1 to produce the WT2 signal line (n3)</param>
public WaveTrendOscillator(string name, int channelPeriod, int averagePeriod, int signalPeriod)
: base(name)
{
if (channelPeriod < 1 || averagePeriod < 1 || signalPeriod < 1)
{
throw new ArgumentException("WaveTrendOscillator: all periods must be greater than zero.");
}

ChannelAverage = new ExponentialMovingAverage(name + "_ChannelAverage", channelPeriod);
ChannelDeviation = new ExponentialMovingAverage(name + "_ChannelDeviation", channelPeriod);
ChannelIndexAverage = new ExponentialMovingAverage(name + "_ChannelIndexAverage", averagePeriod);
Signal = new SimpleMovingAverage(name + "_Signal", signalPeriod);
// The chain ESA -> D -> WT1 -> WT2 only advances each sub-indicator once the
// upstream one is ready, so the total warm-up is the sum of the chained periods
// minus three for the overlap on each transition.
WarmUpPeriod = 2 * channelPeriod + averagePeriod + signalPeriod - 3;
}

/// <summary>
/// Initializes a new instance of the <see cref="WaveTrendOscillator"/> class with the default name.
/// </summary>
/// <param name="channelPeriod">The smoothing period for the typical-price EMA and the deviation EMA (n1)</param>
/// <param name="averagePeriod">The EMA period applied to the channel index to produce WT1 (n2)</param>
/// <param name="signalPeriod">The SMA period applied to WT1 to produce the WT2 signal line (n3)</param>
public WaveTrendOscillator(int channelPeriod, int averagePeriod, int signalPeriod)
: this($"WTO({channelPeriod},{averagePeriod},{signalPeriod})", channelPeriod, averagePeriod, signalPeriod)
{
}

/// <summary>
/// Computes the next value of this indicator from the given bar.
/// </summary>
/// <param name="input">The input bar</param>
/// <returns>The next WT1 value (EMA of the channel index)</returns>
protected override decimal ComputeNextValue(IBaseDataBar input)
{
var typicalPrice = (input.High + input.Low + input.Close) / 3m;

if (!ChannelAverage.Update(input.EndTime, typicalPrice))
{
return 0m;
}

var deviation = Math.Abs(typicalPrice - ChannelAverage);
if (!ChannelDeviation.Update(input.EndTime, deviation))
{
return 0m;
}

var weightedDeviation = NormalizationConstant * ChannelDeviation;
if (weightedDeviation == 0m)
{
return Current.Value;
}

var channelIndex = (typicalPrice - ChannelAverage) / weightedDeviation;
if (!ChannelIndexAverage.Update(input.EndTime, channelIndex))
{
return ChannelIndexAverage.Current.Value;
}

Signal.Update(input.EndTime, ChannelIndexAverage.Current.Value);
return ChannelIndexAverage.Current.Value;
}

/// <summary>
/// Resets this indicator to its initial state.
/// </summary>
public override void Reset()
{
ChannelAverage.Reset();
ChannelDeviation.Reset();
ChannelIndexAverage.Reset();
Signal.Reset();
base.Reset();
}
}
}
136 changes: 136 additions & 0 deletions Tests/Indicators/WaveTrendOscillatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using NUnit.Framework;
using QuantConnect.Data.Market;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Indicators
{
[TestFixture]
public class WaveTrendOscillatorTests : CommonIndicatorTests<IBaseDataBar>
{
protected override IndicatorBase<IBaseDataBar> CreateIndicator()
{
return new WaveTrendOscillator(channelPeriod: 10, averagePeriod: 21, signalPeriod: 4);
}

protected override string TestFileName => "spy_wto.csv";

protected override string TestColumnName => "WT1";

protected override Action<IndicatorBase<IBaseDataBar>, double> Assertion =>
(indicator, expected) =>
Assert.AreEqual(expected, (double)indicator.Current.Value, 1e-4);

[Test]
public void ComparesAgainstExternalDataSignalLine()
{
var wto = (WaveTrendOscillator)CreateIndicator();
TestHelper.TestIndicator(
wto,
TestFileName,
"WT2",
(ind, expected) => Assert.AreEqual(
expected,
(double)((WaveTrendOscillator)ind).Signal.Current.Value,
delta: 1e-4
)
);
}

[Test]
public void ConstructorThrowsOnNonPositivePeriods()
{
Assert.Throws<ArgumentException>(() => new WaveTrendOscillator(0, 21, 4));
Assert.Throws<ArgumentException>(() => new WaveTrendOscillator(10, 0, 4));
Assert.Throws<ArgumentException>(() => new WaveTrendOscillator(10, 21, 0));
}

[Test]
public void IsReadyOnlyAfterAllSubIndicatorsAreReady()
{
const int channelPeriod = 3;
const int averagePeriod = 4;
const int signalPeriod = 2;
var wto = new WaveTrendOscillator(channelPeriod, averagePeriod, signalPeriod);

Assert.IsFalse(wto.ChannelAverage.IsReady);
Assert.IsFalse(wto.ChannelDeviation.IsReady);
Assert.IsFalse(wto.ChannelIndexAverage.IsReady);
Assert.IsFalse(wto.Signal.IsReady);
Assert.IsFalse(wto.IsReady);

// ChannelAverage (ESA) becomes ready first.
for (var i = 0; i < channelPeriod; i++)
{
Assert.IsFalse(wto.ChannelAverage.IsReady);
wto.Update(MakeBar(i));
}
Assert.IsTrue(wto.ChannelAverage.IsReady);
Assert.IsFalse(wto.ChannelDeviation.IsReady);

// ChannelDeviation (D) takes another channelPeriod-1 bars to fill its window
// because we only feed it once ChannelAverage is ready.
for (var i = 0; i < channelPeriod - 1; i++)
{
Assert.IsFalse(wto.ChannelDeviation.IsReady);
wto.Update(MakeBar(channelPeriod + i));
}
Assert.IsTrue(wto.ChannelDeviation.IsReady);
Assert.IsFalse(wto.ChannelIndexAverage.IsReady);

// ChannelIndexAverage (WT1) takes another averagePeriod-1 bars.
for (var i = 0; i < averagePeriod - 1; i++)
{
Assert.IsFalse(wto.ChannelIndexAverage.IsReady);
wto.Update(MakeBar(2 * channelPeriod - 1 + i));
}
Assert.IsTrue(wto.ChannelIndexAverage.IsReady);
Assert.IsFalse(wto.Signal.IsReady);

// Signal (WT2) takes another signalPeriod-1 bars.
for (var i = 0; i < signalPeriod - 1; i++)
{
Assert.IsFalse(wto.Signal.IsReady);
Assert.IsFalse(wto.IsReady);
wto.Update(MakeBar(2 * channelPeriod + averagePeriod - 2 + i));
}
Assert.IsTrue(wto.Signal.IsReady);
Assert.IsTrue(wto.IsReady);
Assert.AreEqual(wto.WarmUpPeriod, wto.Samples);
}

[Test]
public override void ResetsProperly()
{
var wto = (WaveTrendOscillator)CreateIndicator();
TestHelper.TestIndicatorReset(wto, TestFileName);

TestHelper.AssertIndicatorIsInDefaultState(wto.ChannelAverage);
TestHelper.AssertIndicatorIsInDefaultState(wto.ChannelDeviation);
TestHelper.AssertIndicatorIsInDefaultState(wto.ChannelIndexAverage);
TestHelper.AssertIndicatorIsInDefaultState(wto.Signal);
}

private static TradeBar MakeBar(int days)
{
var time = new DateTime(2024, 1, 1).AddDays(days);
var close = 100m + days;
return new TradeBar(time, Symbols.SPY, close, close + 5m, close - 5m, close, 100m, Time.OneDay);
}
}
}
3 changes: 3 additions & 0 deletions Tests/QuantConnect.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,9 @@
<Content Include="TestData\spy_with_williamsR14.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\spy_wto.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\test_cash_equity.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
Loading
Loading