Rotational System going long and short at the same time

I'm trying to make a rotational system that would buy 2 stocks from a watchlist of 20 of the largest stocks and short 2 stocks at the same time. For positionscore for the buys I would use the lowest RSI value and for the shorts, I would use the highest RSI value.

Positionscore looks for the highest values, shorts negative numbers and buys positive numbers. I could sort the RSI values from highest to lowest. For the lowest two values, I could just convert them to really high numbers, and positionscore would result in buys for them. For the highest two values, I could just convert them to negative numbers and positionscore would short them.

Would it be possible to have two longs and two shorts with the same rotational code? Maybe I have to split it up into two rotational codes. I started to write an AFL which won't work as it is. Does anyone have an idea how I might proceed?

//and short two of the stocks with the highest RSI so that you always have two long and two short positions 
_SECTION_BEGIN("Rotational to buy lowest and sell highest RSI"); 
SetChartOptions(1, chartShowArrows | chartShowDates); 
Float = 100000;  
// Options  
SetOption("CommissionAmount",0.0);  
SetOption("AllowPositionShrinking", True); 
SetOption("InitialEquity", Float);  
 //Parameters 
MaxPositions =4;//Optimize("MaxPositions",2,1,20,1);
RSIPeriod =10;//Optimize("RSIPeriod",2,2,25,1);
RSIIndication = RSI(RSIPeriod);
Number=2;
 
SetOption("MaxOpenPositions", MaxPositions ); 
SetOption("WorstRankHeld", MaxPositions + Number); 
SetPositionSize( 100 / MaxPositions, spsPercentOfEquity );  
SetBacktestMode( backtestRotational ); 
//SetTradeDelays( 0, 0, 0, 0 ); //use settings, set to 0 for close or 1 for open next day
//BuyPrice = Close; //has no effect in rotational mode, use settings
  
Score=100-RSIIndication;//for the two buys
Score=-RSIIndication;//for the shorts....can I have two longs and two shorts in the same Rotational AFL?
PositionScore = Score ;
_SECTION_END(); ```

Not sure if you can make that work with the Rotational mode. Another approach is to explicitly rank your symbols using the StaticVarGenerateRanks function (see examples, either on the forum or in the user guide). And then either have 2 separate ranking arrays for long and short signals ... or if you're shorting the bottom 'x' and buying the top 'x' use a single array.

1 Like

@Marcel before creating a solution I think you need to think about the logic of the strategy you are proposing. Your basic premise is going Long on the most "oversold" stocks and going Short the most "overbought" stocks. Reasonable at first glance.

But you do not actually look for stocks that are "overbought" or "oversold". Instead you are willing to accept the relatively most overbought or relatively most oversold in your list.

Consider the scenario where the lowest RSI in your list is 48 and the highest RSI is 52. Your strategy considers those as the most oversold and overbought, but would you consider those RSI values to really suggest that? I'm fairly certain that none of the mean reversion strategies you have looked at would consider selling short a stock just because it's RSI had climbed to 52 (just as an example).

Similarly depending on how correlated your watch list constituents are, they may all approach oversold (or overbought) simultaneously. For example in a market correction, your lowest may have RSI's around 15 and your highest RSI may be around 20. Ok to to go long perhaps on the two at 15 but is your logic really to be selling short the two stocks with RSI around 20?

I don't know but it doesn't seem intuitive to me.

If you are looking at "pairs trading" it might be best to start with a different premise. If the idea is to learn how to properly code an afl that includes some long/short hedging I think that is a great idea.

2 Likes

Yes, I was thinking along the lines of a pair trading system but instead of just looking at two stocks, it would look at a group of stocks or perhaps ETFs. I would want to buy one or more of the stocks with the lowest relative price and then short the same number of the highest price stocks. I wouldn't necessarily use RSI but I would try any indicator that would work with the concept.

I'm not sure that you could make a successful system using this method. If something like that does work, you probably would have heard of it already. The challenge would be to find a way to code it so that you could backtest it. Any thoughts or ideas from anyone would be welcome.

In Howard Bandy's book, Quantitative Trading Systems, there's a code tilted Figure 15.1. You can buy his book or download the code from here: http://blueowlpress.com/books/quantitative-trading-systems/

Howard's code is for a rotational system that will go long, or short on a group of ETFs depending on the ROC. It will go short if the ROC reads negative for any of the ETFs.

The code allows for longs and shorts at the same time, but it's not quite what I had in mind where you would have matching long and shorts like in pair trading. It does prove that it is possible to go long and short at the same time with the same Amibroker rotational code.

1 Like

From this link: https://www.amibroker.com/guide/afl/staticvargenerateranks.html
there's a nice example of StaticVarGenerateRanks. I could sort a list of 15 ETFs according to RSI and rank them from -7 to +7. I figure that I could use the -7 to generate a sell signal and the +7 as a buy signal in rotational system. I could make an AFL to plot the rank of the ETFs. I think I could make that into a rotational system but I'm not too sure how I could do that. Any suggestions?

/// from @link https://www.amibroker.com/guide/afl/staticvargenerateranks.html
// Example 1. code for normal ranking mode
// (everything done is done in one pass, can be used in indicator):
/////////////////////////////////
SetChartOptions(1, chartShowArrows | chartShowDates); 
_N(Title = StrFormat("{{NAME}} - {{INTERVAL}} {{DATE}} Open %g, Hi %g, Lo %g, Close %g (%.1f%%) \n{{VALUES}}", O, H, L, C, SelectedValue( ROC( C, 1 ) ) ));	
symlist = "DIA,EEM,FXI,IBB,IJH,IJR,IWB,IWM,IYF,KRE,QQQ,SOXX,SPY,URTH,XLK";


// delete static variables - DO NOT forget the asterisk (wildcard) at the end
StaticVarRemove( "ValuesToSort*" );

// fill input static arrays

qty = 0;


for ( i = 0; ( sym = StrExtract( symlist, i ) ) != ""; i++ )
{
    SetForeign( sym );
    IndicatorSetting = Optimize("IndicatorSetting",15,2,55,1);
     Value = RSI(IndicatorSetting);
     Price=C;
    RestorePriceArrays();
    StaticVarSet( "ValuesToSort" + sym, -Value );
        qty++;
}

// perform ranking
StaticVarGenerateRanks( "rank", "ValuesToSort", 0, 1224 ); // normal rank mode

// read ranking (subtract from quantity gives you order from BOTTOM)




for ( i = 0; ( sym = StrExtract( symlist, i ) ) != ""; i++ )
{
    Plot( qty - StaticVarGet( "RankValuesToSort" + sym )-7, sym, colorCustom10 + i,stylethick );
   
 }```

@marcel you might be able to use this article to code your idea (ignore the steps in the article that make it a long only system)

http://www.amibroker.com/kb/2016/04/17/long-only-rotational-back-test/

Try using your ranks as your PositionScore.

Sorry, did not see your response until today. Here is some code that I think will work. I tested minimally, and it seemed to do what you want: buy the top 'x' (default 2), and short the bottom 'x' (default also 2). Using RSI(14) as the filter. Obviously you can change that to whatever filter you want. It will use the watchlist that you select from the "Apply to" window (better than hard coding an array of symbols).

/* 
	Long & short system:
		Buy (long) the top x
		Sell (short) the bottom x

	Rotate positions on specified timeframe:
		daily
		weekly
		monthly
*/
	
NumberLong = Param("# of Long Positions", 2, 0, 100, 1);
NumberShort = Param("# of Short Positions", 2, 0, 100, 1);

// Trading frequency
TradeFreq = ParamList("Trade Frequency", "Day|Week|Month", 1);

// set trade signal based on frequency
switch(TradeFreq) {
	case "Day":
		Rotate = 1;
		break;
		
	case "Week":
		Rotate = DayOfWeek() < Ref(DayOfWeek(), -1);
		break;
	
	case "Month":
		Rotate = Month() != Ref(Month(),-1);
		break;

}

// 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), Null, filterScore)); 

		// restore price arrays to the active symbol
		RestorePriceArrays();
	}
	
	// Generate ranks. resulting ranks will be held in the static var set "RankValuesToSort_filter'sym'"
	StaticVarGenerateRanks("Rank", "ValuesToSort_filter", 0, 1224); // normal rank mode
	
	StaticVarSet("ValuesToSort_NumSymbols", NumSymbols);
}

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

PositionSize = -100/(NumberLong + NumberShort);	// Equally divide capital among the positions held
SetOption ("MaxOpenPositions", NumberLong + NumberShort);
SetOption ("AllowPositionShrinking", True);
SetOption("HoldMinDays", 1);

// Buy / rotate rule: first bar, or rotate day
Firstbar = Status("firstbarintest") OR Status("firstbarinrange");

Buy = (Firstbar OR Rotate) AND (SymRank <= NumberLong);
Sell = Rotate;

Short = (Firstbar OR Rotate) AND (SymRank > (StaticVarGet("ValuesToSort_NumSymbols") - NumberShort));
Cover = Rotate;

// Exploration output.
Filter = rotate OR firstbar;

AddColumn(Rotate, "Rotate", 1.0);
AddColumn(Firstbar, "Firstbar", 1.0);

// Exploration for debugging
AddColumn(C, "Close", 2.1);
AddColumn(SymRank, "Sym Rank", 2.0);
AddColumn(StaticVarGet("RankValuesToSort_filter" + Name()), "Rank", 2.0);
AddColumn(StaticVarGet("ValuesToSort_filter" + Name()), "Filter Value", 2.2);

7 Likes

@derfahrer, that's a really nice code you made. Thank you so much. I hope I can code like that someday. Of course the code wouldn't be profitable as it is, but perhaps with filtering and optimization I can make something that works. I'll let you know about any results I can get with it.

writing the code is the easier part. The harder part is devising a good system. If you wanted to try an approach where you used one filter (say RSI(14)) for ranking long positions, and a different filter for short positions, you could do that by having 2 separate rank arrays. (just clone the ranking code, changing the variable names as needed)

1 Like

I'm using norgate data which includes constituent data. So if I apply the same logic the issue I have is that in the ranking there will be symbols that I won't trade on that day because it was not historically in the universe. Do you have any ideas on how to handle that scenario?

Good question. If you were using index membership, I think I would make the constituent check when generating the score to be ranked. i.e., use the indicator score (RSI in my simple example) if the symbol is a member on that day, otherwise make the value 0 or something that would not be ranked). That might create some issues with the long/short aspect though ... so would need to think further on that.

I managed to get it working using 2 variables one for buys and one for shorts. I also applied the constituent filter in the ranking as you mentioned.

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

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

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

		// make the symbol be the 'current' symbol (so the OHLC arrays can be used directly)
		SetForeign(sym);
	
		buyFilterScore = 100 - RSI(14);
		sellFilterScore = RSI(14);

		StaticVarSet("ValuesToSort_BuyFilter" + sym, IIf(IsEmpty(buyFilterScore) OR NOT NorgateIndexConstituentTimeSeriesOther("$SPX", sym), -1, buyFilterScore)); 	
		StaticVarSet("ValuesToSort_ShortFilter" + sym, IIf(IsEmpty(sellFilterScore) OR NOT NorgateIndexConstituentTimeSeriesOther("$SPX", sym), -1, sellFilterScore)); 		

		// restore price arrays to the active symbol
		RestorePriceArrays();
	}
	
	// Generate ranks. resulting ranks will be held in the static var set "RankValuesToSort_filter'sym'"
	StaticVarGenerateRanks("Rank", "ValuesToSort_BuyFilter", 0, 1224); // normal rank mode
	StaticVarGenerateRanks("Rank", "ValuesToSort_ShortFilter", 0, 1224); // normal rank mode
	
}

1 Like

@kefa and @derfahrer - thanks so much for sharing your work on this. I have been struggling with a similar issue and had put it to the side.

@kefa, one quick question - I have reflected the changes that you added in terms of allowing for historical constituents but I don't get any short trades. I think it is because the use of SymRankShort > ( StaticVarGet("ValuesToSort_NumSymbols") - NumberShort) in the Short command. In other words, the SymRankShort is now less than the score of Symbols less NumberShort. Instead of using StaticVarGet("ValuesToSort_NumSymbols") is it possible to reference the number of symbols in the index at that time as per the Norgate index database?

Perhaps this isn't the issue at all. Any thoughts would be welcome. I have added the code below.

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

if (Status("stocknum") == 0) 
{
	// delete static variables 
	StaticVarRemove("*ValuesToSort*"); 

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

		// make the symbol be the 'current' symbol (so the OHLC arrays can be used directly)
		SetForeign(sym);
	
		buyFilterScore = 100 - RSI(14);
		sellFilterScore = RSI(14);

		StaticVarSet("ValuesToSort_BuyFilter" + sym, IIf(IsEmpty(buyFilterScore) OR NOT NorgateIndexConstituentTimeSeriesOther(IndexCode, sym), -1, buyFilterScore)); 	
		StaticVarSet("ValuesToSort_ShortFilter" + sym, IIf(IsEmpty(sellFilterScore) OR NOT NorgateIndexConstituentTimeSeriesOther(IndexCode, sym), -1, sellFilterScore)); 
		
		// restore price arrays to the active symbol
		RestorePriceArrays();
	}
	
	// Generate ranks. resulting ranks will be held in the static var set "RankValuesToSort_filter'sym'"
	StaticVarGenerateRanks("Rank", "ValuesToSort_BuyFilter", 0, 1224); // normal rank mode
	StaticVarGenerateRanks("Rank", "ValuesToSort_ShortFilter", 0, 1224); // normal rank mode	
}

// get this symbol's rank from the Rank array
SymRankBuy = StaticVarGet("RankValuesToSort_BuyFilter" + Name());
SymRankShort = StaticVarGet("RankValuesToSort_ShortFilter" + Name());

PositionSize = -100/(NumberLong + NumberShort);	// Equally divide capital among the positions held
SetOption ("MaxOpenPositions", NumberLong + NumberShort);
SetOption ("AllowPositionShrinking", True);
SetOption("HoldMinDays", 1);

// Buy / rotate rule: first bar, or rotate day
Firstbar = Status("firstbarintest") OR Status("firstbarinrange");

Buy = (Firstbar OR Rotate) AND (SymRankBuy <= NumberLong);
Sell = Rotate;

Short = (Firstbar OR Rotate) AND (SymRankShort > ( StaticVarGet("ValuesToSort_NumSymbols") - NumberShort));
Cover = Rotate;

Hi @djohnson925,

Not sure why you would need the symbol count anymore? Both the buy and short scores have been sorted so the best ones are at the top, i.e. the 1st rank represents the best signal. There is no need to look at the end rankings.

There is no direct way to get the universe count that I worked out. However for the scenario we have in this example where the buy and the short scores are the inverse of each other we can simply add them up and subtract one to get the universe count. This is because of the way the signals have been constructed and the sorting done. The short ranking will be the reverse of the buy ranking. Hope that makes sense.

SymBuyRank = StaticVarGet("RankValuesToSort_BuyFilter" + Name());
SymShortRank = StaticVarGet("RankValuesToSort_ShortFilter" + Name());

NumSymbols = SymBuyRank + SymShortRank - 1;