Skip to content

Commit 0dc7e1e

Browse files
AlexCatarinoclaude
andcommitted
Add WaveTrend Oscillator indicator
Implements `WaveTrendOscillator` (BarIndicator) per issue #6411. Adds the indicator class, the `WTO` helper in `QCAlgorithm.Indicators.cs`, unit tests inheriting `CommonIndicatorTests<IBaseDataBar>`, and the TA-Lib-generated reference CSV under `Tests/TestData/spy_wto.txt`. The indicator's `Current.Value` exposes the main wave trend line (WT1 = EMA of the channel index). A public `Signal` property exposes the signal line (WT2 = SMA of WT1). Formula follows the LazyBear TradingView script: ESA = EMA(HLC3, n1); D = EMA(|HLC3 - ESA|, n1) CI = (HLC3 - ESA) / (0.015 * D) WT1 = EMA(CI, n2); WT2 = SMA(WT1, m) Default parameters: n1=10, n2=21, m=4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8205d00 commit 0dc7e1e

5 files changed

Lines changed: 6141 additions & 0 deletions

File tree

Algorithm/QCAlgorithm.Indicators.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2766,6 +2766,27 @@ public WilderMovingAverage WWMA(Symbol symbol, int period, Resolution? resolutio
27662766
return wilderMovingAverage;
27672767
}
27682768

2769+
/// <summary>
2770+
/// Creates a new WaveTrendOscillator indicator for the symbol.
2771+
/// The indicator will be automatically updated on the given resolution.
2772+
/// </summary>
2773+
/// <param name="symbol">The symbol whose WaveTrend Oscillator we want</param>
2774+
/// <param name="channelPeriod">The period used to smooth the typical price and its mean deviation (n1)</param>
2775+
/// <param name="averagePeriod">The period used to smooth the channel index into the main wave trend line (n2)</param>
2776+
/// <param name="signalPeriod">The period used to smooth the wave trend line into the signal line</param>
2777+
/// <param name="resolution">The resolution</param>
2778+
/// <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>
2779+
/// <returns>The WaveTrendOscillator indicator for the requested symbol with the given parameters</returns>
2780+
[DocumentationAttribute(Indicators)]
2781+
public WaveTrendOscillator WTO(Symbol symbol, int channelPeriod = 10, int averagePeriod = 21, int signalPeriod = 4, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
2782+
{
2783+
var name = CreateIndicatorName(symbol, $"WTO({channelPeriod},{averagePeriod},{signalPeriod})", resolution);
2784+
var waveTrendOscillator = new WaveTrendOscillator(name, channelPeriod, averagePeriod, signalPeriod);
2785+
InitializeIndicator(waveTrendOscillator, resolution, selector, symbol);
2786+
2787+
return waveTrendOscillator;
2788+
}
2789+
27692790
/// <summary>
27702791
/// Creates a Wilder Swing Index (SI) indicator for the symbol.
27712792
/// The indicator will be automatically updated on the given resolution.

Indicators/WaveTrendOscillator.cs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using QuantConnect.Data.Market;
18+
19+
namespace QuantConnect.Indicators
20+
{
21+
/// <summary>
22+
/// Represents the WaveTrend Oscillator (WTO) developed by LazyBear.
23+
///
24+
/// The oscillator uses the typical price (HLC3) and is computed as follows:
25+
/// ESA = EMA(HLC3, channelPeriod)
26+
/// D = EMA(|HLC3 - ESA|, channelPeriod)
27+
/// CI = (HLC3 - ESA) / (0.015 * D)
28+
/// WT1 = EMA(CI, averagePeriod)
29+
/// WT2 = SMA(WT1, signalPeriod)
30+
///
31+
/// The indicator's current value is WT1 (the main wave trend line).
32+
/// The <see cref="Signal"/> property exposes WT2, which is typically plotted as the signal line.
33+
/// </summary>
34+
public class WaveTrendOscillator : BarIndicator, IIndicatorWarmUpPeriodProvider
35+
{
36+
/// <summary>
37+
/// Constant used to scale the channel index so most WTO values fall within +/- 100.
38+
/// </summary>
39+
private const decimal K = 0.015m;
40+
41+
private readonly ExponentialMovingAverage _channelEma;
42+
private readonly ExponentialMovingAverage _meanDeviationEma;
43+
44+
/// <summary>
45+
/// Gets the main wave trend line (WT1), computed as an EMA of the channel index.
46+
/// </summary>
47+
public ExponentialMovingAverage WaveTrend { get; }
48+
49+
/// <summary>
50+
/// Gets the signal line (WT2), computed as an SMA of <see cref="WaveTrend"/>.
51+
/// </summary>
52+
public SimpleMovingAverage Signal { get; }
53+
54+
/// <summary>
55+
/// Initializes a new instance of the <see cref="WaveTrendOscillator"/> class.
56+
/// </summary>
57+
/// <param name="name">The name of this indicator</param>
58+
/// <param name="channelPeriod">The period used to smooth the typical price and its mean deviation (n1)</param>
59+
/// <param name="averagePeriod">The period used to smooth the channel index into the main wave trend line (n2)</param>
60+
/// <param name="signalPeriod">The period used to smooth the wave trend line into the signal line</param>
61+
public WaveTrendOscillator(string name, int channelPeriod, int averagePeriod, int signalPeriod)
62+
: base(name)
63+
{
64+
_channelEma = new ExponentialMovingAverage(name + "_ChannelEMA", channelPeriod);
65+
_meanDeviationEma = new ExponentialMovingAverage(name + "_MeanDeviationEMA", channelPeriod);
66+
WaveTrend = new ExponentialMovingAverage(name + "_WaveTrend", averagePeriod);
67+
Signal = new SimpleMovingAverage(name + "_Signal", signalPeriod);
68+
69+
WarmUpPeriod = 2 * channelPeriod + averagePeriod + signalPeriod - 3;
70+
}
71+
72+
/// <summary>
73+
/// Initializes a new instance of the <see cref="WaveTrendOscillator"/> class using default parameters (10, 21, 4).
74+
/// </summary>
75+
/// <param name="channelPeriod">The period used to smooth the typical price and its mean deviation (n1)</param>
76+
/// <param name="averagePeriod">The period used to smooth the channel index into the main wave trend line (n2)</param>
77+
/// <param name="signalPeriod">The period used to smooth the wave trend line into the signal line</param>
78+
public WaveTrendOscillator(int channelPeriod = 10, int averagePeriod = 21, int signalPeriod = 4)
79+
: this($"WTO({channelPeriod},{averagePeriod},{signalPeriod})", channelPeriod, averagePeriod, signalPeriod)
80+
{
81+
}
82+
83+
/// <summary>
84+
/// Gets a flag indicating when this indicator is ready and fully initialized.
85+
/// </summary>
86+
public override bool IsReady => Signal.IsReady;
87+
88+
/// <summary>
89+
/// Required period, in data points, for the indicator to be ready and fully initialized.
90+
/// </summary>
91+
public int WarmUpPeriod { get; }
92+
93+
/// <summary>
94+
/// Computes the next value of this indicator from the given state.
95+
/// </summary>
96+
/// <param name="input">The input given to the indicator</param>
97+
/// <returns>The current value of the main wave trend line (WT1)</returns>
98+
protected override decimal ComputeNextValue(IBaseDataBar input)
99+
{
100+
var typicalPrice = (input.High + input.Low + input.Close) / 3.0m;
101+
102+
_channelEma.Update(input.EndTime, typicalPrice);
103+
if (!_channelEma.IsReady)
104+
{
105+
return 0m;
106+
}
107+
108+
var absDeviation = Math.Abs(typicalPrice - _channelEma.Current.Value);
109+
_meanDeviationEma.Update(input.EndTime, absDeviation);
110+
if (!_meanDeviationEma.IsReady)
111+
{
112+
return 0m;
113+
}
114+
115+
var weightedMeanDeviation = K * _meanDeviationEma.Current.Value;
116+
if (weightedMeanDeviation == 0m)
117+
{
118+
return WaveTrend.Current.Value;
119+
}
120+
121+
var channelIndex = (typicalPrice - _channelEma.Current.Value) / weightedMeanDeviation;
122+
WaveTrend.Update(input.EndTime, channelIndex);
123+
if (!WaveTrend.IsReady)
124+
{
125+
return WaveTrend.Current.Value;
126+
}
127+
128+
Signal.Update(input.EndTime, WaveTrend.Current.Value);
129+
return WaveTrend.Current.Value;
130+
}
131+
132+
/// <summary>
133+
/// Resets this indicator to its initial state.
134+
/// </summary>
135+
public override void Reset()
136+
{
137+
_channelEma.Reset();
138+
_meanDeviationEma.Reset();
139+
WaveTrend.Reset();
140+
Signal.Reset();
141+
base.Reset();
142+
}
143+
}
144+
}
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using NUnit.Framework;
18+
using QuantConnect.Data.Market;
19+
using QuantConnect.Indicators;
20+
21+
namespace QuantConnect.Tests.Indicators
22+
{
23+
[TestFixture]
24+
public class WaveTrendOscillatorTests : CommonIndicatorTests<IBaseDataBar>
25+
{
26+
protected override IndicatorBase<IBaseDataBar> CreateIndicator()
27+
{
28+
RenkoBarSize = 1m;
29+
VolumeRenkoBarSize = 0.5m;
30+
return new WaveTrendOscillator(10, 21, 4);
31+
}
32+
33+
protected override string TestFileName => "spy_wto.txt";
34+
35+
protected override string TestColumnName => "WaveTrend";
36+
37+
protected override Action<IndicatorBase<IBaseDataBar>, double> Assertion =>
38+
(indicator, expected) =>
39+
Assert.AreEqual(expected, (double)indicator.Current.Value, 1e-4);
40+
41+
[Test]
42+
public void ComparesWithExternalDataSignal()
43+
{
44+
var wto = CreateIndicator();
45+
TestHelper.TestIndicator(
46+
wto,
47+
TestFileName,
48+
"Signal",
49+
(ind, expected) => Assert.AreEqual(
50+
expected,
51+
(double)((WaveTrendOscillator)ind).Signal.Current.Value,
52+
delta: 1e-4
53+
)
54+
);
55+
}
56+
57+
[Test]
58+
public override void ResetsProperly()
59+
{
60+
var wto = new WaveTrendOscillator(10, 21, 4);
61+
foreach (var bar in TestHelper.GetTradeBarStream(TestFileName, false))
62+
{
63+
wto.Update(bar);
64+
}
65+
Assert.IsTrue(wto.IsReady);
66+
Assert.IsTrue(wto.WaveTrend.IsReady);
67+
Assert.IsTrue(wto.Signal.IsReady);
68+
69+
wto.Reset();
70+
TestHelper.AssertIndicatorIsInDefaultState(wto);
71+
TestHelper.AssertIndicatorIsInDefaultState(wto.WaveTrend);
72+
TestHelper.AssertIndicatorIsInDefaultState(wto.Signal);
73+
}
74+
75+
[Test]
76+
public override void WarmsUpProperly()
77+
{
78+
const int channelPeriod = 3;
79+
const int averagePeriod = 4;
80+
const int signalPeriod = 2;
81+
var wto = new WaveTrendOscillator(channelPeriod, averagePeriod, signalPeriod);
82+
var expectedWarmUp = 2 * channelPeriod + averagePeriod + signalPeriod - 3;
83+
Assert.AreEqual(expectedWarmUp, wto.WarmUpPeriod);
84+
85+
var reference = DateTime.Today;
86+
for (var i = 0; i < expectedWarmUp - 1; i++)
87+
{
88+
Assert.IsFalse(wto.IsReady);
89+
wto.Update(new TradeBar
90+
{
91+
Symbol = Symbols.SPY,
92+
Time = reference.AddDays(i),
93+
Open = 100m + i,
94+
High = 101m + i,
95+
Low = 99m + i,
96+
Close = 100.5m + i,
97+
Volume = 1000
98+
});
99+
}
100+
wto.Update(new TradeBar
101+
{
102+
Symbol = Symbols.SPY,
103+
Time = reference.AddDays(expectedWarmUp - 1),
104+
Open = 100m,
105+
High = 101m,
106+
Low = 99m,
107+
Close = 100.5m,
108+
Volume = 1000
109+
});
110+
Assert.IsTrue(wto.IsReady);
111+
Assert.IsTrue(wto.WaveTrend.IsReady);
112+
Assert.IsTrue(wto.Signal.IsReady);
113+
}
114+
115+
[Test]
116+
public void DefaultConstructorUsesStandardParameters()
117+
{
118+
var wto = new WaveTrendOscillator();
119+
Assert.AreEqual("WTO(10,21,4)", wto.Name);
120+
Assert.AreEqual(2 * 10 + 21 + 4 - 3, wto.WarmUpPeriod);
121+
}
122+
}
123+
}

Tests/QuantConnect.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,9 @@
595595
<Content Include="TestData\spy_with_vwap.txt">
596596
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
597597
</Content>
598+
<Content Include="TestData\spy_wto.txt">
599+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
600+
</Content>
598601
<Content Include="TestData\spy_with_williamsR14.txt">
599602
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
600603
</Content>

0 commit comments

Comments
 (0)