Evaluating position size based upon latest signals

howto
Tags: #<Tag:0x00007fb3da594a30>

#1

Hi,

If I have a watch list containing a number of symbols and once a week all open positions are closed and new symbols to buy are established such that the number held each week varies, is it possible to ensure that all available equity each time the next new positions are opened is equally split between the new positions?


#2

I have a similar question – searched the forum and came across this post.

I could see 2 possible ways of doing it:

  1. read the symbols from a watchlist then loop through the list and determine and count the buy signals. Then set position size based on that. May be easier or harder depending on the system and rules.

  2. use the custom backtester inferface to go through the trade list, count the number of trades and set position size accordingly. This seems to be the more general solution. <— If anyone has an example of this it would be appreciated! I think I can figure it out, but if someone has an example that works that would help.


#3

OK, I have tried to code up a simple example to do this, using the custom backtester (based on example code). It attempts to count the number of entry signals, and then sets the position size for each, based on the count.

There is something I am not understanding about this, as my IsEntry() signal count is 2x the max open positions. Maybe someone has an idea why this is the case and what I should do differently.

SetCustomBacktestProc("");

if ( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject();
    bo.PreProcess();

    for ( bar = 0; bar < BarCount; bar++ )
    {
        Cnt = 0;
        for ( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
        {
            if (sig.IsEntry())
				Cnt++;
        }
		
		_TRACEF("%g", Cnt);
		
       for ( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
        {
            if (sig.IsEntry())
				sig.PosSize = -100/Cnt;
        }
        bo.ProcessTradeSignals( bar );
    }
    bo.PostProcess();
}

SetOption("HoldMinBars", 1);
SetOption( "MaxOpenPositions", 10 );

PositionScore = ROC(C,2);

Buy = Month() != Ref(Month(), -1);
Sell = Month() != Ref(Month(), -1); 

#4

A couple of observations:

  1. You don’t need to loop through the signal list to count entry signals. You can simply call GetSignalQty().

  2. Setting Max Open Positions to 10 doesn’t mean that AmiBroker will only send you 10 entry signals. Rather, it will send you all entry signals, up to a max of 2x max positions, just as you observed. This is to give you the opportunity to “pass” on some entry signals in the CBT and still have enough to fill your open positions.

Your solution will work in cases where you want to enter trades for ALL entry signals generated in your Phase 1 AFL. However, if you want to limit your number of trades using some other value (max open positions or anything else), you should use that instead of the signal count.

Matt


#5

ah, yes that makes sense now. I had a feeling it was something like that. Thanks. The way my real code is written, that won’t be a problem because it’s generating buy signals based on using the static ranking functions, not on position score.

On the GetSignalQty(), I’d missed the part where you can have it count only buys or sells.

Seems to be working now. thanks again.


#6

Thanks to both of you for the information.


#7

Here I think is a properly working example. It ranks according to RSI(14), then buys only if higher than the desired rank AND if the rank has improved in the last month. Buy/sell monthly.

The position size is set based on the number of entries . It looks to work, but test for yourself to make sure.

What I am still not sure of is how I would handle this if I don’t buy/sell all positions each month, and then have some positions that remain open. In that case, I would want to set the position size of the new entries to be divided evenly among available capital. I tried to make that work without success.

/* 
	Example of adjusting position size based on number of current signals
	
*/

SetCustomBacktestProc("");

// custom backtest procedure to adjust position size 
if ( Status( "action" ) == actionPortfolio )
{
    bo = GetBacktesterObject();
    bo.PreProcess();

    for ( bar = 0; bar < BarCount; bar++ )
    {
		Cnt = bo.GetSignalQty(bar, 1); // get entries
                        		
		_TRACEF("Bar: %g, Cnt: %g", bar, Cnt);
				
		// set the position size based on number of entries + open positions
		for ( sig = bo.GetFirstSignal( bar ); sig; sig = bo.GetNextSignal( bar ) )
		{
			if (sig.IsEntry())
				sig.PosSize = -100/Cnt;
		}
		bo.ProcessTradeSignals( bar );
	}
    bo.PostProcess();
}	

NumberHeld = Param("# of Positions", 2, 1, 100, 1);
 
// The following block gets all the filter values and does the ranking
// only need to do the calculations once -- so do for the first stock

// read in all symbols
Mainlistnum = GetOption( "FilterIncludeWatchlist" ); 
Mainlist = GetCategorySymbols( categoryWatchlist, Mainlistnum );

NumSymbols = 0;
if (Status("stocknum") == 0) {

	// delete static variables 
	StaticVarRemove("*ValuesToSort*"); 

	for (i = 0; (sym = StrExtract(Mainlist, i)) != ""; i++ ){

		NumSymbols++;
		// make the symbol be the 'current' symbol (so the OHLC arrays can be used directly)
		SetForeign(sym);

		filterScore = RSI(14);
		StaticVarSet("ValuesToSort_filter" + sym, IIf(IsEmpty(filterScore), -100, filterScore)); 
		
		// restore price arrays to the active symbol
		RestorePriceArrays();
	}
	
	// Generate ranks. resulting ranks will be held in the static var set "RankValuesToSort_filter1'sym'"
	StaticVarGenerateRanks("Rank", "ValuesToSort_filter", 0, 1224); // normal rank mode 
}

// get this symbol's rank from the Rank array
SymRank = StaticVarGet("RankValuesToSort_filter" + Name());

SetOption ("MaxOpenPositions", NumberHeld);
SetOption ("AllowPositionShrinking", True);
SetOption ("HoldMinDays", 1);

PositionSize = -100/NumberHeld;	// Equally divide capital among the positions held

// see if rank improved since 21 bars; if not, then don't buy
RankImprove = IIf(SymRank <= Ref(SymRank, -21), 1, 0);

// rebalance monthly
rebalance = Month() != Ref(Month(), -1);

Buy = rebalance AND (SymRank <= NumberHeld AND RankImprove);
Sell = rebalance;

// Exploration output.
Filter = 1;
AddColumn(Buy, "Buy", 1.0);
AddColumn(C, "Close", 2.1);
AddColumn(SymRank, "Sym Rank", 2.0);
AddColumn(StaticVarGet("RankValuesToSort_filter" + Name()), "Rank", 2.0);
AddColumn(StaticVarGet("ValuesToSort_filter" + Name()), "filterScore", 2.2);

#8

Thank you for the code example.


#9

A quick way would be rebalancing the newly opened positions the moment you open them.

// http://www.amibroker.com/kb/2006/03/06/re-balancing-open-positions/
SetOption ("MinPosValue", 1 ); 

PositionSize = 1;  // set a low value sot that you have funds for all entry signals

SetCustomBacktestProc("");

if (Status("action") == actionPortfolio) 
{
    bo = GetBacktesterObject();	//  Get backtester object
    bo.PreProcess();	//  Do pre-processing (always required)

    for (i = 0; i < BarCount; i++)	//  Loop through all bars
    {
        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)

        desired_value = bo.Equity/bo.GetOpenPosQty(); 
        
        for ( pos = bo.GetFirstOpenPos(); pos ; pos = bo.GetNextOpenPos())
        {
			price = pos.GetPrice ( i , "C" ); 
			current_value = pos.GetPositionValue;
			diff = current_value - desired_value ; 
			
			if ( diff ) 
			bo.ScaleTrade( bar, pos.Symbol, diff < 0, price, abs( diff ) );
        }
        
    }  
      
    bo.PostProcess();	//  Do post-processing (always required)
}


#10

Thanks, yes that would seem to work. However what I was looking to do is to enter the new positions each with an equal amount from the available cash. So e.g., say I have 5 positions open, then sell 3 and enter 2 new ones. I would want the 2 new entries to use the funds from selling the 3 current positions (but not scaling the existing positions).

That would seem to be more complicated. Also I have the trade delay set to 1, so the signals get evaluated per closing price, then are executed next day.

It seems doable to work that out, but honestly not sure that is better. But I wanted to compare: holding existing positions and not re-balancing vs. re-balancing. I will play around with it a bit more.


#11

Thanks aron for great work. Compiling i can't find bar variable, i've changed it with "i".