- ago
I want to build a strategy that scales into a position in multiple tranches. The first entry is triggered by an indicator signal (e.g., RSI(5) < 40). After that, additional entries should be based on the fill price of the first order or last order —for example, enter a second tranche if the price drops 5% below the first entry, and a third tranche if it drops another 5% below the second entry. Subsequent entries should be able to fill on the SAME day of the initial fill if price target hits.

How can I implement this kind of logic? Thanks.
0
530
Solved
18 Replies

Reply

Bookmark

Sort
- ago
#1
Not a lot of action?

I’m not great with c#… but maybe you could declare a variable and add to it while checking for drops in RSI below your threshold? +1 for every Y drop below X, and open another position, kind of thing?

And then you could create some robustness by checking how many positions you had open each time, before adding +1 and opening the new tranche? Count how many items are in getpositionsallsymbols() (should be equal to your +1 variable before the new tranche)? And then inverse on the way up if you’re selling on the way back (or just use limits)?

I’d imagine Claude or ChatGPT could get you started.
0
- ago
#2
QUOTE:
enter a second tranche if the price drops 5% below the first entry, and a third tranche if it drops another 5% below the second entry.

Perhaps I'm reading this wrong, but you're wanting to repeatably enter into a Long trade when the stock is free falling downward?

QUOTE:
Subsequent entries should be able to fill on the SAME day

That's not a problem if the next trade will be on a subsequent intraday bar.

This strategy is complicated enough that it will need to be done in C# code. If you're a coder, I would post what you have done so far perhaps with the buying of the first position. Include the exit code in your post. Perhaps the forum can add to your initial solution.

If you're not a coder, then I would get a local partner that is to code this for you, and you could learn some C# coding in the process.

This may not take that much coding, but it's probably too complicated for a new programmer to code.

It's easy to sample the "simulated" entry price of the most recent Position.
CODE:
double price = LastPosition.EntryPrice;
0
- ago
#3
I think the main question I have is that how can I place the scale in orders for the same day after the initial order is filled. The following code only scale on the second day after the first fill. Another issue with the this code is that if on the next day there's a big drop both scale in orders will be filled at the same prices but what i intended to do is that the third order limit price is 5% below the second order limit price.

CODE:
   public override void Execute(BarHistory bars, int idx)       {          Position currentPosition = FindOpenPosition(0);          if (currentPosition == null)          {             if (rsi[idx] < RSI_OVERSOLD)             {                PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 0, "Buy - RSI oversold");             }          }          else          {             int openPos = OpenPositions.Count;             if (openPos == 1) // scall in orders             {                initialFillPrice = LastOpenPosition.EntryPrice;                double entryPrice =initialFillPrice * 0.95;                PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, entryPrice, 0,                       "First scale in");                PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, entryPrice * 0.95, 0,                       "Second scale in");             } else if (openPos == 2) {                PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, LastOpenPosition.EntryPrice * 0.95, 0,                       "Second scale in");             }                          if (rsi[idx] > RSI_OVERBOUGHT)             {                foreach (Position pos in OpenPositions)                {                   ClosePosition(pos, OrderType.Market, 0, "Sell all - RSI overbought");                }             }          }       }
0
- ago
#4
If a condition is mutually exclusive, then use a switch() statement as shown.

CODE:
      public override void Execute(BarHistory bars, int idx)       {          if (HasOpenPosition(bars, PositionType.Long) && rsi[idx]>RSI_OVERBOUGHT)          {             foreach (Position pos in OpenPositions)                ClosePosition(pos, OrderType.Market, 0, "Sell all - RSI overbought");          }          else          {             switch (OpenPositions.Count)             {                case 0: //no positions owned                   {                      if (rsi[idx] < RSI_OVERSOLD)                         PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 0, "Buy - RSI oversold");                      break;                   }                case 1: //one existing position                   {                      double entryPrice = LastOpenPosition.EntryPrice * 0.97;                      PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, entryPrice, 0,                         "First scale in");                      break;                   }                case 2: //two existing positions                   {                      double entryPrice = LastOpenPosition.EntryPrice * 0.97;                      PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, entryPrice, 0,                         "Second scale in");                      break;                   }                default: //three or more existing positions                      break;             }          }       }
Now explain again why this code isn't working for you. You need to use intrabars such as 15-minute bars. Daily bars won't work here.

A MarketClose order isn't going to work for "Case 0" because it occurs at the end of the market day and you want the other orders to occur (on subsequent bars) on the same day as the first order; therefore, "Case 0" must occur earlier in the day (as a Market order) so there's time (and bars) for the forthcoming orders on that same day.
0
Glitch8
 ( 8.86% )
- ago
#5
I assume the first entry is a market order? If so then you can get the entry price using the NextSessionOpen indicator or looking ahead one bar, and place follow up orders certain percentages below that price as limit orders. you just place all the orders in one code block. I’ll whip up a quick example.
0
Glitch8
 ( 8.86% )
- ago
#6
Here's a minimal example using 3 positions, and a chart showing an instance of where it bought three times in one day.

CODE:
using WealthLab.Backtest; using System; using WealthLab.Core; using WealthLab.Data; using WealthLab.Indicators; using System.Collections.Generic; using WealthLab.Fundamental; 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) {          rsi5 = RSI.Series(bars.Close, 5);          PlotIndicator(rsi5);          StartIndex = 5; } //execute the strategy rules here, this is executed once for each bar in the backtest history public override void Execute(BarHistory bars, int idx) {          //we're looking ahead one bar so cannot process the final bar          if (idx == bars.Count - 1)             return;                       bool buySetup = rsi5[idx] < 40;          bool sellSetup = rsi5[idx] > 60;          int posCount = OpenPositions.Count;          if (posCount == 0 && buySetup)          {             PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0);             double entryPrice = bars.Open[idx + 1];             double nextEntry = entryPrice * 0.95;             PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, nextEntry);             nextEntry = nextEntry * 0.95;             PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, nextEntry);          }          else if (posCount < 3 && buySetup && !sellSetup)          {             double nextEntry = LastOpenPosition.EntryPrice * 0.95;             if (bars.Open[idx + 1] < nextEntry)                nextEntry = bars.Open[idx + 1];             for (int n = posCount; n < 3; n++)             {                nextEntry = nextEntry * 0.95;                PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, nextEntry);             }          }          if (posCount > 0 && sellSetup)          {             Transaction t = PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0);             t.Quantity = OpenQuantity;          } }       //declare private variables below       private RSI rsi5; } }


1
Best Answer
- ago
#7
Can I assume the code below sells all open positions without looping through them individually?
CODE:
if (posCount > 0 && sellSetup) { Transaction t = PlaceTrade(bars, TransactionType.Sell, OrderType.Market, 0); t.Quantity = OpenQuantity; }
0
Glitch8
 ( 8.86% )
- ago
#8
Yes, it assigns the Quantity of the Transaction to the total quantity of open positions. The WL8 backtester will split that quantity behind the scenes and apply it to all the open positions.
1
Cone8
 ( 22.26% )
- ago
#9
@Glitch - that's a pretty convenient way to exit multiple positions, but there's a problem with consistency when multiple order types are used (which I found when trying to apply it) - even when there's only 1 Position.

To see what I mean, run this example on TQQQ. Without assigning the OpenQuantity to the Transaction (as is), you'll see the order always exit at Limit, as it should.

Then change false/true for the exit to assign the OpenQuantity and run it several times until you see it switch to use the MarketClose order and vice-versa.

CODE:
using System; using WealthLab.Backtest; using WealthLab.Core; namespace WealthScript11 { public class MyStrategy : UserStrategyBase {        public override void Initialize(BarHistory bars) {          nIn = bars.IndexOf(new(2025, 9, 4)); } public override void Execute(BarHistory bars, int idx) {          if (idx == nIn)          {             PlaceTrade(bars, TransactionType.Buy, OrderType.Market);          }                    if (idx == nIn + 4) {             if (false)      // change to true and run several times until you see the exit switch from LMT to MOC to LMT             {                t = PlaceTrade(bars, TransactionType.Sell, OrderType.Limit, 95.25, "LMT");                t.Quantity = OpenQuantity;                t = PlaceTrade(bars, TransactionType.Sell, OrderType.MarketClose, 0, "MOC");                t.Quantity = OpenQuantity;             }             else             {                t = PlaceTrade(bars, TransactionType.Sell, OrderType.Limit, 95.25, "LMT");                t = PlaceTrade(bars, TransactionType.Sell, OrderType.MarketClose, 0, "MOC");                            }             } }       Transaction t;       int nIn; } }


0
- ago
#10
QUOTE:
... change false/true for the exit to assign the OpenQuantity and run it several times until you see it switch to use the MarketClose order and vice-versa.

It's the "vice-versa" part that caught my eye. This behavior is expected because Transaction.Quantity is a double and OpenQuantity is an int. You always expect some random rounding error when converting between types.

I suppose the tough question is, "Should OpenQuantity be made a double as well to avoid this rounding error between OpenQuantity and Transaction.Quantity?"

We face this problem in numerical analysis all the time. Typically we try to avoid converting between numerical types to get around this. What matters is that the rounding error "averages out" over iterations. That's why the Round() method has special rules. Do you remember the Astronomer's Rule from numerical analysis? ... round so the last digit is even.
0
Cone8
 ( 22.26% )
- ago
#11
1. OpenQuantity is a double type, as is the Transaction.Quantity.

2. This behavior is not expected (hence my post) and has nothing to do with trade quantity rounding. Furthermore, my bare-bones example is only using only 1 trade (entry), not multiple positions.

The problem is:
When the t.Quantity is assigned, the Limit exit order (which should be filled before the close) is "missed" in favor of the MarketClose order - randomly! There's no scenario in which that's correct. It's a Backtester bug.
0
- ago
#12
QUOTE:
1. OpenQuantity is a double type, as is the Transaction.Quantity.

You're right. IntelliSense documents it as a double.

However, the QuickRef and the website documents OpenQuantity as an int instead. Someone needs to fix that. (Next time I'll check IntelliSense first.)
0
Glitch8
 ( 8.86% )
- ago
#13
Hmmm I changed the false to true and ran it 30 times on SPY and it never once issued the MOC exit. Is it somehow symbol or preference-specific?
0
Cone8
 ( 22.26% )
- ago
#14
Yes. The example specifically uses a limit price for a specific day for TQQQ that would fill after the open. It will always fill on the open for SPY if you don't change the limit price.
0
Glitch8
 ( 8.86% )
- ago
#15
Gotcha, it's fixed for Build 136.
2
- ago
#16
Thanks Glitch for the example code.

One related question: is there a way to have the third entry limit price based on the fill price of the second/latest entry? In current logic entries are all based on different percentages of the initial fill. Thanks.
0
Glitch8
 ( 8.86% )
- ago
#17
It already does that, the only possible prices for the second fill are the limit price, or the market open if the market opens below that price. Either way the code handles that and bases the price of the third tranche on the fill price of the second.
1
- ago
#18
CODE:
else if (posCount < 3 && buySetup && !sellSetup)
I think since the "true" state of buySetup and sellSetup are mutually exclusive by design, one can just simplify the above statement to ...
CODE:
else if (posCount < 3 && buySetup)
I'm gathering that the multiple transactions produced by the first block of PlaceTrade statements are only active for one bar; therefore, one needs the second block of PlaceTrade statements to reinstate them. I didn't realize transactions have a time-of-life of just one bar whether they are filled or not. (A secret undocumented feature, which is interesting.)
0

Reply

Bookmark

Sort