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
20 changes: 20 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3018,6 +3018,26 @@ public RogersSatchellVolatility RSV(Symbol symbol, int period, Resolution? resol
return indicator;
}

/// <summary>
/// Creates a new YangZhangVolatility indicator for the symbol.
/// The indicator will be automatically updated on the given resolution.
/// </summary>
/// <param name="symbol">The symbol whose YangZhangVolatility we want</param>
/// <param name="period">The period of the rolling window used to compute volatility</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>A new YangZhangVolatility indicator with the specified period</returns>
[DocumentationAttribute(Indicators)]
public YangZhangVolatility YZV(Symbol symbol, int period,
Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
{
var name = CreateIndicatorName(symbol, $"YZV({period})", resolution);
var indicator = new YangZhangVolatility(name, period);
InitializeIndicator(indicator, resolution, selector, symbol);

return indicator;
}

/// <summary>
/// Creates a ZeroLagExponentialMovingAverage indicator for the symbol. The indicator will be automatically
/// updated on the given resolution.
Expand Down
167 changes: 167 additions & 0 deletions Indicators/YangZhangVolatility.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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;

namespace QuantConnect.Indicators
{
/// <summary>
/// Represents the Yang Zhang volatility estimator.
/// Yang Zhang is the most efficient known estimator of historical volatility that
/// is independent of drift and opening gaps. It combines overnight (open-to-previous-close)
/// variance, intraday (close-to-open) variance, and Rogers-Satchell variance with an
/// optimal weighting factor k.
/// Reference: Yang, D. and Zhang, Q. (2000). "Drift Independent Volatility Estimation
/// Based on High, Low, Open, and Close Prices."
/// </summary>
public class YangZhangVolatility : BarIndicator, IIndicatorWarmUpPeriodProvider
{
private readonly int _period;
private readonly decimal _k;

private decimal _previousClose;

private readonly IndicatorBase<IndicatorDataPoint> _overnightSum;
private readonly IndicatorBase<IndicatorDataPoint> _overnightSumSq;
private readonly IndicatorBase<IndicatorDataPoint> _intradaySum;
private readonly IndicatorBase<IndicatorDataPoint> _intradaySumSq;
private readonly IndicatorBase<IndicatorDataPoint> _rsSum;

/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => Samples >= _period + 1;

/// <summary>
/// Required period, in data points, for the indicator to be ready and fully initialized.
/// </summary>
public int WarmUpPeriod => _period + 1;

/// <summary>
/// Initializes a new instance of the <see cref="YangZhangVolatility"/> class using the specified name and period.
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="period">The period of the rolling window</param>
public YangZhangVolatility(string name, int period)
: base(name)
{
if (period < 2)
{
throw new ArgumentOutOfRangeException(nameof(period),
"YangZhangVolatility requires a period of at least 2.");
}

_period = period;

// k minimizes the variance of the combined estimator
_k = 0.34m / (1.34m + (period + 1m) / (period - 1m));

_overnightSum = new Sum(name + "_OvernightSum", period);
_overnightSumSq = new Sum(name + "_OvernightSumSq", period);
_intradaySum = new Sum(name + "_IntradaySum", period);
_intradaySumSq = new Sum(name + "_IntradaySumSq", period);
_rsSum = new Sum(name + "_RSSum", period);
}

/// <summary>
/// Initializes a new instance of the <see cref="YangZhangVolatility"/> class using the specified period.
/// </summary>
/// <param name="period">The period of the rolling window</param>
public YangZhangVolatility(int period)
: this($"YZV({period})", period)
{
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>A new value for this indicator</returns>
protected override decimal ComputeNextValue(IBaseDataBar input)
{
if (input.Open <= 0 || input.High <= 0 || input.Low <= 0 || input.Close <= 0)
{
return 0m;
}

if (Samples == 1)
{
_previousClose = input.Close;
return 0m;
}

if (_previousClose <= 0)
{
_previousClose = input.Close;
return 0m;
}

// Overnight return: ln(Open / PreviousClose)
var overnightReturn = (decimal)Math.Log((double)(input.Open / _previousClose));

// Intraday return: ln(Close / Open)
var intradayReturn = (decimal)Math.Log((double)(input.Close / input.Open));

// Rogers-Satchell per-bar value
var rsValue = (decimal)(
Math.Log((double)input.High / (double)input.Close) * Math.Log((double)input.High / (double)input.Open)
+ Math.Log((double)input.Low / (double)input.Close) * Math.Log((double)input.Low / (double)input.Open));

// Feed rolling sums
_overnightSum.Update(input.EndTime, overnightReturn);
_overnightSumSq.Update(input.EndTime, overnightReturn * overnightReturn);
_intradaySum.Update(input.EndTime, intradayReturn);
_intradaySumSq.Update(input.EndTime, intradayReturn * intradayReturn);
_rsSum.Update(input.EndTime, rsValue);

_previousClose = input.Close;

if (!IsReady)
{
return 0m;
}

var n = (decimal)_period;

// Overnight variance: sample variance of overnight returns
var overnightVariance = (_overnightSumSq.Current.Value - _overnightSum.Current.Value * _overnightSum.Current.Value / n) / (n - 1m);

// Intraday variance: sample variance of intraday returns
var intradayVariance = (_intradaySumSq.Current.Value - _intradaySum.Current.Value * _intradaySum.Current.Value / n) / (n - 1m);

// Rogers-Satchell variance: population mean of RS values
var rsVariance = _rsSum.Current.Value / n;

// Yang Zhang combined estimator
var yzVariance = overnightVariance + _k * intradayVariance + (1m - _k) * rsVariance;

return (decimal)Math.Sqrt((double)Math.Max(0m, yzVariance));
}

/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset()
{
_previousClose = 0;
_overnightSum.Reset();
_overnightSumSq.Reset();
_intradaySum.Reset();
_intradaySumSq.Reset();
_rsSum.Reset();
base.Reset();
}
}
}
202 changes: 202 additions & 0 deletions Tests/Indicators/YangZhangVolatilityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
/*
* 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, Parallelizable(ParallelScope.Fixtures)]
public class YangZhangVolatilityTests : CommonIndicatorTests<IBaseDataBar>
{
protected override IndicatorBase<IBaseDataBar> CreateIndicator()
{
RenkoBarSize = 1m;
return new YangZhangVolatility(9);
}

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

protected override string TestColumnName => "YZV9";

[Test]
public void YzvComputesCorrectly()
{
// Hand-computed values for YZV(3) with known OHLC bars
// Period 3, k = 0.34 / (1.34 + (3+1)/(3-1)) = 0.34 / (1.34 + 2) = 0.34 / 3.34
var yzv = new YangZhangVolatility(3);
var time = new DateTime(2024, 1, 1);

// Bar 1 (seed): O=100, H=102, L=99, C=101
yzv.Update(new TradeBar(time, Symbols.SPY, 100m, 102m, 99m, 101m, 1000));
Assert.IsFalse(yzv.IsReady);
Assert.AreEqual(0m, yzv.Current.Value);

// Bar 2: O=101.5, H=103, L=100.5, C=102 (prev close = 101)
yzv.Update(new TradeBar(time.AddDays(1), Symbols.SPY, 101.5m, 103m, 100.5m, 102m, 1000));
Assert.IsFalse(yzv.IsReady);
Assert.AreEqual(0m, yzv.Current.Value);

// Bar 3: O=102.5, H=104, L=101.5, C=103 (prev close = 102)
yzv.Update(new TradeBar(time.AddDays(2), Symbols.SPY, 102.5m, 104m, 101.5m, 103m, 1000));
Assert.IsFalse(yzv.IsReady);
Assert.AreEqual(0m, yzv.Current.Value);

// Bar 4: O=103.5, H=105, L=102, C=104 (prev close = 103)
// Now IsReady (Samples = 4 >= 3 + 1)
yzv.Update(new TradeBar(time.AddDays(3), Symbols.SPY, 103.5m, 105m, 102m, 104m, 1000));
Assert.IsTrue(yzv.IsReady);

// Hand-compute expected value:
// overnight returns: ln(101.5/101), ln(102.5/102), ln(103.5/103)
var o1 = Math.Log(101.5 / 101.0); // 0.004950...
var o2 = Math.Log(102.5 / 102.0); // 0.004889...
var o3 = Math.Log(103.5 / 103.0); // 0.004845...

// intraday returns: ln(102/101.5), ln(103/102.5), ln(104/103.5)
var c1 = Math.Log(102.0 / 101.5); // 0.004914...
var c2 = Math.Log(103.0 / 102.5); // 0.004866...
var c3 = Math.Log(104.0 / 103.5); // 0.004819...

// RS values: ln(H/C)*ln(H/O) + ln(L/C)*ln(L/O)
var rs1 = Math.Log(103.0 / 102.0) * Math.Log(103.0 / 101.5) + Math.Log(100.5 / 102.0) * Math.Log(100.5 / 101.5);
var rs2 = Math.Log(104.0 / 103.0) * Math.Log(104.0 / 102.5) + Math.Log(101.5 / 103.0) * Math.Log(101.5 / 102.5);
var rs3 = Math.Log(105.0 / 104.0) * Math.Log(105.0 / 103.5) + Math.Log(102.0 / 104.0) * Math.Log(102.0 / 103.5);

var n = 3.0;
var k = 0.34 / (1.34 + (n + 1.0) / (n - 1.0));

// Sample variance for overnight
var sumO = o1 + o2 + o3;
var sumOSq = o1 * o1 + o2 * o2 + o3 * o3;
var oVar = (sumOSq - sumO * sumO / n) / (n - 1.0);

// Sample variance for intraday
var sumC = c1 + c2 + c3;
var sumCSq = c1 * c1 + c2 * c2 + c3 * c3;
var cVar = (sumCSq - sumC * sumC / n) / (n - 1.0);

// RS mean
var rsVar = (rs1 + rs2 + rs3) / n;

var yzVar = oVar + k * cVar + (1.0 - k) * rsVar;
var expected = Math.Sqrt(Math.Max(0, yzVar));

Assert.AreEqual(expected, (double)yzv.Current.Value, 1e-10,
"YZV at bar 4 should match hand-computed value");
}

[Test]
public void MinimumPeriodOfTwo()
{
var yzv = new YangZhangVolatility(2);
var time = new DateTime(2024, 1, 1);

// Bar 1 (seed)
yzv.Update(new TradeBar(time, Symbols.SPY, 100m, 102m, 99m, 101m, 1000));
Assert.IsFalse(yzv.IsReady);

// Bar 2
yzv.Update(new TradeBar(time.AddDays(1), Symbols.SPY, 101.5m, 103m, 100m, 102m, 1000));
Assert.IsFalse(yzv.IsReady);

// Bar 3: now ready (Samples = 3 >= 2 + 1)
yzv.Update(new TradeBar(time.AddDays(2), Symbols.SPY, 102.5m, 104m, 101m, 103m, 1000));
Assert.IsTrue(yzv.IsReady);
Assert.Greater((double)yzv.Current.Value, 0,
"YZV(2) should produce a positive volatility estimate");
}

[Test]
public void FlatMarketReturnsZero()
{
// When all OHLC prices are identical every bar, all log returns are 0
var yzv = new YangZhangVolatility(3);
var time = new DateTime(2024, 1, 1);

for (int i = 0; i < 5; i++)
{
yzv.Update(new TradeBar(time.AddDays(i), Symbols.SPY, 100m, 100m, 100m, 100m, 1000));
}

Assert.IsTrue(yzv.IsReady);
Assert.AreEqual(0d, (double)yzv.Current.Value, 1e-15,
"Flat market with zero returns should produce zero volatility");
}

[Test]
public void LargeOvernightGap()
{
// A big gap up should produce higher volatility than steady bars
var yzv = new YangZhangVolatility(3);
var time = new DateTime(2024, 1, 1);

// Seed bar
yzv.Update(new TradeBar(time, Symbols.SPY, 100m, 101m, 99m, 100m, 1000));

// Steady bars
yzv.Update(new TradeBar(time.AddDays(1), Symbols.SPY, 100.5m, 101.5m, 99.5m, 101m, 1000));
yzv.Update(new TradeBar(time.AddDays(2), Symbols.SPY, 101.5m, 102.5m, 100.5m, 102m, 1000));

// Big gap up: open 20% above previous close
yzv.Update(new TradeBar(time.AddDays(3), Symbols.SPY, 122m, 123m, 121m, 122m, 1000));

Assert.IsTrue(yzv.IsReady);
Assert.Greater((double)yzv.Current.Value, 0.05,
"A 20% overnight gap should produce high volatility");
}

[Test]
public void PeriodBelowTwoThrows()
{
Assert.Throws<ArgumentOutOfRangeException>(() => new YangZhangVolatility(1));
Assert.Throws<ArgumentOutOfRangeException>(() => new YangZhangVolatility(0));
Assert.Throws<ArgumentOutOfRangeException>(() => new YangZhangVolatility(-5));
}

[Test]
public void ResetAndContinue()
{
var yzv = new YangZhangVolatility(3);
var time = new DateTime(2024, 1, 1);

// Feed to ready
for (int i = 0; i < 5; i++)
{
yzv.Update(new TradeBar(time.AddDays(i), Symbols.SPY,
100m + i, 102m + i, 99m + i, 101m + i, 1000));
}
Assert.IsTrue(yzv.IsReady);
var valueBeforeReset = yzv.Current.Value;

// Reset
yzv.Reset();
Assert.IsFalse(yzv.IsReady);
Assert.AreEqual(0m, yzv.Current.Value);

// Feed again with same data, should get same result
for (int i = 0; i < 5; i++)
{
yzv.Update(new TradeBar(time.AddDays(i), Symbols.SPY,
100m + i, 102m + i, 99m + i, 101m + i, 1000));
}
Assert.IsTrue(yzv.IsReady);
Assert.AreEqual((double)valueBeforeReset, (double)yzv.Current.Value, 1e-10,
"After reset, same data should produce same result");
}
}
}
Loading