Any chance on seeing an "Anchored VWAP" Indicator, where we can set the anchor to a specific date (along with the time as is currently available)?
I've coded the Anchored VWAP as an indicator within ThinkorSwim successfully, but C# is still a challenge for me.
I've coded the Anchored VWAP as an indicator within ThinkorSwim successfully, but C# is still a challenge for me.
Rename
C++ is a challenge for me, too. Perhaps someone could code it for you in C# if there's description of the indicator, and following your thinkScript example code might help.
Here's a good start for you to turn it into a custom indicator -
CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using System.Collections.Generic; namespace WealthScript72 { public class MyStrategy : UserStrategyBase { public override void Initialize(BarHistory bars) { DateTime startDateTime = new DateTime(2023, 11, 1, 13, 30, 0); TimeSeries myvwap = VWAP2(bars, startDateTime); PlotTimeSeries(myvwap, "VWAP2", "Price", WLColor.NeonYellow); } TimeSeries VWAP2(BarHistory bars, DateTime startDateTime) { TimeSeries vwap2 = new TimeSeries(bars.DateTimes, true); int startIdx = bars.IndexOf(startDateTime); if (startIdx > -1) { double d = 0, cvol = 0; for (int bar = startIdx; bar < bars.Count; bar++) { d += (bars.AveragePriceHLC[bar] * bars.Volume[bar]); cvol += bars.Volume[bar]; vwap2[bar] = d / cvol; } } return vwap2; } public override void Execute(BarHistory bars, int idx) { } } }
Thank you very much, Cone!
But, I'm having difficulty saving this indicator even while in Windows Admin mode as required, and even after the code compiles OK, I still receive an "Error populating indicator: "Object reference not set to an instance of an object."
I believe that I did provide the necessary Indicator Properties.
But, I'm having difficulty saving this indicator even while in Windows Admin mode as required, and even after the code compiles OK, I still receive an "Error populating indicator: "Object reference not set to an instance of an object."
I believe that I did provide the necessary Indicator Properties.
You have to make it into an indicator. This code doesn't do that.
Oh, well, THAT's a disappointment. I thought that the code you were offering was a "starting point" toward making a custom indicator, so I carefully read the F1 help page on creating a custom indicator which I could accomplish, using your code.
I'm not a coder. I suspect many of us are not coders on this platform. There should be a much easier way to accomplish something like this, IMO.
I'm not a coder. I suspect many of us are not coders on this platform. There should be a much easier way to accomplish something like this, IMO.
QUOTE:
There should be a much easier way to accomplish something like this, IMO.
A much easier way to code a custom logic in C#? Yes, it's called ChatGPT.
What ideas do you have to program a new indicator in an easier way?
It will only take about 5 minutes to turn that into an indicator, so I'll do it since this gives me another idea for another indicator that could use this one.
Also, we have the Concierge Service for custom programming jobs and consulting.
It will only take about 5 minutes to turn that into an indicator, so I'll do it since this gives me another idea for another indicator that could use this one.
Also, we have the Concierge Service for custom programming jobs and consulting.
Here you go. With this anchored VWAP indicator you can anchor by date/time or by length (sliding window of n bars). Note that the date dropdown is kind of hard to read if you're using WL8 dark mode...
CODE:
using System; using WealthLab.Core; using WealthLab.Indicators; namespace WLUtility.Indicators { /// <summary> /// Anchored VWAP indicator that allows anchoring VWAP by date/time or by an n-bar sliding window. /// </summary> public class AnchoredVwap : IndicatorBase { private const int ParamIndexSource = 0; private const int ParamIndexAnchorDate = 1; private const int ParamIndexUseLength = 2; private const int ParamIndexLength = 3; public AnchoredVwap() { // for WL 8 - do not remove } public AnchoredVwap(BarHistory source, DateTime anchorDate, bool useLength, int length) { Parameters[ParamIndexSource].Value = source; Parameters[ParamIndexAnchorDate].Value = anchorDate; Parameters[ParamIndexUseLength].Value = useLength; Parameters[ParamIndexLength].Value = length; Populate(); } private BarHistory Source => Parameters[ParamIndexSource].AsBarHistory; private DateTime AnchorDateTime => Parameters[ParamIndexAnchorDate].AsDate; private bool UseLength => Parameters[ParamIndexUseLength].AsBoolean; private int Length => Parameters[ParamIndexLength].AsInt; public override string Name => "Anchored VWAP"; public override string Abbreviation => "Anchored VWAP"; public override string HelpDescription => "Anchored VWAP by date or bar count"; public override string PaneTag => "Price"; public override WLColor DefaultColor { get; } = WLColor.Purple; public override void Populate() { DateTimes = Source.DateTimes; double cumulativePriceVolume = 0, cumulativeVolume = 0; // be speedy var source = Source; var hlc = source.AveragePriceHLC; var volume = source.Volume; if (UseLength) { // sliding window... var length = Length; if (length < 1) { // For an invalid length don't throw an exception because it will cause a problem when using building blocks // as of WL 8 Build 54. // When running a strategy or dragged to a chart, all values will be NaN. // So, dont' use an invalid length. return; } for (var idx = 0; idx < length - 1; idx++) { cumulativePriceVolume += hlc[idx] * volume[idx]; cumulativeVolume += volume[idx]; } var removeIdx = 0; for (var idx = length - 1; idx < source.Count; idx++) { cumulativePriceVolume += hlc[idx] * volume[idx]; cumulativeVolume += volume[idx]; Values[idx] = cumulativePriceVolume / cumulativeVolume; // remove oldest from accumulators cumulativePriceVolume -= hlc[removeIdx] * volume[removeIdx]; cumulativeVolume -= volume[removeIdx]; removeIdx++; } } else { // fixed date/time... var startIdx = Source.IndexOf(AnchorDateTime); if (startIdx > -1) { for (var idx = startIdx; idx < source.Count; idx++) { cumulativePriceVolume += hlc[idx] * volume[idx]; cumulativeVolume += volume[idx]; Values[idx] = cumulativePriceVolume / cumulativeVolume; } } } } public static AnchoredVwap Series(BarHistory source, DateTime anchorDate, bool useLength, int length) { var key = CacheKey("AnchoredVWAP", anchorDate, useLength, length); if (source.Cache.TryGetValue(key, out var vwap)) { return (AnchoredVwap) vwap; } var result = new AnchoredVwap(source, anchorDate, useLength, length); source.Cache[key] = result; return result; } protected override void GenerateParameters() { AddParameter("Source", ParameterType.BarHistory, null); AddParameter("Anchor Date", ParameterType.Date, DateTime.Now.Date); AddParameter("Use length instead of anchor date?", ParameterType.Boolean, false); AddParameter("Length", ParameterType.Int32, 20); } } }
Thank you, Paul1986!! That worked like a charm!
I improved the indicator by allowing a TimeSeries to be specified for the price component (the P) in VWAP. This way you can use any TimeSeries for the price instead of the previously hardcoded AveragePriceHLC. Of course, the TimeSeries should ideally be derived from and synchronized with the source bars.
The TimeSeries defaults to AveragePriceHLC of the bars. This is a breaking change to the code of Post #8 above because of the TimeSeries parameter was added...
The TimeSeries defaults to AveragePriceHLC of the bars. This is a breaking change to the code of Post #8 above because of the TimeSeries parameter was added...
CODE:
using System; using WealthLab.Core; using WealthLab.Indicators; namespace WLUtility.Indicators { /// <summary> /// Anchored VWAP indicator that allows anchoring VWAP by date/time or by an n-bar sliding window. /// The price (the P in VWAP) is specified by a TimeSeries. This defaults to AveragePriceHLC. /// </summary> public class AnchoredVwap : IndicatorBase { private const int ParamIndexSource = 0; private const int ParamIndexTimeSeries = 1; private const int ParamIndexAnchorDate = 2; private const int ParamIndexUseLength = 3; private const int ParamIndexLength = 4; public AnchoredVwap() { // for WL 8 - do not remove } public AnchoredVwap(BarHistory source, TimeSeries timeSeries, DateTime anchorDate, bool useLength, int length) { Parameters[ParamIndexSource].Value = source; Parameters[ParamIndexTimeSeries].Value = timeSeries; Parameters[ParamIndexAnchorDate].Value = anchorDate; Parameters[ParamIndexUseLength].Value = useLength; Parameters[ParamIndexLength].Value = length; Populate(); } private BarHistory Source => Parameters[ParamIndexSource].AsBarHistory; private TimeSeries TimeSeries => Parameters[ParamIndexTimeSeries].AsTimeSeries; private DateTime AnchorDateTime => Parameters[ParamIndexAnchorDate].AsDate; private bool UseLength => Parameters[ParamIndexUseLength].AsBoolean; private int Length => Parameters[ParamIndexLength].AsInt; public override string Name => "Anchored VWAP"; public override string Abbreviation => "Anchored VWAP"; public override string HelpDescription => "Anchored VWAP by date or bar count"; public override string PaneTag => "Price"; public override WLColor DefaultColor { get; } = WLColor.Purple; public override void Populate() { double cumulativePriceVolume = 0, cumulativeVolume = 0; // be speedy var source = Source; var ts = TimeSeries; var volume = source.Volume; var tsFirstValidIndex = ts.FirstValidIndex; DateTimes = source.DateTimes; if (UseLength) { // sliding window... var length = Length; if (length < 1) { // For an invalid length don't throw an exception because it will cause a problem when using building blocks // as of WL 8 Build 54. // When running a strategy or dragged to a chart, all values will be NaN. // So, dont' use an invalid length. return; } var startIdx = tsFirstValidIndex + length - 1; // accumulate volume and price * volume to use for the first result value that can be generated for (var idx = tsFirstValidIndex; idx < startIdx && idx < ts.Count; idx++) { cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; } var removeIdx = tsFirstValidIndex; for (var idx = startIdx; idx < ts.Count; idx++) { cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; Values[idx] = cumulativeVolume == 0 ? double.NaN : cumulativePriceVolume / cumulativeVolume; // remove oldest from accumulators cumulativePriceVolume -= ts[removeIdx] * volume[removeIdx]; cumulativeVolume -= volume[removeIdx]; removeIdx++; } // if no valid values, just set to one past end of the time series FirstValidIndex = Math.Min(startIdx, ts.Count); } else { // fixed date/time... var startIdx = source.IndexOf(AnchorDateTime); if (startIdx > -1) { // if starting in an invalid index into the time series then // move up to the first valid index if (startIdx < tsFirstValidIndex) { startIdx = tsFirstValidIndex; } for (var idx = startIdx; idx < source.Count; idx++) { cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; Values[idx] = cumulativeVolume == 0 ? double.NaN : cumulativePriceVolume / cumulativeVolume; } FirstValidIndex = startIdx; } else { // no valid values, just set to one past end of the time series FirstValidIndex = ts.Count; } } } public static AnchoredVwap Series(BarHistory source, TimeSeries timeSeries, DateTime anchorDate, bool useLength, int length) { var key = CacheKey("AnchoredVWAP", timeSeries, anchorDate, useLength, length); if (source.Cache.TryGetValue(key, out var vwap)) { return (AnchoredVwap) vwap; } var result = new AnchoredVwap(source, timeSeries, anchorDate, useLength, length); source.Cache[key] = result; return result; } protected override void GenerateParameters() { AddParameter("Source", ParameterType.BarHistory, null); AddParameter("Price", ParameterType.TimeSeries, PriceComponent.AveragePriceHLC); AddParameter("Anchor Date", ParameterType.Date, DateTime.Now.Date); AddParameter("Use length instead of anchor date?", ParameterType.Boolean, false); AddParameter("Length", ParameterType.Int32, 20); } } }
Many Thanks, Paul! It works beautifully, and the WL8 Optimizer reveals some very interesting preliminary results which seem to contradict some popular web-based opinions re: anchor placement.
BTW, have you ever seen the Auto-Anchored VWAP at work?
Trading view has it in their platform.
Below is the technical definition in case you are interested.
https://www.tradingview.com/support/solutions/43000652199-vwap-auto-anchored/
Thanks, again, for your coding help.
BTW, have you ever seen the Auto-Anchored VWAP at work?
Trading view has it in their platform.
Below is the technical definition in case you are interested.
https://www.tradingview.com/support/solutions/43000652199-vwap-auto-anchored/
Thanks, again, for your coding help.
@Neverlong - thanks for the reference.
I created an indicator with this code, but I'm getting the "index out of range" error, and no matter where I set the StartIndex I get the same error.
This seems to be the offending code:
This seems to be the offending code:
CODE:
AVWAP = new AnchoredVwap(bars, bars.AveragePriceOHLC, new DateTime(2024, 9, 20), true, Parameters[22].AsInt); PlotIndicator(AVWAP, WLColor.Purple); _startIndexList.Add(AVWAP);
What symbol?
What scale?
Does your strategy have at least 23 parameters? (i.e. 0 through 22 inclusive). If so, what is the value of Parameters[22].AsInt?
What scale?
Does your strategy have at least 23 parameters? (i.e. 0 through 22 inclusive). If so, what is the value of Parameters[22].AsInt?
The symbol is TQQQ on the hourly timeframe.
Yes, it has 23 parameters, and I've tested a couple different values of the parameter, between 5 and 200 and I still get the error.
The code compiles ok, I'm just getting this error when I run it.
EDIT: the code runs without the A-VWAP indicator, and the A-VWAP indicator runs ok in other strategies.
thanks
Yes, it has 23 parameters, and I've tested a couple different values of the parameter, between 5 and 200 and I still get the error.
The code compiles ok, I'm just getting this error when I run it.
EDIT: the code runs without the A-VWAP indicator, and the A-VWAP indicator runs ok in other strategies.
thanks
When you have more than a handful of parameters, it would be far better to assign them to Parameter variables instead of relying on the index.
Re: Yes, it has 23 parameters
Just to make sure, put this code at the end of your Initialize() section and see what it says:
Re: Yes, it has 23 parameters
Just to make sure, put this code at the end of your Initialize() section and see what it says:
CODE:
WriteToDebugLog("Parameters.Count is " + Parameters.Count);
Cone is right - check your parameters. I wrote a little test strategy, and I could not recreate the error you reported. I tried a lot of manual parameter and strategy setting combinations, as well as performing optimization.
Here is the test strategy if you want to try it out...
Here is the test strategy if you want to try it out...
CODE:
using System; using WealthLab.Backtest; using WealthLab.Core; using WLUtility.Indicators; namespace WealthLabStrategies.Test { public class TestAnchoredVwap : UserStrategyBase { public TestAnchoredVwap() { ParamLength = AddParameter("Length", ParameterType.Int32, 5, 5, 200, 5); } private Parameter ParamLength { get; } private int Length { get; set; } private AnchoredVwap Avwap { get; set; } public override void Initialize(BarHistory bars) { Length = ParamLength.AsInt; Avwap = AnchoredVwap.Series(bars, bars.AveragePriceOHLC, new DateTime(2024, 9, 20), true, Length); PlotIndicator(Avwap); } public override void Execute(BarHistory bars, int idx) { } } }
Thanks, Paul and Cone, you nailed it. I didn't have 23 parameters, just 20.
Chalk it up to not enough sleep last night.
Cone, I do need to simply the code a bit - I don't use all those parameters anymore.
Chalk it up to not enough sleep last night.
Cone, I do need to simply the code a bit - I don't use all those parameters anymore.
How to reinitialize the indicator during the backtest except for creating 100 copies of the indicator at once with different starting points?
Another option i see is to hardcode the starting point selection logic directly into the indicator
Another option i see is to hardcode the starting point selection logic directly into the indicator
@ww5 - I believe the following is what you want. I added a Reset method to the indicator. That way you can call it from your Execute method whenever it determines it needs to move the anchor date or change the length. Please be sure to read the class comment header, especially about plotting. Also, after the updated indicator code is updated test code.
(Side note: 99% of the comments were AI-generated in about one minute!)
Strategy for testing AnchoredVwap...
(Side note: 99% of the comments were AI-generated in about one minute!)
CODE:
using System; using System.Collections.Generic; using WealthLab.Core; using WealthLab.Indicators; namespace WLUtility.Indicators { /// <summary> /// Anchored Volume Weighted Average Price (VWAP) indicator that calculates VWAP from a specified anchor point. /// This indicator supports two anchoring modes: /// 1. Date-based anchoring: VWAP is calculated from a specific date/time forward /// 2. Length-based anchoring: VWAP is calculated using a sliding window of n bars /// The price component (the P in VWAP) is specified by a TimeSeries parameter, which defaults to AveragePriceHLC. /// Key Features: /// - Can anchor VWAP to any specific date or use a rolling window /// - Customizable price series (typical price, close, OHLC/4, etc.) /// - Cached for performance when using the Series static method /// - Reset method allows dynamic parameter changes during runtime /// Important Notes: /// - When using Reset() with useLength = false (date-based anchoring), PlotIndicator may only show /// the latest values or no values at all, depending on the anchor date relative to the current data range. /// - Date-based anchoring creates a cumulative VWAP from the anchor date forward, which may result in /// limited visible plot data if the anchor is recent. /// - Length-based anchoring provides a continuous rolling VWAP suitable for all chart timeframes. /// </summary> public sealed class AnchoredVwap : IndicatorBase { private const int ParamIndexSource = 0; private const int ParamIndexTimeSeries = 1; public const int ParamIndexAnchorDate = 2; public const int ParamIndexUseLength = 3; public const int ParamIndexLength = 4; private const bool DefaultUseLength = false; private const int DefaultLength = 20; private const string CacheKeyPreamble = "AnchoredVWAP"; public AnchoredVwap() { // needed for WL 8 - do not remove } public AnchoredVwap(BarHistory source, TimeSeries timeSeries, DateTime anchorDate, bool useLength = DefaultUseLength, int length = DefaultLength) { Parameters[ParamIndexSource].Value = source; Parameters[ParamIndexTimeSeries].Value = timeSeries; Parameters[ParamIndexAnchorDate].Value = anchorDate; Parameters[ParamIndexUseLength].Value = useLength; Parameters[ParamIndexLength].Value = length; Populate(); } private BarHistory Source => Parameters[ParamIndexSource].AsBarHistory; private TimeSeries TimeSeries => Parameters[ParamIndexTimeSeries].AsTimeSeries; private DateTime AnchorDateTime => Parameters[ParamIndexAnchorDate].AsDate; private bool UseLength => Parameters[ParamIndexUseLength].AsBoolean; private int Length => Parameters[ParamIndexLength].AsInt; public override string Name => "Anchored VWAP"; public override string Abbreviation => "Anchored VWAP"; public override string HelpDescription => "Anchored VWAP by date or bar count"; public override string PaneTag => "Price"; public override WLColor DefaultColor { get; } = WLColor.Purple; private bool IsCached { get; set; } public override void Populate() { // Initialize cumulative accumulators for VWAP calculation // VWAP = Sum(Price * Volume) / Sum(Volume) double cumulativePriceVolume = 0, cumulativeVolume = 0; // Cache frequently accessed properties for performance var source = Source; var ts = TimeSeries; var volume = source.Volume; var tsFirstValidIndex = ts.FirstValidIndex; // Set DateTimes to match the source bar history DateTimes = source.DateTimes; if (UseLength) { // ===== LENGTH-BASED ANCHORING MODE ===== // Calculate VWAP using a sliding window of fixed length // This creates a rolling VWAP that moves with each new bar var length = Length; if (length < 1) { // For an invalid length don't throw an exception because it will cause a problem when using building blocks // as of WL 8 Build 54. // When running a strategy or dragged to a chart, all values will be NaN. // So, don't use an invalid length. return; } // Calculate the starting index where we can first generate a valid VWAP value // We need at least 'length' bars of data to calculate the first VWAP var startIdx = tsFirstValidIndex + length - 1; // Pre-accumulate volume and price*volume for the initial window // This builds up the cumulative values for bars [tsFirstValidIndex, startIdx) for (var idx = tsFirstValidIndex; idx < startIdx && idx < ts.Count; idx++) { cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; } // Track which bar to remove from the sliding window as we advance var removeIdx = tsFirstValidIndex; // Calculate VWAP for each bar starting from startIdx for (var idx = startIdx; idx < ts.Count; idx++) { // Add the current bar to the window cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; // Calculate VWAP: avoid division by zero if no volume Values[idx] = cumulativeVolume == 0 ? double.NaN : cumulativePriceVolume / cumulativeVolume; // Remove the oldest bar from the sliding window to maintain fixed length cumulativePriceVolume -= ts[removeIdx] * volume[removeIdx]; cumulativeVolume -= volume[removeIdx]; removeIdx++; } // Set the first valid index - if no valid values, set to end of series FirstValidIndex = Math.Min(startIdx, ts.Count); } else { // ===== DATE-BASED ANCHORING MODE ===== // Calculate cumulative VWAP starting from a specific anchor date/time // This creates a growing VWAP that accumulates from the anchor point forward // Find the bar index corresponding to the anchor date/time var startIdx = source.IndexOf(AnchorDateTime); if (startIdx > -1) { // If the anchor date falls before the first valid index of the time series, // adjust to start from the first valid data point if (startIdx < tsFirstValidIndex) { startIdx = tsFirstValidIndex; } // Calculate cumulative VWAP from anchor point to end of data for (var idx = startIdx; idx < source.Count; idx++) { // Accumulate price*volume and volume from anchor date forward cumulativePriceVolume += ts[idx] * volume[idx]; cumulativeVolume += volume[idx]; // Calculate cumulative VWAP: avoid division by zero if no volume Values[idx] = cumulativeVolume == 0 ? double.NaN : cumulativePriceVolume / cumulativeVolume; } // Set first valid index to the anchor starting point FirstValidIndex = startIdx; } else { // Anchor date not found in the data range // Set FirstValidIndex beyond the end to indicate no valid values FirstValidIndex = ts.Count; } } } /// <summary> /// Resets the anchored VWAP with new parameters and recalculates values. /// This method allows you to change the anchor date, switch between date-based and length-based anchoring, /// and adjust the length of the sliding window (when useLength is true) on the fly. /// Important: When switching from length-based to date-based anchoring (useLength = false), the indicator /// will start a new cumulative VWAP calculation from the specified anchor date. This may result in /// discontinuity of the VWAP line on the chart. It is recommended to use this method with caution /// and to be aware of the current chart context (e.g., visible range, anchor date, etc.). /// </summary> /// <param name="anchorDate">New anchor date</param> /// <param name="useLength">Whether to use length-based anchoring instead of date</param> /// <param name="length">New length for sliding window (when useLength is true)</param> public void Reset(DateTime anchorDate, bool useLength = DefaultUseLength, int length = DefaultLength) { // Remove from cache with old parameters before updating if (IsCached) { var oldKey = CacheKey(CacheKeyPreamble, TimeSeries, AnchorDateTime, UseLength, Length); Source.Cache.TryRemove(oldKey, out _); } // preserve current values to restore them after recalculation var copy = new List<double>(Values); Parameters[ParamIndexAnchorDate].Value = anchorDate; Parameters[ParamIndexUseLength].Value = useLength; Parameters[ParamIndexLength].Value = length; Populate(); // Restore values up to the FirstValidIndex for (var i = 0; i < FirstValidIndex; i++) { Values[i] = copy[i]; } if (IsCached) { // Add back to cache with new parameters var newKey = CacheKey(CacheKeyPreamble, TimeSeries, anchorDate, useLength, length); Source.Cache[newKey] = this; } } /// <summary> /// Creates or retrieves a cached AnchoredVwap indicator instance. /// This static factory method provides performance optimization by caching indicator instances /// based on their parameter values. Multiple calls with identical parameters will return /// the same cached instance rather than creating new objects. /// Cache Key Components: /// - TimeSeries (price data source) /// - Anchor DateTime (for date-based anchoring) /// - UseLength flag (anchoring mode) /// - Length value (window size for length-based anchoring) /// Important Notes: /// - Cached instances have IsCached = true, affecting Reset() method behavior /// - Cache is maintained at the BarHistory level /// - Changing parameters via Reset() will update the cache accordingly /// - Use this method when you need consistent indicator instances across multiple strategy calls /// </summary> /// <param name="source">The BarHistory containing price and volume data</param> /// <param name="timeSeries">The price series to use (e.g., Close, AveragePriceHLC, etc.)</param> /// <param name="anchorDate">The anchor date/time for date-based anchoring</param> /// <param name="useLength">True for length-based sliding window, false for date-based anchoring</param> /// <param name="length">Window size for length-based anchoring (ignored when useLength is false)</param> /// <returns>An AnchoredVwap indicator instance (cached or newly created)</returns> public static AnchoredVwap Series(BarHistory source, TimeSeries timeSeries, DateTime anchorDate, bool useLength = DefaultUseLength, int length = DefaultLength) { var key = CacheKey(CacheKeyPreamble, timeSeries, anchorDate, useLength, length); if (source.Cache.TryGetValue(key, out var vwap)) { return (AnchoredVwap) vwap; } var result = new AnchoredVwap(source, timeSeries, anchorDate, useLength, length); source.Cache[key] = result; result.IsCached = true; return result; } protected override void GenerateParameters() { AddParameter("Source", ParameterType.BarHistory, null); AddParameter("Price", ParameterType.TimeSeries, PriceComponent.AveragePriceHLC); AddParameter("Anchor Date", ParameterType.Date, DateTime.Now.Date); AddParameter("Use length instead of anchor date?", ParameterType.Boolean, DefaultUseLength); AddParameter("Length", ParameterType.Int32, DefaultLength); } } }
Strategy for testing AnchoredVwap...
CODE:
using System; using WealthLab.Backtest; using WealthLab.Core; using WLUtility.Indicators; namespace WealthLabStrategies.Test { /// <summary> /// Test strategy for the AnchoredVwap indicator that demonstrates its functionality and Reset method behavior. /// This strategy creates an AnchoredVwap indicator and dynamically changes its parameters during execution /// to test the Reset method's ability to handle parameter changes and recalculation. /// Key Testing Features: /// - Tests both length-based and date-based anchoring modes /// - Dynamically resets anchor parameters every 30 minutes (when minutes % 30 == 1) /// - Uses varying length parameters based on time of day to test different window sizes /// - Validates indicator values by counting valid numbers vs NaN values in Cleanup /// Parameters: /// - Length: Window size for length-based anchoring (5-200, step 5) /// - Use Length: Switch between length-based (1) and date-based (0) anchoring /// Testing Notes: /// - When UseLength = false (date-based), frequent resets may result in limited visible data /// - The strategy logs value counts to help verify indicator behavior /// - Reset frequency and parameter changes are designed to stress-test the indicator /// </summary> public class TestAnchoredVwap : UserStrategyBase { public TestAnchoredVwap() { ParamLength = AddParameter("Length", ParameterType.Int32, 5, 5, 200, 5); ParamUseLength = AddParameter("Use Length ?", ParameterType.Int32, 0, 0, 1); } private Parameter ParamUseLength { get; } private Parameter ParamLength { get; } private int Length { get; set; } /// <summary> /// Determines anchoring mode: true for length-based sliding window, false for date-based anchoring /// </summary> private bool UseLength => ParamUseLength.AsInt != 0; /// <summary> /// The AnchoredVwap indicator instance being tested /// </summary> private AnchoredVwap Avwap { get; set; } /// <summary> /// Initializes the test strategy by creating an AnchoredVwap indicator with initial parameters /// and adding it to the chart for visual verification. /// </summary> /// <param name="bars">The bar history data</param> public override void Initialize(BarHistory bars) { Length = ParamLength.AsInt; // Create AnchoredVwap with yesterday's date as initial anchor point //Avwap = new AnchoredVwap(bars, bars.AveragePriceOHLC, DateTime.Today.AddDays(-1), UseLength, Length); Avwap = AnchoredVwap.Series(bars, bars.AveragePriceOHLC, DateTime.Today.AddDays(-1), UseLength, Length); PlotIndicator(Avwap); } /// <summary> /// Executes on each bar to test the Reset method functionality. /// Resets the AnchoredVwap parameters every n minutes (when minutes % n == 0) /// with a new anchor date and dynamically calculated length based on time of day when /// testing length-based VWAP. /// </summary> /// <param name="bars">The bar history data</param> /// <param name="idx">Current bar index</param> public override void Execute(BarHistory bars, int idx) { var isStreaming = ExecutionMode is StrategyExecutionMode.StrategyMonitor or StrategyExecutionMode.StreamingChart; var td = bars.DateTimes[idx].TimeOfDay; // Test anchored VWAP Reset method every n minutes // Prevent excessive resets and logging during streaming by checking if the datetime has already been logged if (td.Minutes % 5 == 0) { // Reset with current bar's datetime as new anchor and variable length based on time var newLength = td.Minutes / 5 + 5; Avwap.Reset(bars.DateTimes[idx], UseLength, newLength); // Log the reset action and current parameters // if streaming, log only the last bar to avoid clutter if (!isStreaming || idx == bars.Count - 1) { var currentParameterValues = $"Avwap current parameters are: " + $"AnchorDate={Avwap.Parameters[AnchoredVwap.ParamIndexAnchorDate].Value}" + $"UseLength={Avwap.Parameters[AnchoredVwap.ParamIndexUseLength].Value}, Length={Avwap.Parameters[AnchoredVwap.ParamIndexLength].Value}, "; var expectedParameterValues = $"Expected parameters are: AnchorDate={bars.DateTimes[idx]}, UseLength={UseLength}, Length={newLength}"; var message = currentParameterValues + Environment.NewLine + expectedParameterValues; WLHost.Instance.AddLogItem("TestAnchoredVwap", message, WLColor.AliceBlue); } } } /// <summary> /// Performs validation after strategy execution by counting valid values vs NaN values /// in the AnchoredVwap indicator. This helps verify the indicator's behavior and /// identify any issues with the Reset method or parameter changes. /// </summary> /// <param name="bars">The bar history data</param> public override void Cleanup(BarHistory bars) { var numberCount = 0; var nanCount = 0; // Count valid numbers vs NaN values for validation for (var i = 0; i < Avwap.Count; i++) { if (!double.IsNaN(Avwap[i])) { numberCount++; } else { nanCount++; } } // Log results for analysis var message = $"Number Count: {numberCount}, NaN Count: {nanCount} for {bars.Symbol}"; WLHost.Instance.AddLogItem("TestAnchoredVwap", message, WLColor.AliceBlue); } } }
@paul1986, thanks, just what I wanted! Elegant solution.
Your Response
Post
Edit Post
Login is required