- ago
For example, let's take the simplest limit buy strategies that are several percent below the closing price on the daily chart. When we scan the system on a large list of stocks, we get 20-100-500 limit buy orders. In real trading, everything is easy - we can sort orders by priority and place, for example, 10 limit orders. Or, if there are already 6 open positions available, place 4 limit orders. But for testing on history, for a non-programmer, it is difficult to write such a script.
Therefore, it would be nice to have such a "Position Size", for example, in the "Advanced Pos Sizer" section.
0
807
16 Replies

Reply

Bookmark

Sort
- ago
#1
Could you clarify what is difficult and what position sizing rule is on your mind?
0
- ago
#2
Sort limit orders by priority and place only the first 10 (if there are no positions). Or less if there are already open positions.

To make it clearer, here is my pseudocode as I represent it :)

CODE:
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) {          _ma = new SMA(bars.Close, 200);          _rsi = new RSI(bars.Close, 2);          _roc = new ROC(bars.Close, 4); } //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 (!HasOpenPosition(bars, PositionType.Long)) { if(bars.Close[idx] > _ma[idx])          if(_rsi[idx] < 20)             {              1. Add tickers to the "LIST<string>" that meet the condition. 2. Add "_roc" value (priority) to the "LIST<string>" 3. Calculate how many positions can be opened for free capital (maximum number of positions - 10) = N 4. Sort "LIST<string>" by "_roc" value and leave the first N tickers             } If stocks are on the "LIST<string>" PlaceTrade(bars, TransactionType.Buy, OrderType.Limit, bars.Close[idx] * 0.98, 0);                    } else { //code your sell conditions here } }
0
- ago
#3
QUOTE:
Sort limit orders by priority:

Priority is Transaction.Weight now. Note that starting from Build 7, there's a new feature in Strategy Settings > Backtest data > Advanced Settings called "Use Granular stop/limit processing". It requires intraday data which is used by WL to look into and determine which orders would've been executed in real life.

QUOTE:
place only the first 10 (if there are no positions).

You could specify "Max Open Pos" in Strategy Settings > Position Sizing Settings.

Would that get the job done?
0
- ago
#4
The point is not to place 500 limit orders and wait for the first 10 to be filled, but simply place 10 limit orders sorted by weight. If some do not come true, then it is not necessary.

Probably, my English is not very clear, because I write through Google translate. To make it clearer, here is an excerpt from a description of one of the strategies:



0
- ago
#5
QUOTE:
The point is not to place 500 limit orders and wait for the first 10 to be filled, but simply place 10 limit orders sorted by weight.

If my understanding of your idea is correct, you could do the priority filtering in PreExecute and later in Execute only take the 10 trades with the most desirable weight. Please check out the code sample for PreExecute:

https://www.wealth-lab.com/Support/ApiReference/UserStrategyBase
0
Glitch8
 ( 10.94% )
- ago
#6
If you take this approach and only submit the 10 orders, realize that they might not fill, leaving you with very few trades in your backtest. That is why it’s often desirable to submit all of the sorted signals.
1
- ago
#7
@Eugene

Thank you. I'll try if I can.

@Glitch

Yes, I understand the consequences. Moreover, this approach is time-tested for me. I just would like to test this approach on history and I think that it would be nice to have such a template in "position sizer".
0
Cone8
 ( 24.99% )
- ago
#8
QUOTE:
have such a template in "position sizer".
Although, it's a sort of combination of Max Open Positions and Max Entries per Bar, it's unique enough to be controlled by the strategy rules. The general case is what Glitch explained.

It seems easy enough to implement. You just have to do the following in PreExecute:

1. perform the ranking
2. determine the number of open Positions with OpenPositionsAllSymbols
3. subtract the number of Market Exits on the next bar (optional)
4. and subtract the result from the number of Max Positions to arrive at the number of signals.
5. store those symbols (or BarHistory) of the highest ranked signals in a static List to enable their signals on the next bar. In other words, you check the List in Execute to determine if you place a signal for that BarHistory.
1
- ago
#9
@Cone
Thank you. I will try to improve my programming skills :)
0
- ago
#10
@Cone

Only, in my opinion, an important part is missing here.

0. We have S&P-500, five hundred tickers. We scan the conditions of the system, for example, Close > SMA(200), RSI(2) < 20, etc. At the output, we get 60 candidates for opening a position.

And only then they should be sorted by weight, to select only 10 of them (or less):

QUOTE:

1. perform the ranking
2. determine the number of open Positions with OpenPositionsAllSymbols
3. subtract the number of Market Exits on the next bar (optional)
4. and subtract the result from the number of Max Positions to arrive at the number of signals.
5. store those symbols (or BarHistory) of the highest ranked signals in a static List to enable their signals on the next bar. In other words, you check the List in Execute to determine if you place a signal for that BarHistory.
0
- ago
#11
I can't imagine how, before the PreExecute, where tickers will be sorted by weight, create a list of tickers that have passed through the filters like:

if (Close[idx] > SMA[idx])
if (RSI[idx] < 30)

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)       {          _roc = new ROC(bars.Close, 4);          bars.Cache["ROC"] = _roc;          _rsi = new RSI(bars.Close, 2);          _sma = new SMA(bars.Close, 200);                StartIndex = 200;       }           //      if (_rsi[idx] < 30)    //         if (bars.Close[idx] > _sma[idx])    //            buys.Add();    //      WriteToDebugLog( bars.DateTimes[idx].ToShortDateString() + bars.Symbol);              //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' ROC 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             ROC _roc = (ROC)bh.Cache["ROC"];                {                   double _rocVal = _roc[idx];                   bh.UserData = _rocVal; //save the current ROC value along with the BarHistory instance                }          }          //sort the participants by ROC value (lowest to highest)          participants.Sort((a, b) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));          //keep the top 10 symbols          buys.Clear();          for (int n = 0; n < 10; 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.Limit, bars.Close[idx] * 0.98).Weight = -_roc[idx];          }          else          {             //sell logic, sell if it's not in the buys list          //   if (!inBuyList)             if (_rsi[idx] > 60)                PlaceTrade(bars, TransactionType.Sell, OrderType.Market);          }       }       //declare private variables below       private ROC _roc;       private RSI _rsi;       private SMA _sma;    } }
0
Cone8
 ( 24.99% )
- ago
#12
Hey, that's a good start. But give this a try. It took me a while to wrap my head around this, and I left the debug code in, which is mostly self-explanatory. I left a time-based exit in there, but you can change or add your RSI exit.

I assumed you didn't want to place orders for symbols that were already open, so symbols that appear like [ADCD] were high in the ranking but skipped since they were already open.

I hope this helps!

CODE:
using WealthLab.Backtest; using System; using System.Text; using WealthLab.Core; using WealthLab.Indicators; using System.Drawing; using System.Collections.Generic; using System.Linq; namespace WealthScript123 {    public class ComplexMaxPositionLimit : UserStrategyBase    {             const int _maxPositions = 10;       ROC _roc;       SMA _sma;       RSI _rsi;       static List<BarHistory> _filteredcandidates = new List<BarHistory>();       static List<string> _openSymbols = new List<string>();              public override void Initialize(BarHistory bars)       {          StartIndex = 200;                    _roc = ROC.Series(bars.Close, 1);          _sma = SMA.Series(bars.Close, 200);          _rsi = RSI.Series(bars.Close, 2);          bars.Cache["roc"] = _roc;          bars.Cache["sma"] = _sma;          bars.Cache["rsi"] = _rsi;       }              public override void PreExecute(DateTime dt, List<BarHistory> participants)       {          _filteredcandidates.Clear();          foreach (BarHistory bh in participants)          {             int idx = GetCurrentIndex(bh);             TimeSeries sma = (TimeSeries)bh.Cache["sma"];             TimeSeries rsi = (TimeSeries)bh.Cache["rsi"];             if (bh.Close[idx] > sma[idx] && rsi[idx] < 20)                _filteredcandidates.Add(bh);          }                              //rank the candidates by roc          foreach (BarHistory bh in _filteredcandidates)          {             TimeSeries roc = (TimeSeries)bh.Cache["roc"];             int idx = GetCurrentIndex(bh);             bh.UserData = roc[idx];          }          //sort the candidates by value (lowest to highest)          _filteredcandidates.Sort((a, b) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));                    // or highest to lowest...          // _filteredcandidates.Sort((b, a) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));          //process Exits now so we can add more slots for the market Exits          int openPositions = 0;          int marketExits = 0;                    StringBuilder sb = new StringBuilder("Open: ");          _openSymbols.Clear();                    foreach (Position p in OpenPositionsAllSymbols)          {             if (p.NSF) continue;             openPositions++;             _openSymbols.Add(p.Symbol);             sb.Append(p.Symbol).Append(", ");                          int idx = GetCurrentIndex(p.Bars);             if (idx + 1 - p.EntryBar >= 10) //sell at market after 10 days             {                PlaceTrade(p.Bars, TransactionType.Sell, OrderType.Market);                marketExits++;             }                          // --- more exit logic here ---          }          WriteToDebugLog("------");                             //slots available          int slots = _maxPositions - openPositions + marketExits;          if (slots > _maxPositions)          {             WriteToDebugLog("************ This shouldn't have happened ************");             slots = _maxPositions;          }                    WriteToDebugLog(sb.ToString());          WriteToDebugLog(string.Format("{0:d}\tCandidates: {1} Open: {2}; Exits: {3}: Slots: {4}",             dt, _filteredcandidates.Count, openPositions, marketExits, slots));          //place limit orders for the top slots          sb.Clear();          int placedOrders = 0;          int n = 0;                    while (placedOrders < slots)          {             if (n >= _filteredcandidates.Count)                break;             BarHistory bh = _filteredcandidates[n];             n++;             //don't place entry order for a symbol already open             if (_openSymbols.Contains(bh.Symbol))             {                sb.Append("[" + bh.Symbol + "]").Append(", ");                continue;             }                          int idx = GetCurrentIndex(bh);             double limit = bh.Close[idx] * 0.98;                         PlaceTrade(bh, TransactionType.Buy, OrderType.Limit, limit);             placedOrders++;             sb.Append(bh.Symbol).Append(", ");          }                         WriteToDebugLog(dt.ToString("yyyy-MM-dd") + " Limits: " + sb.ToString());       }                     public override void Execute(BarHistory bars, int idx)       {          //not required!       }    } }
1
- ago
#13
@Cone

Thank you, this is top class, of course! I would never program that myself.
I tried it, everything works.

I just tried to create the same system of blocks to see the weight of the signals for today and compare which signals the system chose. It turns out that not with the lowest or highest weight, but at first glance, somehow selectively. But I'm not sure yet, maybe I did something wrong, I'll figure it out.
0
- ago
#14
On the right is the System from the discussion. On the left is a system with the same conditions from blocks. It can be seen that candidates for opening a position are not selected with extreme weights.

0
Cone8
 ( 24.99% )
- ago
#15
What I provided was just an example.
1. I don't think I have all your specific requirements. I used ROC(1) - I thought I had read something about the close to close change. And, as I mentioned, the exit logic isn't correct.

2. Note this section in the code. If you want to favor large ROC, then comment/uncomment the right one.

CODE:
         //sort the candidates by value (lowest to highest)          _filteredcandidates.Sort((a, b) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));                    // or highest to lowest...          // _filteredcandidates.Sort((b, a) => a.UserDataAsDouble.CompareTo(b.UserDataAsDouble));


3. I didn't assign weights - it's not required. The strategy only Places the trades necessary and always tries to fill the slots available.
1
- ago
#16
@Cone
I understood you. Thanks again :)
0

Reply

Bookmark

Sort