- ago
Hello,

I'm trying to port some strategies from a long time discontinued backtester to Wealth-Lab. Currently, I'm mapping terminology/functionlity from there to here.

There's a class of strategies that rebalance portfolio to target weights at certain periodicity. Think 60/40 stock/bond portfolio as a classic example. In this case, you would only have two positions during the whole time. But each position would have multiple transactions. You buy 100 shares in SPY and 50 shaes in IEF, then in a year you sell 20 shares in SPY and buy 5 shares in IEF, then next year you may buy 10 shares of SPY and sell 3 shares of IEF, and so on. A position is not considered closed until the number of shares for an asset in the porfolio is 0.

Here's a dummy strategy with rebalancing logic to mimic the above: allocate 100% of equity to SPY on even months, and only 50% of equity on odd ones.
Tested on SPY only, with Specific year set to 2024.

After reading through discussion here

https://www.wealth-lab.com/Discussion/Scale-into-long-and-short-with-a-different-size-12772

I came up with this draft:

CODE:
using WealthLab.Backtest; using WealthLab.Core; namespace WLTinkering; public class CalibratePosition : UserStrategyBase { private int _monthRebalanced = 0; private Transaction? _txn; public override void Execute(BarHistory bars, int idx) { var date = bars.DateTimes[idx]; var currentMonth = bars.DateTimes[idx].Month; // be 100% invested on even months // and 50% invested on odd months var qnt = currentMonth % 2 == 0 ? (int) (CurrentEquity / bars.Close[idx]) : (int) (CurrentEquity / 2 / bars.Close[idx]); if (_monthRebalanced != currentMonth) { _monthRebalanced = currentMonth; if (HasOpenPosition(bars, PositionType.Long)) { if (_txn != null) { _txn.Quantity = qnt; WriteToDebugLog($"Rebalancing, {date.ToShortDateString()}, Qnt: {qnt}"); } } else { _txn = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, "Initiating position"); _txn.Quantity = qnt; WriteToDebugLog($"First trade, {date.ToShortDateString()}, Qnt: {qnt}"); } } } }


Saving a reference to `Transaction _txn` on initial buy, and then modifying its quantity on subsequent months doesn't work.


Then I tried keeping a reference to each rebalancing event and setting quantity size on those like this:

CODE:
using WealthLab.Backtest; using WealthLab.Core; namespace WLTinkering; public class CalibratePosition : UserStrategyBase { private int _monthRebalanced = 0; private Transaction? _txn; public override void Execute(BarHistory bars, int idx) { var date = bars.DateTimes[idx]; var currentMonth = bars.DateTimes[idx].Month; var qnt = currentMonth % 2 == 0 ? (int) (CurrentEquity / bars.Close[idx]) : (int) (CurrentEquity / 2 / bars.Close[idx]); if (_monthRebalanced != currentMonth) { _monthRebalanced = currentMonth; if (HasOpenPosition(bars, PositionType.Long)) { var position = OpenPositions[0]; if (position.Quantity > qnt) { var sellTxn = PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0, "Scale out"); sellTxn.Quantity = position.Quantity - qnt; } else { var buyTxn = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, "Scale in"); buyTxn.Quantity = qnt - position.Quantity; } } else { _txn = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, "Initiating position"); _txn.Quantity = qnt; WriteToDebugLog($"First trade, {date.ToShortDateString()}, Qnt: {qnt}"); } } } }


It doesn't work as expected either.

The only solution that conforms to strategy rebalancing logic is this:

CODE:
using WealthLab.Backtest; using WealthLab.Core; namespace WLTinkering; public class CalibratePosition : UserStrategyBase { private int _monthRebalanced = 0; public override void Execute(BarHistory bars, int idx) { var date = bars.DateTimes[idx]; var currentMonth = bars.DateTimes[idx].Month; var qnt = currentMonth % 2 == 0 ? (int) (CurrentEquity / bars.Close[idx]) : (int) (CurrentEquity / 2 / bars.Close[idx]); if (_monthRebalanced != currentMonth) { _monthRebalanced = currentMonth; if (HasOpenPosition(bars, PositionType.Long)) { var position = OpenPositions[0]; ClosePosition(position, OrderType.Market, 0, "Close aka rebalance"); var txn = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, "Open aka rebalance"); txn.Quantity = qnt; WriteToDebugLog($"Rebalance, {date.ToShortDateString()}, Qnt Diff: {position.Quantity - qnt}"); } else { var txn = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, "Initiating position"); txn.Quantity = qnt; WriteToDebugLog($"First trade, {date.ToShortDateString()}, Qnt: {qnt}"); } } } }


Here the allocation to SPY alternates between 100%/50% as was intended.



But this creates multiple positions.



Is there a cleaner way of doing this?
0
160
Solved
6 Replies

Reply

Bookmark

Sort
- ago
#1
The following code doesn't make sense.
CODE:
if (HasOpenPosition(bars, PositionType.Long)) { if (_txn != null) { _txn.Quantity = qnt; WriteToDebugLog($"Rebalancing, {date.ToShortDateString()}, Qnt: {qnt}"); } }
If you want to change the quantity of a position, you need to sell that position and create a new one with PlaceTrade. Alternatively, you can sell part of an existing position or buy an additional position of the same symbol. That works too.

I haven't tried it, but something like this makes more sense.
CODE:
      if (HasOpenPosition(bars, PositionType.Long))       {          Position pos = LastOpenPosition;          double qntDiff = qnt - pos.Quantity;          if (qntDiff > 0.0)          {             Transaction t = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0.0, $"Buy {qntDiff} shares");             t.Quantity = qntDiff;          }          else          {             Transaction t = PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0.0, $"Sell {qntDiff} shares");             t.Quantity = -qntDiff;          }          WriteToDebugLog($"Rebalancing, {date.ToShortDateString()}, Qnt: {qnt}");       }
In addition, the time-of-life of a Transaction variable, such as _txn, must be limited to the Execute{block} because Execute consumes it when placing the trade. So _txn must be declared "local" in Execute. It cannot be declared as a class variable outside of Execute{}.

Finally, the Initialize{block} is not optional. You need to include one where you initialize the data structures (such as your class variable values) that need to be set for each stock WL is iterating over in your dataset. You would initialize any trading indicators there as well.

Is there only one stock in your entire dataset? If this is a paired trading strategy, then you should follow one of the sample paired trading strategies in the Sample Strategies folder.
0
- ago
#2
Your code runs, but it's only following SPY. It's not doing anything useful.

CODE:
using System; using WealthLab.Backtest; using WealthLab.Core; namespace WLTinkering {    public class CalibratePosition : UserStrategyBase    {       int monthRebalanced;              public override void Initialize(BarHistory bars)       {          monthRebalanced = 0;       }       public override void Execute(BarHistory bars, int idx)       {          DateTime date = bars.DateTimes[idx];          int currentMonth = date.Month;          double qnt = currentMonth % 2 == 0 ?             (int)(CurrentEquity / bars.Close[idx]) : //100% invested on even months             (int)(CurrentEquity / bars.Close[idx] / 2.0); //and 50% invested on odd months          if (monthRebalanced != currentMonth)          {             monthRebalanced = currentMonth;             if (HasOpenPosition(bars, PositionType.Long))             {                Position pos = LastOpenPosition;                double qntDiff = qnt - pos.Quantity;                if (qntDiff > 0.0)                {                   Transaction t = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0.0, $"Buy {qntDiff} shares");                   t.Quantity = qntDiff;                }                else                {                   Transaction t = PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0.0, $"Sell {qntDiff} shares");                   t.Quantity = -qntDiff;                }                WriteToDebugLog($"Rebalancing, {date.ToShortDateString()}, Qnt: {qnt}");             }             else             {                Transaction t = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0.0, "Initiating position");                t.Quantity = qnt;                WriteToDebugLog($"First trade, {date.ToShortDateString()}, Qnt: {qnt}");             }          }       }    } }
0
- ago
#3
Thanks for taking the time to address the question.

Regarding the usefulness of my code, yes, it's silly as a trading model. But that's intentional. My current situation involves exploring how to do certain things in WL, and the question was about changing the size of a position during its lifetime. That's why I opted for having just one ticker and not using any indicators, while still having some condition (month number) for executing trades. Fewer moving parts means less time spent on understanding how a certain piece of functionality works.

Just for reference, here are a couple of real-life scenarios for resizing positions:

1. You want to do volatility targeting for an asset - when volatility is high, you reduce your allocation to this instrument (but don't sell it entirely); when it's low, you increase the size. Robert Carver discusses this approach in his "Advanced Futures Trading Strategies" book.
2. You do stock momentum trading and review your portfolio once a week. In addition to reviewing which tickers should be in the portfolio, you may also want to reset weights of existing positions to be equally weighted (1/n). An example of such a fully specified model can be found in "Stocks on the Move" by Andreas Clenow.
3. You have a profitable trading rule indicating an oversold state which is likely to mean revert. You decide to enter in steps: allocate 20% of equity if the price drops 1%, then allocate 30% equity if it drops another 1% from the previous entry, then 50% more if it drops again 1% from the second entry.

Each case shares a similar characteristic - change position size based on some condition, but don't close the position entirely.

Now apparently different frameworks have different terminology. The one I'm switching from has a Position object, which can have many Trades associated with it. A similar approach is used in Quantopian's zipline framework (RIP), and QuantConnect (steep learning curve, slow backtesting).

Here are screenshots from discontinued backtester of the same dummy strategy of allocating 100% to SPY on even months, and 50% on odd ones:





There is only one position for the whole run, and there are 12 trades, one for each month.

I did some experiments in WL. Some observations:

1) The reference to `_txn` persists through the whole backtest. I can see it on each `Execute` call after the position has been opened:

CODE:
private Transaction? _txn; public override void Execute(BarHistory bars, int idx) { // do stuff ... _txn = PlaceTrade(...); _txn.Quantity = qnt; } }


The solution proposed by @superticker is essentially the same as my second draft (`ReferenceEquals(LastOpenPosition, OpenPositions[0]`) returns true, so it's the same object). And the end result of the backtest is not what was expected:





The last version of code in my original post yields pretty much the same result in WL. It just does so by closing the position entirely. It's fine if you keep that in mind - that this is rebalancing. But is there another way of doing it in WL that will treat it as a single position?
0
- ago
#4
QUOTE:
is there another way of doing it in WL that will treat it as a single position?

The short answer is "no". The longer answer is that you can use the PositionTag on PlaceTrade to group and address separate PlaceTrade positions as one. The backtester will address such a grouping collectively. I would read the description about PlaceTrade for more information.

There's also something called a Backtester CancelationCode. Go to https://www.wealth-lab.com/Support/ApiReference/Backtester and search for "CancelationCode" and read about it.

I don't use either feature, so I can't help with this. I would get something working on WealthLab first, then address the more esoteric stuff later.
1
Glitch8
 ( 11.27% )
- ago
#5
This is the bread and butter of the Rebalance method in the framework. Caveat: please upgrade to Build 143 before running this code, I detected and corrected a flaw that recently got introduced to Rebalance.

Here's some sample code that goes in 100% on even months and 50% on odd using Rebalance.

CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using System.Collections.Generic; namespace WealthScript1 { public class MyStrategy : UserStrategyBase { //create indicators and other objects here, this is executed prior to the main trading loop public override void Initialize(BarHistory bars) { } //execute the strategy rules here, this is executed once for each bar in the backtest history public override void Execute(BarHistory bars, int idx) {          //are we at a rebalance month?          DateTime dt = bars.DateTimes[idx];          DateTime nextTradingDay = dt.AddDays(1);          while(!MarketManager.UsaStocks.IsTradingDay(nextTradingDay))             nextTradingDay = nextTradingDay.AddDays(1);          if (dt.Month != nextTradingDay.Month)          {             //go 100% in on even months, 50% on odd months             double exposure = nextTradingDay.Month % 2 == 0 ? 100.0 : 50.0;             Rebalance(bars, exposure);          } } //declare private variables below } }


1
Best Answer
- ago
#6
Thanks Glitch!

I had this facepalm moment yesterday when I discovered Rebalance method in WL documentation and a link to your video with explanation.

It is a very handy feature - just pass your desired percentage allocation and WL figures out all the necessary details.
1

Reply

Bookmark

Sort