- ago
Hi,
I am testing if filters can be added to PreExecute to further refine the trade signals rather than based on just one indicator.
As a baseline I used the code in QuickRef..PreExecute:
CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Indicators; using WealthLab.ChartWPF; using System.Drawing; using System.Collections.Generic; namespace WealthScript4 {    public class MyStrategy : UserStrategyBase    {       //the list of symbols that we should buy each bar       private static List<BarHistory> buys = new List<BarHistory>();       //create the weight indicator and stash it into the BarHistory object for reference in PreExecute       public override void Initialize(BarHistory bars)       {          rsi = new RSI(bars.Close, 14);          bars.Cache["RSI"] = rsi;       }       //this is called prior to the Execute loop, determine which symbols have the lowest RSI       public override void PreExecute(DateTime dt, List<BarHistory> participants)       {          //store the symbols' RSI value in their BarHistory instances          foreach (BarHistory bh in participants)          {             RSI rsi = (RSI)bh.Cache["RSI"];             int idx = GetCurrentIndex(bh); //this returns the index of the BarHistory for the bar currently being processed             double rsiVal = rsi[idx];             bh.UserData = rsiVal; //save the current RSI value along with the BarHistory instance          }          //sort the participants by RSI value (lowest to highest)          participants.Sort((a, b) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));          //keep the top 3 symbols          buys.Clear();          for (int n = 0; n < 3; n++)          {             if (n >= participants.Count)                break;             buys.Add(participants[n]);          }       }       //execute the strategy rules here, this is executed once for each bar in the backtest history       public override void Execute(BarHistory bars, int idx)       {          bool inBuyList = buys.Contains(bars);          if (!HasOpenPosition(bars, PositionType.Long))          {             //buy logic - buy if it's in the buys list             if (inBuyList)                PlaceTrade(bars, TransactionType.Buy, OrderType.Market);          }          else          {             //sell logic, sell if it's not in the buys list             if (!inBuyList)                PlaceTrade(bars, TransactionType.Sell, OrderType.Market);          }       }       //declare private variables below       private RSI rsi;    } }


Test code with some filters added:
CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Indicators; using WealthLab.ChartWPF; using System.Drawing; using System.Collections.Generic; namespace WealthScript3 {    public class MyStrategy : UserStrategyBase    {       //the list of symbols that we should buy each bar       private static List<BarHistory> buys = new List<BarHistory>();       //create the weight indicator and stash it into the BarHistory object for reference in PreExecute       public override void Initialize(BarHistory bars)       {          rsi = new RSI(bars.Close, 14);          bars.Cache["RSI"] = rsi;          PlotTimeSeries(rsi, bars.Symbol + ", " + rsi.Description, "pRSI", Color.Red, PlotStyles.Line);          //I added          // HV          _hv = new HV(bars.Close, 30, 252);          PlotTimeSeries(_hv, bars.Symbol + ", " + _hv.Description, "pHV", Color.Blue, PlotStyles.Line);          bars.Cache["HV"] = _hv;          // ROC          _roc = new ROC(bars.Close, 20);          PlotTimeSeries(_roc, bars.Symbol + ", " + _roc.Description, "pROC", Color.Chocolate, PlotStyles.Line);          bars.Cache["ROC"] = _roc;          StartIndex = 126;       }       //this is called prior to the Execute loop, determine which symbols have the lowest RSI       public override void PreExecute(DateTime dt, List<BarHistory> participants)       {          //store the symbols' RSI value in their BarHistory instances          foreach (BarHistory bh in participants)          {             int idx = GetCurrentIndex(bh); //this returns the index of the BarHistory for the bar currently being processed             RSI rsi = (RSI)bh.Cache["RSI"];             HV _hv = (HV)bh.Cache["HV"];             ROC _roc = (ROC)bh.Cache["ROC"];             bool IsLastTDOM = bh.DateTimes[idx].IsLastTradingDayOfMonth(bh); //its last trading day of month             if (idx >= StartIndex)             {                if (IsLastTDOM)                {                   WriteToDebugLog(idx + "\t" + bh.DateTimes[idx].ToShortDateString() + "\t" + bh.Symbol                      + "\t" + "RSI = " + rsi[idx].ToString("#0.00")                      + "\t" + "HV = " + _hv[idx].ToString("#0.00")                      + "\t" + "ROC = " + _roc[idx].ToString("#0.00")); //check                   if (rsi[idx] < 60                      && _hv[idx] < 30                      && _roc[idx] > 0)                   {                      double rsiVal = rsi[idx];                      bh.UserData = rsiVal; //save the current RSI value along with the BarHistory instance                   }                }             }          }          //sort the participants by RSI value (lowest to highest)          participants.Sort((a, b) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));          //keep the top 3 symbols          buys.Clear();          for (int n = 0; n < 3; n++)          {             if (n >= participants.Count)                break;             buys.Add(participants[n]);          }       }       //execute the strategy rules here, this is executed once for each bar in the backtest history       public override void Execute(BarHistory bars, int idx)       {          if (bars.DateTimes[idx].IsLastTradingDayOfMonth(bars))          {             //Check             for (int a = buys.Count -1; a > -1; a--)                WriteToDebugLog("Execute loop" + "\t" + bars.DateTimes[idx].ToShortDateString()                   + "\t" + buys[a].Symbol + "\t" + Convert.ToString(buys[a].UserData));             bool inBuyList = buys.Contains(bars);             if (!HasOpenPosition(bars, PositionType.Long))             {                //buy logic - buy if it's in the buys list                if (inBuyList)                   PlaceTrade(bars, TransactionType.Buy, OrderType.Market);             }             else             {                //sell logic, sell if it's not in the buys list                if (!inBuyList)                   PlaceTrade(bars, TransactionType.Sell, OrderType.Market);             }          }       }       //declare private variables below       private RSI rsi; private HV _hv; private ROC _roc;           } }


Settings used: Dow 30/ Daily/ 3yrs/ 3 positions @33% equity ea



I have a number of Qs:
1) Most of the trades don't make any sense as they clearly failed the filters yet got traded. Using the values seen in debug window as of last month:

Of the 3 currently Open Positions (JPM, CAT, NKE) 2 of them failed one of the filters (roc > 0) as of 3/31/21, if not previously, yet are open!
2) The values for the UserData (rsi[idx]) retrieved from the buys list inside the Execute section have NO relation to the value input inside the PreExecute section. Just for example, JPM's rsi value on 3/31/21 was 52.32 but as retrieved inside the Execute loop it is 46.03...and these values extend back months in some cases which is virtually impossible!
3) It usually takes 2 runs for the Backtest to stabilize, is that expected behavior?
4) Should I be adding bars.Cache.Clear(); somewhere? And if, so, where?

Kindly advise what mistake(s) I have done.
1
384
9 Replies

Reply

Bookmark

Sort
- ago
#1
I just have one question. If the if-test below fails, do you simply leave UserData undefined?
CODE:
if (rsi[idx] < 60 && _hv[idx] < 30 && _roc[idx] > 0) { double rsiVal = rsi[idx]; bh.UserData = rsiVal; //save the current RSI value along with the BarHistory instance }
Isn't leaving an undefined UserData result for the Sort operation dangerous? If I were you, I would add an else statement in there, which sets UserData to zero or some low defined value. I'm not sure what Sort will do with undefined data.

My other casual comment is that I would create a different criteria for the exit condition. A separate "inSellList" if you will.

A third option would be to add some hysteresis to the buy verses sell test. For example, buy the position if it falls in the top 5% of the merit metric (RSI). And sell the position if it falls in the bottom 80% of the merit metric. To do that, you'll need to pass the merit metric into the Execute{} block. That's easy to do by simply replacing the UserData.RSI with the rank sort index immediately after sorting. Now you have UserData.RankSortIndex instead. And you can test that RankSortIndex in the Execute{} block to get its rank for testing buy verses sell, and you'll have your hysteresis in the test metric (which will prevent "bouncing" between these two conditions/states).

My point is to be creative. There's more than one way to skin a cat. Happy computing.
1
Glitch8
 ( 10.94% )
- ago
#2
This is a large chunk of code that will take some time to look through, but I quickly wanted to add that the Backtester clears all BarHistory Cache prior to execution so you needn't worry about doing that yourself.
0
- ago
#3
Make sure that you dont' store any Double.IsNaN values to UserData, because if you sort the list using "participants.Sort((a, b) => [...]", you'll get them at the top of your List.

Had the same case about two weeks ago. Best way to recognize this is to directly attach the Visual Studio Debugger.
1
- ago
#4
@Glitch,
Thanks for looking, I'll wait.

@superticker,
Good catch! I put in an else where it was missing and put in an arbitrary value [rsiVal = 1000;] there. While that took care of the extremely prolonged holding period of some of the Positions, in some cases the arbitrary value of 1000 actually made through to the sort (when there were not enough candidates who passed the test) lol; clearly, it needs further work.
I'll work on the Sell side once I'm confident the Buy side is robust & stable.
As for the RankSortIndex, I'd considered that many years ago but then dropped it as a robust rule because it is strongly dependent on when the backtesting period starts e.g. some symbol may fortuitously make it to the top 5% on that start date, and never again, and you'd enter a Position; if your test had started a little sooner or later then that symbol would never be there and there would be no Position.
0
- ago
#5
@Springroll,
I'd tried the code snippet you'd posted in another thread:
CODE:
if (Double.IsNaN(val))             {                val = 1000;             }

but it caused some unexpected results so I dropped it (for now, at least).
BTW, I don't have VS.
0
- ago
#6
@Sammy_G, you're missing quite some fun by not having Visual Studio Code or Visual Studio Community. Both are free. Just imagine putting the script execution on pause and reviewing the state of each variable in real-time... and then editing this code on-the-fly if a bug is found. Even adding new blocks of code without having to restart WL7.
1
- ago
#8
I believe this is the weak section of the code (its been updated from the initial post):
CODE:
if (IsLastTDOM) { if (rsi[idx] < 60 && _hv[idx] < 30 && _roc[idx] > 0) { double rsiVal = rsi[idx]; bh.UserData = rsiVal; //save the current RSI value along with the BarHistory instance } else { double rsiVal = 1000; //assign an arbitrary value bh.UserData = rsiVal; }


I want those symbols that fail the filters to either:
- not even make it to the participants list
or
- be assigned a value that would be meaningless during the Sort (so they get dropped that way).

I've hit a mental block, help needed.
0
- ago
#9
I'll repeat what I said in Reply# 1. I think you're going to have "bouncing" unless you add some hysteresis to the buy verses sell test.

"Bouncing" is an engineering term we use with bang-bang control systems. Imagine if your house thermostat didn't have hysteresis. Then when it hits its 75 degree set point, the furnace would bounce on and off at that exact temperature. By adding about 2 degrees of hysteresis, so it goes off at 76 degrees and turns on again at 74 degrees, you prevent that.

Alternatively, you could avoid the bouncing by pre-conditioning the RSI with a "slow" filter. In this context, that would be an IIR filter [EMA-type indicator with multiple poles such as TEMA: sell signal = TEMA(RSI); whereas,... buy signal = RSI. You "might" be able to use the filtered signal for both buy and sell.] with a long period. Some cheaper thermostats use that approach, but then the room doesn't follow the temperature setting as well; it's slow to respond.

Gee, you have a computer, so simply program in "any kind" of hysteresis. You can do that lots of ways if you didn't like my original suggestion in Reply# 1 (i.e. buy above 95% rank and sell below 80% rank; i.e. 15% hysteresis). Be creative.
1

Reply

Bookmark

Sort