Skip to content
Closed
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
23 changes: 23 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2726,6 +2726,29 @@ public Vortex VTX(Symbol symbol, int period, Resolution? resolution = null, Func
return indicator;
}

/// <summary>
/// Creates a new WaveTrend Oscillator indicator. This combines two nested exponential moving
/// averages over the typical price with a signal line, smoothing a CCI-like transformation
/// into a bounded oscillator suitable for spotting overbought/oversold conditions and crossovers.
/// 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 channel length (EMA of typical price)</param>
/// <param name="averagePeriod">The average length (EMA applied to the CCI-like series, produces WT1)</param>
/// <param name="signalPeriod">The period of the SMA applied to WT1 to produce the signal line (WT2)</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 over the specified parameters</returns>
[DocumentationAttribute(Indicators)]
public WaveTrendOscillator WTO(Symbol symbol, int channelPeriod = 10, int averagePeriod = 21, int signalPeriod = 4,
Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
{
var name = CreateIndicatorName(symbol, $"WTO({channelPeriod},{averagePeriod},{signalPeriod})", resolution);
var indicator = new WaveTrendOscillator(name, channelPeriod, averagePeriod, signalPeriod);
InitializeIndicator(indicator, resolution, selector, symbol);
return indicator;
}

/// <summary>
/// Creates a new Williams %R indicator. This will compute the percentage change of
/// the current closing price in relation to the high and low of the past N periods.
Expand Down
128 changes: 128 additions & 0 deletions Indicators/WaveTrendOscillator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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>
/// WaveTrend Oscillator (WTO) as popularized by LazyBear on TradingView.
/// It produces a main oscillator line (WT1) and a smoothed signal line (WT2).
/// The formula applies two nested exponential moving averages to a Commodity
/// Channel Index-like transformation of the typical price, then smooths the
/// result with a simple moving average to derive the signal line:
/// hlc3 = (High + Low + Close) / 3
/// esa = EMA(hlc3, channelPeriod)
/// d = EMA(|esa - hlc3|, channelPeriod)
/// ci = (hlc3 - esa) / (0.015 * d)
/// WT1 = EMA(ci, averagePeriod)
/// WT2 = SMA(WT1, signalPeriod)
/// </summary>
public class WaveTrendOscillator : BarIndicator, IIndicatorWarmUpPeriodProvider
{
private readonly ExponentialMovingAverage _esa;
private readonly ExponentialMovingAverage _d;
private readonly ExponentialMovingAverage _tci;

/// <summary>
/// Gets the signal line (WT2): the simple moving average of WT1.
/// </summary>
public IndicatorBase<IndicatorDataPoint> Signal { get; }

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

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

/// <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 channel length used by the EMA that smooths the typical price</param>
/// <param name="averagePeriod">The average length used by the EMA applied to the CCI-like series (produces WT1)</param>
/// <param name="signalPeriod">The period of the simple moving average applied to WT1 to produce WT2</param>
public WaveTrendOscillator(string name, int channelPeriod, int averagePeriod, int signalPeriod)
: base(name)
{
_esa = new ExponentialMovingAverage(name + "_ESA", channelPeriod);
_d = new ExponentialMovingAverage(name + "_D", channelPeriod);
_tci = new ExponentialMovingAverage(name + "_TCI", averagePeriod);
Signal = new SimpleMovingAverage(name + "_Signal", signalPeriod);
WarmUpPeriod = 2 * channelPeriod + averagePeriod + signalPeriod - 3;
}

/// <summary>
/// Initializes a new instance of the <see cref="WaveTrendOscillator"/> class with the default name format.
/// </summary>
/// <param name="channelPeriod">The channel length used by the EMA that smooths the typical price</param>
/// <param name="averagePeriod">The average length used by the EMA applied to the CCI-like series (produces WT1)</param>
/// <param name="signalPeriod">The period of the simple moving average applied to WT1 to produce WT2</param>
public WaveTrendOscillator(int channelPeriod, int averagePeriod, int signalPeriod)
: this($"WTO({channelPeriod},{averagePeriod},{signalPeriod})", channelPeriod, averagePeriod, signalPeriod)
{
}

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

_esa.Update(input.EndTime, hlc3);
if (!_esa.IsReady)
{
return 0m;
}

_d.Update(input.EndTime, Math.Abs(_esa.Current.Value - hlc3));
var divisor = 0.015m * _d.Current.Value;
if (!_d.IsReady || divisor == 0m)
{
return 0m;
}

var ci = (hlc3 - _esa.Current.Value) / divisor;
_tci.Update(input.EndTime, ci);
if (!_tci.IsReady)
{
return 0m;
}

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

/// <summary>
/// Resets this indicator to its initial state.
/// </summary>
public override void Reset()
{
_esa.Reset();
_d.Reset();
_tci.Reset();
Signal.Reset();
base.Reset();
}
}
}
109 changes: 109 additions & 0 deletions Tests/Indicators/WaveTrendOscillatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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(10, 21, 4);
}

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

protected override string TestColumnName => "TCI";

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

[Test]
public void ConstructorWithDefaultName()
{
var indicator = new WaveTrendOscillator(10, 21, 4);
Assert.AreEqual("WTO(10,21,4)", indicator.Name);
}

[Test]
public void ConstructorWithCustomName()
{
var indicator = new WaveTrendOscillator("CustomWTO", 10, 21, 4);
Assert.AreEqual("CustomWTO", indicator.Name);
}

[Test]
public void WarmUpPeriodIsCorrect()
{
var indicator = new WaveTrendOscillator(10, 21, 4);
Assert.AreEqual(42, indicator.WarmUpPeriod);
}

[Test]
public void IsReadyAfterWarmUp()
{
var indicator = new WaveTrendOscillator(10, 21, 4);
Assert.IsFalse(indicator.IsReady);

var start = new DateTime(2024, 1, 1);
for (var i = 0; i < indicator.WarmUpPeriod; i++)
{
var price = 100m + i;
indicator.Update(new TradeBar
{
Time = start.AddDays(i),
Symbol = Symbol.Empty,
Open = price,
High = price + 1m,
Low = price - 1m,
Close = price,
Volume = 1000
});
}

Assert.IsTrue(indicator.IsReady);
}

[Test]
public void SignalIsReadyAfterWarmUp()
{
var indicator = new WaveTrendOscillator(10, 21, 4);
var start = new DateTime(2024, 1, 1);
for (var i = 0; i < indicator.WarmUpPeriod; i++)
{
var price = 100m + (decimal)Math.Sin(i / 3.0);
indicator.Update(new TradeBar
{
Time = start.AddDays(i),
Symbol = Symbol.Empty,
Open = price,
High = price + 0.5m,
Low = price - 0.5m,
Close = price,
Volume = 1000
});
}

Assert.IsTrue(indicator.Signal.IsReady);
Assert.AreNotEqual(0m, indicator.Signal.Current.Value);
}
}
}
3 changes: 3 additions & 0 deletions Tests/QuantConnect.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,9 @@
<Content Include="TestData\spy_pso.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\spy_wto.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="TestData\spy_with_vwap.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
Expand Down
Loading
Loading