- ago
I have a dip buying strategy (similar to knife juggler) that runs against 3000 symbols. I currently use an intraday strategy for backtesting that utilize 5 minute bars and buys the first 30 trades (by time of day) from that 3000 list that trigger. I have live traded this strategy now for 2 months and the backtesting from the intraday strategy match the live trades taken by about 95% (some are missed due to not being filled, or multiple trades occurred at the last 5 minute window to reach 30 that differed from the live trade, or dipped too fast for the trigger to hit and then submit the order).

When I run the same dip buying strategy on daily bars and utilize the new 5 minute granular limit/stop processing option I get wildly different results (about 50% match the live trades actually taken by time of day). I understand some trades will differ if multiple trades occur during that 5 minute window but the results seem too far off from live trading. Am I misinterpreting the granular processing method?

Thank you,
Eric

1
1,107
11 Replies

Reply

Bookmark

Sort
- ago
#1
I should add that I'm using a fixed equity % for all trades (3.33% for 30 positions).
0
Glitch8
 ( 10.94% )
- ago
#2
Running the same strategy on intraday vs daily will often produce wildly different results. Unless you had some special code in your intraday strategy was correctly coded to process the same way as it would running on daily data.
0
- ago
#3
The intraday code is purely for backtesting only. It brings in limit orders from the daily strategy and processes them by time of day (exactly what granular processing says it does). The intraday code is matching live trades using the limit orders developed by the daily strat, the granular processor is not.
0
Glitch8
 ( 10.94% )
- ago
#4
I’m not sure what to say without seeing any code. I mean, we validated the granular processing and verified it was working correctly. Are you sure you have enough intraday data to cover the 3000 symbols? Also make sure the desired intraday provider is checked in the Data Manager and moved to the top position.
0
Glitch8
 ( 10.94% )
- ago
#6
We did receive your code, but it relies on external files and I am not able to validate your intraday system and explain why the results are different than running on the daily scale. Rather, I'm exposing a few new properties in Build 8 to illustrate how the granular processing is correctly operating. Here is a modified Knife Juggler that is run on ...

- a daily data set of symbols AAPL BBBY IBM MSFT SNAP
- six months data range backtest
- 100% of equity with margin of 1.1
- Granular processing set to 10 minutes (TD Ameritrade selected in Data Manager as an intraday historical source)

The code is set to execute on 4/28/2021, where 4 of the stocks in the DataSet will hit the limit order. Normally, WL7 would determine which one to fill randomly (or based on Transaction Weight if it was set.) But with Granular Processing enabled, it looks at the 10 minute intraday data to see which of the stock hit the limit first. I am writing the relevant info to the Debug window. Bottom line, MSFT hit the limit first at 10:10 AM. SNAP also hit at 10:10 so it was a toss up, and the backtester randomly chose MSFT. The other stocks hit at 10:20 and 12:00 so the Granular Processing correctly excluded them.

Note that the script uses some properties that are being made available in Build 8. This is just to show the inner workings of the feature and that it's operating correctly.

CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Indicators; using System.Drawing; using System.Collections.Generic; namespace WealthScript1 { public class MyStrategy : UserStrategyBase {     public MyStrategy() : base() { } public override void Initialize(BarHistory bars) {       //BacktestSettings.FuturesMode = true;       source = bars.Close;          pct = 0.3;       pct = (100.0 - pct) / 100.0;       multSource = source * pct;          StartIndex = 20; } public override void PreExecute(DateTime dt, List<BarHistory> participants) { } public override void Execute(BarHistory bars, int idx) {          if (bars.DateTimes[idx].Date != new DateTime(2021, 4, 28))             return;          int index = idx;          Position foundPosition0 = FindOpenPosition(0);          bool condition0;          if (foundPosition0 == null)          {             condition0 = false;             {                condition0 = true;             }             if (condition0)             {                val = multSource[idx];                _transaction = PlaceTrade(bars, TransactionType.Buy, OrderType.LimitMove, val, 0);             }          }          else          {             condition0 = false;             {                condition0 = true;             }             if (condition0)             {                if (idx - foundPosition0.EntryBar + 1 >= 2)                {                   ClosePosition(foundPosition0, OrderType.Market);                }             }             condition0 = false;             {                condition0 = true;             }             if (condition0)             {                value = (5.00 / 100.0) + 1.0;                ClosePosition(foundPosition0, OrderType.Limit, foundPosition0.EntryPrice * value);             }          } }       public override void PostExecute(DateTime dt, List<BarHistory> participants)       {       } public override void BacktestComplete() {          DateTime dt = DateTime.MinValue;          foreach (Transaction t in Backtester.TransactionLog)          {             if (dt != t.EntryDate.Date)             {                WriteToDebugLog("============");                WriteToDebugLog(t.EntryDate.Date.ToShortDateString());                dt = t.EntryDate.Date;             }             if (t.TransactionType != TransactionType.Buy)                continue;             string s = "Buy ";             if (t.NSF)                s += "(NSF) ";             if (!Double.IsNaN(t.ExecutionPrice))                s += "(Executed) ";             s += t.Quantity + " " + t.Symbol + " at " + t.OrderPrice;             if (t.GranularWeightBasis != DateTime.MinValue)                s += " - Weight=" + t.Weight + " Trigger: " + t.GranularWeightBasis.ToShortDateTimeString();             WriteToDebugLog(s);          } } private double pct;       private double val;       private TimeSeries source;       private TimeSeries multSource;       private double value;       private Transaction _transaction; } }


Debug Log:
CODE:
4/28/2021 Buy 701 IBM at 142.571 Buy (NSF) (Executed) 750 AAPL at 133.17926 - Weight=-1200 Trigger: 4/29/2021 12:00 PM Buy (Executed) 394 MSFT at 253.79632 - Weight=-1010 Trigger: 4/29/2021 10:10 AM Buy (NSF) (Executed) 3826 BBBY at 26.13137 - Weight=-1020 Trigger: 4/29/2021 10:20 AM Buy (NSF) (Executed) 1616 SNAP at 61.85388091278076 - Weight=-1010 Trigger: 4/29/2021 10:10 AM


0
- ago
#7
QUOTE:
MSFT hit the limit first at 10:10 AM. SNAP also hit at 10:10 so it was a toss up, and the backtester randomly chose MSFT

Dion, when there is a tie on the intraday/granular time level, if the strategy assigns transaction weights will/can those be used as the tie-breaker to determine which trade to take (rather than choosing randomly)? This would ensure consistent backtest results.
0
Glitch8
 ( 10.94% )
- ago
#8
You could submit a Feature Request in a new topic so we can have this request properly recorded and available for voting.
0
- ago
#9
Ok, submitted feature request in new topic - "Use Transaction Weight as Intraday Tie-Breaker for Limit orders executed on Daily or higher scale".
0
Cone8
 ( 24.99% )
- ago
#10
Make sure you vote for your own feature request!
0
- ago
#11
Thanks. Done.
0

Reply

Bookmark

Sort