AFL Code Examples

These code examples are mainly taken from the book “Quantitative Trading Systems” (2nd Edition) by Howard B Bandy. Some of them are also taken from various AFL tutorials on Youtube.

Relevant Tutorials

Code Snippets

Entries and Exits

Entry and Exit at Fixed Intervals

SetTradeDelays(0, 0, 0, 0);
EntryInterval = Param("Entry Interval", 6, 4, 100, 1);
HoldDays = Param("Hold Days", 3, 1, 100, 1);

BuyPrice = C;

Buy = (BarIndex() % EntryInterval) == 0;  //Enter every EntryInterval days
Sell = BarsSince(Buy) >= HoldDays;  //Exit after HoldDays days

Sell = ExRem(Sell, Buy);  //No more sell signal until you reach a buy signal

Scaling In

/** Method 1: 10% of equity per trade **/

SetPositionSize(10, spsPercentOfEquity);

/** Method 2: First position 50%, subsequent 10% **/

PosSize = IIf(Buy==1, 50, IIf(Buy == sigScaleIn, 10, 0));
SetPositionSize(PosSize, spsPercentOfEquity);

/** Method 3: First position is 50% of equity, subsequent scale ins at (50/number of scales)*percent of current position **/


//Determine how many times we scale in (excluding the first purchase)
ScalesSinceEntry = SumSince(Buy==1, Buy==sigScaleIn);	

//Assign Position Sizes
ScalePosSize= Nz(50/ScalesSinceEntry, 0);

EntryPosSize = 50;

//Set position size for first position
SetPositionSize(EntryPosSize, spsPercentOfEquity);

//Update the position size when buy==sigscalein
SetPositionSize(ScalePosSize, IIf(Buy==sigScaleIn, spsPercentOfPosition, spsNoChange));	

Scaling Out

Scaling in and out are both coded in the Buy array.

/** Buy when price breaks out of a 5 days high, scale out 10% every 5 days. Sell when price breaks below 20 days low **/
bullCond = H>Ref(HHV(H, 5), -1);
Sell = L<Ref(LLV(L, 20), -1);

bullCond = ExRem(bullCond, Sell);   //Remove excess bullCond signals

Buy = IIf(bullCond, 1, IIf(BarsSince(Buy==1)%5==0, sigScaleOut, 0));

Sell = ExRem(Sell, Buy);   //Remove excess sell signals

//Buy 50% of equity, scale out 10% each time
PosSize = IIf(Buy==1, 50, IIf(Buy==sigScaleOut, 10, 0));  
SetPositionSize(PosSize, spsPercentOfEquity);

Profit Target

/*Using a 10% profit target. No exit otherwise.
 This example shows how we can use a scalar in our code */

SetTradeDelays(0, 0, 0, 0);
BuyPrice = C;
SellPrice = C;

Buy = Cross(MA(C, 5), MA(C, 25));

Sell = 0;

ProfitTargetPercent = 10;

EntryPrice = 0; //This is a scalar

// Loop through all the bars
// There are BarCount bars, indexed from 0 to BarCount-1

for(i=0; i<BarCount; i++)
{
	if(EntryPrice == 0 AND Buy[i] == 1)	//Check for being flat and entering a new trade
	{
		EntryPrice = BuyPrice[i];
	}else if (EntryPrice > 0 AND H[i] > EntryPrice*(1.0 + 0.01*ProfitTargetPercent))
	{
		Sell[i] = 1;
		SellPrice[i] = EntryPrice*(1.0 + 0.01*ProfitTargetPercent);
		EntryPrice = 0; 	//Reset entry price
	}
}

Buy = ExRem(Buy, Sell);
Sell = ExRem(Sell, Buy);

Multiple Exits

/*
Three Exits:
1. If first profit target is hit, scale out 50% of position
2. If second profit target is hit, exit completely
3. If trailing stop is hit, exit completely

This demonstrates how to code a trailing stop exit and how to do a scale out

*/


SetTradeDelays(0, 0, 0, 0);
BuyPrice = C;
SellPrice = C;

Buy = Cross(MA(C, 5), MA(C, 25));
Sell = 0;

//Targets (in percentages)
FirstProfitTarget = 10;
SecondProfitTarget = 20;
TrailingStop = 10;

//Scalars to keep track of prices while in trade
PriceAtBuy = 0;
HighSinceBuy = 0;
Exit = 0;

for (i=0; i< BarCount; i++)
{

	//Check for being flat and entering a new trade
	
	if (PriceAtBuy ==0 AND Buy[i] == 1)
	{
		PriceAtBuy = BuyPrice[i];
	}else if (PriceAtBuy > 0)	//If there is an existing trade
	{
		//Update HighSinceBuy if current high is higher than HighSinceBuy
		HighSinceBuy = Max(High[i], HighSinceBuy);	
		
		if (Exit == 0 AND High[i] >= (1+ FirstProfitTarget*0.01)*PriceAtBuy)												       
		//First Profit Target Hit, scale out
		{
			Exit = 1;
			Buy[i] = sigScaleOut;
			BuyPrice[i] = (1+ FirstProfitTarget*0.01)*PriceAtBuy;
		}
		
		if (Exit == 1 AND High[i] >= (1+SecondProfitTarget*0.01)*PriceAtBuy)	
		//Second Profit Target Hit, exit completely
		{
			Exit = 2;
			SellPrice = Max(Open[i],(1+SecondProfitTarget*0.01)*PriceAtBuy);
		}
		
		if (Low[i] <= (1-TrailingStop*0.01)*HighSinceBuy)			
		//Trailing stop is hit, exit completely
		{
			Exit = 3;
			SellPrice[i] = Min(Open[i], (1-TrailingStop*0.01)*HighSinceBuy);
		}
		

		if (Exit >= 2)
		{
			Buy[i] = 0;

			//Assign the correct sell code. 2 = Max Loss, 3 = Profit, 4 = Trail
			Sell[i] = Exit + 1; 
			

			//Reset scalars
			Exit = 0;
			PriceAtBuy = 0;
			HighSinceBuy = 0;
		
		}
	}

}

SetPositionSize(50, spsPercentOfEquity);

//Set position size to 50% of position when scaling out (i.e. Exit = 1 or Buy == sigScaleOut)
SetPositionSize(50, spsPercentOfPosition*(Buy == sigScaleOut));

Different Types of Entries

Includes code for testing different entry orders

  1. Market on Close
  2. Next Day Open
  3. Limit Order
  4. Stop Order

Refer to https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Chapter_7_%E2%80%93_Entries.

//Copied or Adapted from Mean Reversion Trading Systems by Howard Bandy
/////////////// Code for Market On Close ////////////////////////

SetTradeDelays(0, 0, 0, 0);
BuyPrice = Close;
SellPrice = Close;
ER1 = DayOfWeek() == 2;		//Entry rule - Buy Tues
XR1 = DayOfWeek() == 3;		//Exit rule - Sell Wed
Buy = ER1;
Sell = XR1;

/////////////// Code for Next Day Open ////////////////////////

//This code buys on Wed at Open and sells on Thurs at Open
SetTradeDelays(1, 1, 0, 0);
BuyPrice = Open;
SellPrice = Open;
ER1 = DayOfWeek() == 2;		//Entry rule - Buy Tues
XR1 = DayOfWeek() == 3;		//Exit rule - Sell Wed
Buy = ER1;
Sell = XR1;

//This code buys on Wed at Open and sells on Wed at Close
//Be sure to check the box "Allow same bar exit" on Backtester Settings > General tab

SetTradeDelays(1, 0, 0, 0);
BuyPrice = Open;
SellPrice = Close;
ER1 = DayOfWeek() == 2;		//Entry rule - Buy Tues
XR1 = DayOfWeek() == 3;		//Exit rule - Sell Wed
Buy = ER1;
Sell = XR1;

//This code buys on Wed at Open and sells on Thurs at Open
//The difference is that it uses the previous day's "indicator". This is preferred by the author

SetTradeDelays(0, 0, 0, 0);
BuyPrice = Open;
SellPrice = Open;
ER1 = DayOfWeek() == 2;		//Entry rule - Buy Tues
XR1 = DayOfWeek() == 3;		//Exit rule - Sell Wed
Buy = Ref(ER1, -1);
Sell = Ref(XR1, -1);

/////////////// Code for LIMIT ORDER ////////////////////////

SetTradeDelays(0, 0, 0, 0);
LimitPrice = 0.99*Close;		//Compute limitprice today for use tomorrow
BuyPrice = Ref(LimitPrice, -1);
SellPrice = Close;
Buy = L < Ref(LimitPrice, -1);
Sell = 1;

/////////////// Code for STOP ORDER ////////////////////////

SetTradeDelays(0, 0, 0, 0);
Slippage = 0.02; 				//Your best estimate
StopPrice = 1.01*H;				//Compute stopprice today for use tomorrow
BuyPrice = Ref(StopPrice, -1) + Slippage;
SellPrice = Close;
Buy = H > Ref(StopPrice, -1);
Sell = 1;

Limiting the Number of Daily Entries

limit_daily_entries = 100;

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
    {
        count = 0;

        for( sig = bo.GetFirstSignal( i ); sig; sig = bo.GetNextSignal( i ) )
        {
            if( sig.IsEntry() )
            {
                if( count < limit_daily_entries )
                    count ++;
                else
                    sig.Price = -1 ; // ignore entry signal
            }
        }

        bo.ProcessTradeSignals( i );	//  Process trades at bar (always required)
    }

    bo.PostProcess();	//  Do post-processing (always required)
}

Regime Change

The regime change system has two sets of rules and parameters. Each set defines a model. Trades are taken based on the model that is currently dominant (i.e. more profitable).

Refer to https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Regime_Change.

// RegimeChange.afl
// Taken or adapted from Mean Reversion Trading Systems by Howard Bandy

//Keeps track of results from two sets of rules. Uses whichever one has shown the best recent performance.

//This is not a particularly good system. It is intended to illustrate the concept and technique.

/////////////////////////// System Settings ///////////////////////////
Exclude = Name() == "VIX";

OptimizerSetEngine("cmae");

eq = Param("Fixed Equity", 100000, 1, 10000000);
percentEQ = Param("Percentage of EQ to risk", 1, 0.01, 1, 0.01);
SetOption("ExtraColumnsLocation", 1);
SetOption("CommissionMode", 2); 	//$ per trade
SetOption("CommissionAmount", 5);
SetOption("InitialEquity", eq);
SetPositionSize(percentEQ*eq, spsValue);

MaxPos = 1;
SetOption("MaxOpenPositions", MaxPos);
SetBacktestMode(backtestRegularRawMulti);
SetTradeDelays(0,0,0,0);
BuyPrice = SellPrice = Close;

ROCLB = Optimize("ROCLB", 28, 2, 42, 1);
S2BuyLevel = Optimize("S2BuyLevel", 31, 1, 100, 1);
S3BuyLevel = Optimize("S3BuyLevel", 13, 1, 20, 1);
S2RSILB = Optimize("S2RSILB", 9, 2, 10, 1);
S3RSILB = Optimize("S3RSILB", 3, 2, 10, 1);

/////////////////////////// System 2 ///////////////////////////

RSI2 = RSI(S2RSILB);
Buy = S2Buy = RSI2 < S2BuyLevel;
Sell = S2Sell = RSI2 > 75;

S2Equity = Equity();
S2ROC = ROC(S2Equity, ROCLB);

/////////////////////////// System 3 ///////////////////////////

RSI3 = RSI(S3RSILB);
Buy = S3Buy = RSI3 < S3BuyLevel;
Sell = S3Sell = RSI3 > 75;

S3Equity = Equity();
S3ROC = ROC(S3Equity, ROCLB);

// Compare which to ue by comparing recent results

dominant = IIf(S2ROC > S3ROC, 2, 3);
state[0] = 0;

for (i = 1; i < BarCount; i++)
{
	//If this bar is the first bar of a new state, go flat unless there is a buy signal for the new state
	if(dominant[i] != dominant[i-1])
	{
		switch(dominant[i])
		{
			case 2:
				if (S2Buy[i])
				{
					Buy[i] = 1;
					state[i] = 2;
				}else
				{
					Sell[i] = 1;
					state[i] = 0;
				}
				break;
			
			case 3:
				if (S3Buy[i])
				{
					Buy[i] = 1;
					state[i] = 3;
				}else
				{
					Sell[i] = 1;
					state[i] = 0;
				}
				break;
		}
	}
	//continuation of dominant system	
	else
	{
		switch(state[i-1])
		{
			case 0:		//coming in flat
			{
				if(dominant[i] == 2 AND S2Buy[i])
				{
					Buy[i] = 1;
					state[i] = 2;
				}
				else if (dominant[i] == 3 AND S3Buy[i])
				{
					Buy[i] = 1;
					state[i] = 3;
				}
				else
				{
					Buy[i] = 0;
					state[i] = 0;				
				}
				
				break;			
			}
			
			case 2:		//long from system 2
			{
				if(dominant[i] == 2 AND S2Sell[i])
				{
					Sell[i] = 1;
					state[i] = 0;
				}
				else
				{
					state[i] = 2;
				}
				
				break;
			}
			
			case 3:		//long from system 3
			{
				if (dominant[i] == 3 AND S3Sell[i])
				{
					Sell[i] = 1;
					state[i] = 0;				
				}
				else
				{
					state[i] = 3;
				}
				break;
			}		
		}
	}
}

e = Equity();

/////////////////////////// Plots ///////////////////////////

SetChartOptions(0,chartShowArrows|chartShowDates);
_N(Title = StrFormat("{{NAME}} - {{INTERVAL}} {{DATE}} Open %g, Hi %g, Lo %g, Close %g (%.1f%%) {{VALUES}}", O, H, L, C, SelectedValue( ROC( C, 1 ) ) ));
Plot( C, "Close", ParamColor("Color", colorDefault ), styleNoTitle | ParamStyle("Style") | GetPriceStyle() ); 

S2shapes = IIf(S2Buy, shapeUpArrow, IIf(S2Sell, shapeDownArrow, shapeNone));
S2shapecolors = IIf(S2Buy, colorBlue, IIf(S2Sell, colorGold, colorWhite));
PlotShapes(S2shapes, S2shapecolors);

S3shapes = IIf(S3Buy, shapeDigit3, IIf(S3Sell, shapeDigit3, shapeNone));
S3shapecolors = IIf(S3Buy, colorYellow, IIf(S3Sell, colorAqua, colorWhite));
PlotShapes(S3shapes, S3shapecolors, 0, IIf(S3Buy, 0.99*L, 1.01*H));


Plot(dominant, "Dominant", colorGreen, styleLine|styleThick|styleOwnScale);


Mean Reversion

Smoothing an Oscillator

This example demonstrates how you can use the smoothed version of an oscillator to trigger a signal.

//Center of Gravity.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 143

//Trading system based on John Ehlers' Center of Gravity Oscillator
//Cybernetic Analysis for Stocks and Futures
//Wiley 2004

SetTradeDelays(0,0,0,0);
BuyPrice = C;
SellPrice = C;
SetBarsRequired(200, 0);

function CGOscillator(Price, Length)
{
	Result = 0;
	
	for(i=length; i<BarCount; i++)
	{
		Num = 0;
		Denom = 0;
		
		for(j=0; j<Length;j++)
		{
			Num = Num + (1+j)* Price[i-j];
			Denom = Denom + Price[i-j];
		}
		
		if (Denom !=0)
			Result[i] = 100.0 * ((-Num/Denom) + (Length + 1)/2);			
	}
	
	return Result;
	
}


CGOLength = Param("CGOLength", 13, 1, 250, 10);
SmLength = Param("SmLength", 2, 1, 20, 2);
HoldDays = Param("HoldDays", 6, 1, 10, 1);

Price = (H+L)/2;
CGO = CGOscillator(Price, CGOLength);


CGOSmoothed = DEMA(CGO, SmLength);

Buy = Cross(CGO, CGOSmoothed);
Sell = Cross(CGOSmoothed, CGO) OR (BarsSince(Buy) >= HoldDays);
Sell = ExRem(Sell, Buy);


Plot(CGO, "CG Oscillator", colorRed, styleLine|styleLeftAxisScale);
Plot(CGOSmoothed, "CG Smoothed", colorBlue, styleLine|styleLeftAxisScale);

shape = Buy * shapeUpArrow + Sell * shapeDownArrow;
Plot(Close, "Price", colorBlack, styleCandle);
PlotShapes(shape, IIf(Buy, colorGreen, colorRed), 0, IIf(Buy, Low, High));

Coding a Position-In-Range Indicator

This example demonstrates how you can compute a position-in-range (PIR) statistic and use that to form a series. It also demonstrates how we can use the PIR series and a smoothed version of the series to get buy and sell signals.

I had to make some changes to Line 15 because HHV(r, PIRLookback)-LLV(r, PIRLookback) is sometimes zero, which leads to a “Divide by zero” error. The solution I used is to ensure that HHV(r, PIRLookback)-LLV(r, PIRLookback) is at least 0.01 using the Max() function.

//StochasticOfTheRSI.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 146

SetTradeDelays(0,0,0,0);
BuyPrice = C;
SellPrice = C;

RSILength = Optimize("RSILength", 10, 2, 20, 1);
PIRLookback = Optimize("PIRLookback", 20, 4, 40, 2);
TriggerSmoother = Optimize("TriggerSmoother", 3, 2, 10, 1);

r = RSIa(C, RSILength);

//pir = Position in range
pir = (r-LLV(r, PIRLookback))/Max(0.01, (HHV(r, PIRLookback)-LLV(r, PIRLookback)));
	
//A smoothed copy of the pir series
trigger = DEMA(pir, TriggerSmoother);

Buy = Cross(pir, trigger);
Sell = Cross(trigger, pir);

PlotShapes(Buy*shapeUpArrow, colorBrightGreen);
PlotShapes(Sell*shapeDownArrow, colorRed);

Plot(pir, "PIR", colorRed, styleLine|styleLeftAxisScale);
Plot(trigger, "Trigger", colorBlue, styleLine|styleLeftAxisScale);
 

Converting any series into an oscillator

This example shows how you can convert any series into an oscillator by subtracting a moving average from it.

//DeTrendedPriceOscillator.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 149

/* Using any price series as input, compute an oscillator by subtracting
a moving average of the series from the series itself.
Then use that series to compute Buy and Sell signals */

SetTradeDelays(0,0,0,0);
BuyPrice = C;
SellPrice = C;

//The series to be detrended
Price = C;

//The length of the moving average to remove
MALength = Param("MA Length", 30, 2, 200, 1);

//Subtract the moving average, leaving an oscillator
MovingAverage = MA(Price, MALength);
Oscillator = Price - MovingAverage;

// Smooth the oscillator once
OscSmoothLength = Param("OscSmLen", 10, 1, 50, 1);
SmOsc = DEMA(Oscillator, OscSmoothLength);

// Smooth again to create a trigger line
Trigger = DEMA(SmOsc, 3);

Buy = Cross(SmOsc, Trigger);
Sell = Cross(Trigger, SmOsc);

//Plot(MovingAverage, "MA", colorRed, styleLine);
//Plot(Oscillator, "Osc", colorBlue, styleLine|styleLeftAxisScale);

Plot(SmOsc, "SmOsc", colorGreen, styleLine|styleLeftAxisScale);
Plot(Trigger, "Trigger", colorRed, styleLine|styleLeftAxisScale);

PlotShapes(Buy*shapeUpArrow+Sell*shapeDownArrow, IIf(Buy, colorGreen, colorRed));

Foreign

Using Foreign

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_14_Sector_Analysis.

//UsingForeign.afl
//Adapted from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 197

/*
This program assumes that the file named "signals" has been imported into the Amibroker database.

Any close greater than or equal to 14.0 will be taken as a Buy signal and any close less than or equal 
to 6.0 will be taken as a Sell signal.
*/

sigs = Foreign("signals", "C"); //Load the closing price of signals into variable sigs

RestorePriceArrays();	//Use data from the current symbol from this point on

Buy = sigs >= 14.0;
Sell = sigs <= 6.0;

Plot(C, "C", colorBlack, styleCandle);
Plot(sigs, "Signals", colorRed, styleLine|styleOwnScale);

Using SetForeign

//UsingSetForeign.afl
//Adapted from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 198

/* This code reads one data series that will be used to compute the trading signals for a group of issues */

SetForeign("OEX");	//SetForiegn loads the OHLCV data of OEX into the O,H,L,C,V variables

Buy = Cross(MA(C, 7), MA(C, 3));	//At this point, all calculations are done with the OHLCV of OEX
Sell = BarsSince(Buy) >=3;

RestorePriceArrays();	//From the point forward, use thed ata in the selected data series

/* Use the Charts menu and insert this program. Place the cursor on a trade and select other symbols in the Symbols menu.
You'll see that trades always take place at the same date. */

PlotShapes(Buy*shapeSmallCircle,colorGreen,0, L*0.9); 
PlotShapes(Sell*shapeSmallCircle,colorPink,0, H*1.1); 

Time Frames

Plotting Pivot Levels for Different Timeframes

This indicator plots the pivot points for different time frames. It illustrates how to use the TimeFrameCompress() and TimeFrameExpand() functions.

Refer to https://www.amibroker.com/guide/afl/timeframecompress.html.

tf = Param("TimeFrame", 0, 0, 3);

//tf: 0 = Daily, 1 = Weekly, 2 = Monthly, 3 = Yearly

if (tf == 0)
	t = inDaily;
else if (tf == 1)
	t = inWeekly;
else if (tf == 2)
	t = inMonthly;
else if (tf == 3)
	t = inYearly;

ch = TimeFrameCompress( High, t, compressHigh);
cl = TimeFrameCompress( Low, t, compressLow);
cc = TimeFrameCompress( Close, t);

p = (ch + cl + cc)/3;
r1 = 2*p - cl;
s1 = 2*p - ch;
r2 = p + r1 - s1;
s2 = p - r1 + s1;
r3 = 2*r1 - s1;
s3 = 2*s1 - r1;

p = TimeFrameExpand(p, t);
Plot(Ref(p, -1), "Pivot", colorBlue, styleLine|styleDashed);

r1 = TimeFrameExpand(r1, t);
Plot(Ref(r1, -1), "R1", colorWhite, styleLine|styleDashed);

s1 = TimeFrameExpand(s1, t);
Plot(Ref(s1, -1), "R1", colorWhite, styleLine|styleDashed);

r2 = TimeFrameExpand(r2, t);
Plot(Ref(r2, -1), "R2", colorRed, styleLine|styleDashed);

s2 = TimeFrameExpand(s2, t);
Plot(Ref(s2, -1), "S2", colorRed, styleLine|styleDashed);

r3 = TimeFrameExpand(r3, t);
Plot(Ref(r3, -1), "R3", colorYellow, styleLine|styleDashed);

s3 = TimeFrameExpand(s3, t);
Plot(Ref(s3, -1), "S3", colorYellow, styleLine|styleDashed);

Exploration

Ideal Entries

This code allows us to set the desired profit, maximum acceptable risk and holding period, and finds all the candles that satisfy such parameters.

  • Gain is computed from Close of entry day to Close of exit day.
  • Risk is computed from Close of entry day to lowest Low while in trade.

Once you identify the ideal entry and exit candles, you can working on designing a system that generates signals corresponding to the ideal entry and exit candles.

The code plots hollow Up and Down triangles for the ideal entry and exit candles, respectively. It also includes a simple moving average crossover system that plots a solid Up arrow when a Buy signal occurs.

If all the solid Up arrows correspond perfectly with the hollow Up triangles, we have a perfect system that matches our criteria.

Unfortunately, this is not the case. The objective is to find a system that matches the hollow Up and Down triangles as closely as possible.

One possible use for this code is to use Amibroker’s AddColumn() statements to generate tabular results that can be analysed with a spreadsheet or fed into a machine learning model to train the model. The code below added a AddColumn() statement for the “IdealEntry” array. You can view this array using the “New Analysis window > Explore” feature.

SetTradeDelays(0, 0, 0, 0);
BuyPrice = C;
SellPrice = C;

/*This code looks into the future to compute potential gain and potential loss from the present.*/

// Defining and plotting the hollow triangles for the ideal system

DaysAhead = Param("Days Ahead", 5, 1, 20, 1);
GainAhead = 100.0 * (Ref(C, DaysAhead) - C) / C;
RiskAhead = 100.0 * (C- LLV(Ref(L, DaysAhead), DaysAhead)) / C;

DesiredProfit = Param("Desired Profit", 5, 0, 5, 10, 0.5);
MaximumRisk = Param("Maximum Risk", 3, 0.5, 10, 0.5);

IdealEntry = (GainAhead >= DesiredProfit) AND (RiskAhead <= MaximumRisk);

HoldDays = Param("Hold Days", 3, 1, 60, 1);

IdealExit = BarsSince(IdealEntry) >= HoldDays;
IdealExit = ExRem(IdealExit, IdealEntry); // Remove extra exits, but show all entries

IdealShape = IdealEntry * shapeHollowUpTriangle + IdealExit * shapeHollowDownTriangle;

IdealColor = IIf(IdealEntry, colorPaleGreen, colorPink);
IdealPosition = IIf(IdealEntry, Low, High);
PlotShapes(IdealShape, IdealColor, 0, IdealPosition);

/** Testing a simple moving average crossover system. Alter the code below to test your own system. **/

// Start of system
MALength1 = Param("MA Length 1", 5, 1, 50, 1);
MALength2 = Param("MA Length 2", 20, 1, 50, 1);
MA1 = MA(C, MALength1);
MA2 = MA(C, MALength2);
Buy = Cross(MA1, MA2);
// End of system

//Plot system (up arrows and moving averages)
IndicatorShape = Buy * shapeUpArrow;
IndicatorColor = colorGreen;
IndicatorPosition = IIf(Buy, Low, High);
PlotShapes(IndicatorShape, IndicatorColor, 0, IndicatorPosition);
Plot(MA1, "MA1", colorGreen, styleLine);
Plot(MA2, "MA2", colorBlue, styleLine);
GraphXSpace = 5;

/** Exploration Code **/

Filter = 1;
AddColumn(IdealEntry, "Ideal Entry", 1);

Future Performance

This program computes an advance-decline diffusion index indicator and generates various statistics regarding the performance of the indicator.

It’s very useful for exploration as it allows us to explore the AE (adverse excursion), MAE (maximum adverse excursion), FE (favorable excursion), MFE (maximum favorable excursion), EQ (equity), MEQ (maximum equity), DD (drawdown), and MDD (maximum drawdown) for the indicator after a certain number of days (defined by Horizon).

The indicator can be changed to some other indicators that the user desires to analyse.

Refer to https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Miscellaneous

// IndicatorAnalysisTemplate.afl
//Copied or Adapted from Mean Reversion Trading Systems by Howard Bandy
//Template to create a set of columnar data giving indicator value and performance over the forecast horizon 

//Use Amibroker's Analysis > Explore.
//Export the columns of data to Excel for analysis

///////////// MODIFY THE INDICATOR BELOW /////////////

SetForeign("$NYA");
Plot(C, "C", colorBlack, styleCandle);

ADV = Foreign("#NYSEADV", "C");
DEC = Foreign("#NYSEDEC", "C");
AdvDecLine = 100ADV / (ADV + DEC);

Plot(AdvDecLine, "ADLine", colorRed, styleLine|styleOwnScale);

//The indicator
Indic = AdvDecLine;

///////////// END OF INDICATOR MODIFICATION /////////////


//The forecast horizon
Horizon = 5;

//Examine all data

for (i=1; i<BarCount; i++)
{

	//Entry is C[i]
	//Exit is C[i+Horizon]
	
	AE[i] = 0;		//Adverse Excursion
	MAE[i] = 0;		//Maximum Adverse Excursion
	FE[i] = 0;		//Favorable Excursion
	MFE[i] = 0;		//Maximum Favourable Excursion
	EQ[i] = 0;		//Equity relative to entry
	MEQ[i] = 0;		//Maximum Equity
	DD[i] = 0;		//Drawdown
	MDD[i] = 0;		//Maximum Drawdown
	
	for (j=1; j<=Horizon && i+j < BarCount; j++)
	{
		EQ[i] = (C[i+j] - C[i]) / C[i];
		MEQ[i] = Max(EQ[i], MEQ[i]);
		AE[i] = (C[i] - L[i+j]) / C[i];
		MAE[i] = Max(AE[i], MAE[i]);
		FE[i] = (H[i+j] - C[i]) / C[i];
		MFE[i] = Max(FE[i], MFE[i]);
		DD[i] = MEQ[i] - EQ[i];
		MDD[i] = Max(DD[i], MDD[i]);
		
	}

}


Filter = 1;

AddColumn(O, "Open", 10.6);
AddColumn(H, "High", 10.6);
AddColumn(L, "Low", 10.6);
AddColumn(C, "Close", 10.6);

AddColumn(Indic, "AdvDecLine", 10.6);
AddColumn(Horizon, "Horizon", 10.0);
AddColumn(EQ, "Equity", 10.6);
AddColumn(MEQ, "Max Equity", 10.6);
AddColumn(DD, "Drawdown", 10.6);
AddColumn(MDD, "Max DD", 10.6);
AddColumn(FE, "Fav Excur", 10.6);
AddColumn(MFE, "Max Fav Excur", 10.6);
AddColumn(AE, "Adv Excur", 10.6);
AddColumn(MAE, "Max Adv Excur", 10.6);


Testing Exit Strategies

Code for testing different exit strategies for a system that uses ZZ bottoms for entry.

Note that this system can’t be traded as there is future leak. ZZ bottoms can only be determined a few bars after it occurs. Nonetheless, this code allows us to test which exit strategy works better if we can enter at the exact bottom. It also allows us to analyse what happens if we are a few days early or late.

Exit strategies include

  1. Moving average
  2. RSI
  3. Z Score
  4. Holding period
  5. First Profitable open
  6. Profit target
  7. Trailing exit

Refer to https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Chapter_6_%E2%80%93_Exits.

//Copied or Adapted from Mean Reversion Trading Systems by Howard Bandy
// Zig zag bottom with different exits
// Enter a few days before or after a zigzag bottom. Test alternative exits.

SetOption("ExtraColumnsLocation", 1);
SetOption("CommissionMode", 2); 	//$ per trade
SetOption("CommissionAmount", 5);
SetOption("InitialEquity", 100000);
SetPositionSize(10000, spsValue);
MaxPos = 1;
SetOption("MaxOpenPositions", MaxPos);
SetTradeDelays(0,0,0,0);
BuyPrice = Close;
SellPrice = Close;
//ObFn == K-ratio, CAR/MDD, expectancy

ZZPercent = 1.2; //Optimize("ZZPercent", 1.2, 1, 11, 1);
ZZ = Zig(C, ZZPercent);
ZZBottom = (ZZ < Ref(ZZ, -1)) AND (ZZ < Ref(ZZ, 1));

NumberBottoms = Sum(ZZBottom, 252);

//Entry offset is the number of days between entry and the zz bottom.
//A negative value means the bottom was in the past -- the entry is late

EntryOffset = 0; //EntryOffset = Optimize("EntryOffset", 0, -3, 3, 1);

/****************************** ENTRIES AND EXITS ******************************/
/*

//////////////////////////////// Moving Average Exit ////////////////////////////////

//The length of the lookback for the average
ExitMALength = Optimize("ExitMALength", 2, 2, 10, 1);

//The type of average
ExitMethod = Optimize("ExitMethod", 1, 1, 3, 1);

switch(ExitMethod)
{

	case 1:
		//Simple moving average
		ExitMA = MA(C, ExitMALength);
		Sell = Cross(C, ExitMA);
		break;
		
	case 2:
		//Exponential moving average
		ExitMA = EMA(C, ExitMALength);
		Sell = Cross(C, ExitMA);
		break;
		
	case 3:
		//Adaptive moving average
		ExitMA = AMA(C, ExitMALength);
		Sell = Cross(C, ExitMA);
		break;
		
	default:
		ExitMA = MA(C, ExitMALength);
		Sell = Cross(C, ExitMA);
		break;
}

//Entry
Buy = Ref(ZZBottom, EntryOffset) AND C < ExitMA;



//////////////////////////////// RSI Exit ////////////////////////////////

//The length of the lookback for the RSI
ExitRSILookback = Optimize("ExitRSILookback", 2, 2, 8, 1);
RSIValue = RSIa(C, ExitRSILookback);

//The RSI exit level
ExitRSILevel = Optimize("ExitRSILevel", 50, 0, 100, 1);

//Entry
Buy = Ref(ZZBottom, EntryOffset) AND RSIValue < ExitRSILevel;

//Exit
Sell = Cross(RSIValue, ExitRSILevel);

*/

//////////////////////////////// Z score Indicator Exit ////////////////////////////////

// The length of the lookback for the z score
ExitZScoreLookback = Optimize("ExitZScoreLookback", 2, 2, 20, 1);
ZScore = (C - MA(C, ExitZScoreLookback)) / StDev(C, ExitZScoreLookback);

//z score level
ExitZScoreLevel = 0.0;	//Optimize("ExitZScoreLevel", 0, -1, 2, 0.1);

//Entry
Buy = Ref(ZZBottom, EntryOffset) AND ZScore < ExitZScoreLevel;

//Exit
Sell = Cross(ZScore, ExitZScoreLevel);

//////////////////////////////// Holding Period Exit ////////////////////////////////

//Entry
Buy = Ref(ZZBottom, EntryOffset);

//Exit
Sell = 0;
useClose = 0; //Optimize("useClose", 1, 0, 1, 1);
if (useClose == 1)
	SellPrice = Close;
else
	SellPrice = Open;

HoldDays = 2;	//Optimize("HoldDays", 2, 1, 20, 1); (Set holding period)
ApplyStop(stopTypeNBar, stopModeBars, HoldDays);

//////////////////////////////// First Profitable Open Exit ////////////////////////////////

//Entry
Buy = Ref(ZZBottom, EntryOffset);
EntryPrice = ValueWhen(Buy, BuyPrice);
ProfitableOpen = Open > EntryPrice;

//Exit
Sell = ProfitableOpen;
SellPrice = Open;

//////////////////////////////// Profit Target ////////////////////////////////

//Entry
Buy = Ref(ZZBottom, EntryOffset);

//Exit
Sell = 0;

useATR = 0;	//Optimize("useATR", 1, 0, 1, 1);

if (useATR)
{
	ATRMult = Optimize("ATRMult", 1.0, 0.2, 3.0, 0.2);
	ATRLookback = Optimize("ATRLookback", 5, 1, 20, 1);
	ProfitTarget = ATRMult * ATR(ATRLookback);

}else
{
	ProfitTarget = Optimize("ProfitTarget", 1.0, 0.5, 11.0, 0.5);	//profit target in percentage
}

ApplyStop(stopTypeProfit, stopModePercent, ProfitTarget);

//////////////////////////////// Trailing Exit ////////////////////////////////

useATR = 0;	//Optimize("useATR", 1, 0, 1, 1);

if (useATR)
{
	ATRMult = Optimize("ATRMult", 1.2, 0.2, 3.0, 0.2);
	ATRLookback = Optimize("ATRLookback", 6, 1, 20, 1);
	TrailPoints = ATRMult * ATR(ATRLookback);
	ApplyStop(stopTypeTrailing, stopModePercent, TrailPoints);
}else
{

	TrailPercent = 1.0;
	ApplyStop(stopTypeTrailing, stopModePercent, TrailPercent);
}

  
//////////////////////////////// PLOTS ////////////////////////////////
Plot(C, "C", colorBlack, styleCandle);
Plot(ZZ, "ZigZag", colorBlue, styleLine|styleThick);

 

Filter Testing

Includes code for testing an ATR filter and a moving average filter.

This program also demonstrates how we can convert an indicator into a linear like distribution using the PercentRank() function. This technique can also be used to convert an indicator and test if it performs as desired.

Refer to

// ATRFilter.afl
// Copied or Adapted from Mean Reversion Trading Systems by Howard Bandy
//Test a range of settings of ATR length and ATR level to see if a mean reversion system benefits from using it as a filter to allow or block trades

SetOption("InitialEquity", 100000);
MaxPos = 1;
SetOption("MaxOpenPositions", MaxPos);
SetPositionSize(10000, spsValue);

SetOption("ExtraColumnsLocation", 1);

////////////////// ATR Filter ////////////////////////////////
ATRLB = Optimize("ATRLB", 8, 2, 10, 1);
ATRValueLowerLimit = Optimize("ATRValueLowerLimit", 60, 0, 95, 5);

ATRValue = ATR(ATRLB);
ATRValuePR = PercentRank(ATRValue, 100);		//To transform ATR into a linear like distribution with values between 0 and 100
ATRFilter = ATRValuePR >= ATRValueLowerLimit AND ATRValuePR <= ATRValueLowerLimit + 5;		//ATRValuePR from 0 to 5, 5 to 10, 10 to 15 etc

RSI2 = RSI(2);

//Buy = RSI2 < 25;
Buy = ATRFilter and RSI2 < 25;
Sell = RSI2 > 75;


/////////////////// Moving Average Filter /////////////////////

BuyPrice = SellPrice = Close;

// User functions

function ComputeFib(n)
{

	//The function ComputeFib accepts an integer n, computes the nth Fibonacci number, and returns that value
	
	if (n <= 1)
		Fib = 1;
	else{
		f[0] = 1;
		f[1] = 1;
		
		for (j = 2; j<= n; j++)
			f[j] = f[j-1] + f[j-2];
		
		Fib = f[n];
	}
	
	return (Fib);
}

// Parameters 
// To cover a wide range of moving average lengths without performing a lot of test runs, the Fibonacci numbers are used as lookback lengths

Fibn = Optimize("Fibn", 6, 1, 12, 1);
LB = ComputeFib(Fibn);
FilterMA = MA(C, LB);

//Indicators
RSI2 = RSI(2);
FilterPass = C >= FilterMA;

//Signals
Buy = RSI2 < 25 AND FilterPass;
Sell = RSI2 > 75; 

Others

Anticipating Signals using a Binary Search

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_13_Anticipating_Signals.

//GenericBinarySearch.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 192

Length1 = 1;
Length2 = 36;
AccuracyTarget = 0.0001;

/* 
A cross occurs when A-B goes from positive to zero to negative, or vice versa.
Therefore, if we can calculate the value of A (or B) when A-B goes to zero, we can anticipate the price of A that is needed for a crossover.
For instance, if we know that when A = 12, A-B=0, we can infer that when A = 12.1, A-B>0 (indicating a positive crossover).
*/

function ZeroToFind(P)
{
	
	FTZ = MA(P, Length1) - MA(P, Length2);	//Modify this statement for other crossovers (e.g. Cross between RSI and the value 20)
	return FTZ;
}


//To determine the value of A when A-B=0, we can do a binary search

BC = LastValue(BarIndex());		//BC is the index of the final bar in the existing array

TF = Ref(C, 1);		//TF is the temp array used for the calculations. It stores the value of the close one bar in the future. The code assumes that the variable being searched is the closing price. 

TF[BC] = HGuess = TF[BC-1] * 10;	//TF for the final bar is empty because there is no future bar available. Set TF for the final bar to be 10 times TF for the second last bar. This serves as the high guess for the binary search.

HSign = IIf(LastValue(ZeroToFind(TF))>0, 1, -1);	//Get the sign associated with HGuess.

TF[BC] = LGuess = TF[BC-1] * 0.1;	//Set TF for the final bar to be 0.1 times TF for the second last bar. This serves as the lower guess for the binary search.

LSign = IIf(LastValue(ZeroToFind(TF))>0, 1, -1);	//Get the sign associated with LGuess

/* If the signs of HGuess and LGuess are the same, there is no zero in between them.
Set the return value to zero and return.
Otherwise, loop through the binary search.
*/

if (HSign == LSign)
{
	HGuess = 0.0;
}else
{
	while(abs(HGuess - LGuess) > AccuracyTarget)
	{
		MGuess = (HGuess + LGuess)/2;
		TF[BC] = MGuess;
		
		MSign = IIf(LastValue(ZeroToFind(TF))>0, 1, -1);
		HGuess = IIf(HSign == MSign, MGuess, HGuess);
		LGuess = IIf(LSign == MSign, MGuess, LGuess);		
	}
}


//When the loop finishes, HGuess and LGuess will be very close togethr. Either one is an acceptable value for our purpose.

Filter = BarIndex() == BC;
AddColumn(HGuess, "Zero If Close is: ", 1.9);

Automatic Stock Rotation Based on Position Score

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_15_Rotation.

//Rotation.afl
//Adapted from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 209

EnableRotationalTrading();		//This function is now marked as obsolete. Use SetBacktestMode( backtestRotational ) in new formulas.

NumberHeld = 2; //The number of issues to hold at a time

PositionSize = -100/NumberHeld;		//Allocate funds equally among all the issues

NumberExtras = 3;
WorstRank = NumberHeld + NumberExtras;
SetOption("WorstRankHeld", WorstRank);   //Set WorstRankHeld to be some number greater than the number of positions held

LookBack = 5;		//The lookback period for the Rate of Change indicator
UpDown = 2; 	 	//UpDown allows the ROC to be inverted (treat a rising ROC as a "Sell" signal
AllowShort = 2; 	//1 = Allow, 2 = Do not allow

//Compute a score based on the recent Rate of Change of the closing price
Multiplier = IIf(UpDown==1, 1, -1);
Score = Multiplier*ROC(C, LookBack);
Score = IIf(AllowShort == 1, Score, Max(Score, 0));
PositionScore = Score;

Comparing Arrays and Getting the Maximum Value

The example below calculates the strengths of the S&P 500 index, XLE, XLK, and XLV and uses the function VarGetMax() to compare the four arrays.

The code for VarGetMax() is taken from https://forum.amibroker.com/t/getting-maximum-of-6-arrays/10642/5, credit to fxshrat.

StrengthLookBack = 20;

SetForeign("SP500");
Filters = Ref(MA(C, 50), -1) > Ref(MA(C, 200), -1);
StrengthOneMth = Ref(C, -1)/Ref(C, -1*StrengthLookBack);
StrengthThreeMth = Ref(C, -1)/Ref(C, -3*StrengthLookBack);

SetForeign("XLE");
XLEStrength1 = Ref(C, -1)/Ref(C, -1*StrengthLookBack);

SetForeign("XLK");
XLKStrength1 = Ref(C, -1)/Ref(C, -1*StrengthLookBack);

SetForeign("XLV");
XLVStrength1 = Ref(C, -1)/Ref(C, -1*StrengthLookBack);

RestorePriceArrays();

function VarGetMax( varname, num ) {
    local n, maxall;
    maxall = -1e9;
    for ( n = 1; n <= num; n++ )
        maxall = Max( maxall, VarGet( varname + n ) );
    return maxall;
}

// Assign the arrays for comparing to the variables var1, var2, var3 etc (variables must be named as var*, where * is a running sequence of integers)
var1 = XLKStrength1;
var2 = XLEStrength1;
var3 = XLVStrength1;
var4 = StrengthOneMth;

// Overall maximum n variables
getmax = VarGetMax( "var", n = 4 );

// If XLVStrength1 is maximum, Strong = 1, else Strong = 0
Strong = IIf(XLVStrength1 == getmax, 1, 0);

Testing Seasonality Patterns

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_11_Seasonality_Systems.

//TestingSeasonality.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 155 to Pg 161

SetTradeDelays(0,0,0,0);
BuyPrice = Close;
SellPrice = Close;

model = Param("Model", 0, 1, 2);

if (model == 0)
{
	//Testing which day of the month produces the most profit
	DayToBuy = Optimize("DTB", 1, 1, 31, 1);
	Buy = DayToBuy == Day();
} else if (model == 1)
{
	//Testing which day surrounding the first day of the month produces the most profit
	DOI = Month()!=Ref(Month(), -1);
	Minus8 = Ref(DOI, 8);
	Daynumber = -8 + BarsSince(Minus8);

	DayToBuy = Optimize("day To Buy", 0, -8, 8, 1);
	Buy = Daynumber == DayToBuy;
}else 
{
	//Testing which day surrounding options expiration day produces the most profit
	DOI = (DayOfWeek() == 5) AND ((Day() >=15) AND (Day() <= 21));
	Minus8 = Ref(DOI, 8);
	Daynumber = -8 + BarsSince(Minus8);

	DayToBuy = Optimize("Day To Buy", 0, -8, 8, 1);
	Buy = Daynumber == DayToBuy;
}


//Sell on the close of the next trading day
Sell = BarsSince(Buy) >= 1;

Plotting the Equity Curve

e = Equity();
Maxe = LastValue(Highest(e));

Plot(e, "Equity Curve", colorBlue, styleLine|styleOwnScale, 0, Maxe);

Export Data Series

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_14_Sector_Analysis.

//ExportDataSeries.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 196

/* 
To use this file, open the file in Automatics Analysis. 
Next, open the file in AFL Formula Editor and click the Verify Syntax icon.
A file named Exported.csv will be written to the directory specified as argument to the fopen() function. 
*/

//Change the path below to the path you want the file to be exported to
fh = fopen("C:\\Users\\<username>\\Desktop\\exported.csv", "w");


if(fh)
{
	fputs("Date,Open,High,Low,Close,Volume\n", fh);
	y = Year();
	m = Month();
	d = Day();
	
	for (i=0; i<BarCount; i++)
	{
		ds = StrFormat("%04.0f-%02.0f-%02.0f,",y[i],m[i],d[i]);
		
		fputs(ds, fh);
		
		qs = StrFormat("%.4f,%.4f,%.4f,%.4f,%.0f\n", O[i],H[i],L[i],C[i],V[i]);
		
		fputs(qs, fh);
		
	}

	fclose(fh);
}

Pattern Systems

Refer to https://smarttradingstrategies.com/quantitative-trading-systems-2nd-edition-by-howard-b-bandy/#Chapter_12_Pattern_Systems.

//PatternDailyClose.afl
//Taken from Quantitative Trading System (2nd Edition) by Howard Bandy Pg 174

SetTradeDelays(0,0,0,0);
BuyPrice = Close;
SellPrice = Close;

PctC = (C - Ref(C, -1))/C; 	//Calculate the daily percentage returns

Cutoff = 15;

TopGroup = PctC >= Percentile(PctC, 60, 100-Cutoff);		//Define criteria for a day to be classified in the top group (percentage return is in the top percentile of the past 60 days' returns)
BottomGroup = PctC <= Percentile(PctC, 60, Cutoff);			//Define criteria for a day to be classified in the bottom group (percentage return is in the bottom percentile of the past 60 days' returns)
MiddleGroup = (NOT TopGroup) AND (NOT BottomGroup);

Position = IIf(TopGroup, 3, IIf(MiddleGroup, 2, 1));		//Label the position for each day (1 = BottomGroup, 2 = MiddleGroup, 3 = TopGroup)

//For a single identifier for each bar for its three day sequence 
//For instance, if the daily return is in the top group for today, yesterday and the day before, sequence = 333

Sequence = 100 * Ref(Position, -2) + 10 * Ref(Position, -1) + Position;

/* These AddColumn statements display the categories each day is assigned to, 
and help verify that the program works as it should.
Run as an Exploration with Current Symbol and Last Days set to 100.
*/

Filter = 1;
AddColumn(C, "C", 1.9);
AddColumn(PctC, "PctC", 1.9);
AddColumn(TopGroup, "TopGroup", 1.0);
AddColumn(MiddleGroup, "MiddleGroup", 1.0);
AddColumn(BottomGroup, "BottomGroup", 1.0);
AddColumn(Position, "Position", 1.0);
AddColumn(Sequence, "Sequence", 1.0);

// Create a set of optimizations that will cycle through all the possible sequences
TwoDaysAgo = Optimize("TwoDaysAgo", 1, 1, 3, 1);
OneDayAgo = Optimize("OneDayAgo", 1, 1, 3, 1);
ThisDay = Optimize("ThisDay", 1, 1, 3, 1);

Selected = 100 * TwoDaysAgo + 10 * OneDayAgo + ThisDay;

Buy = Selected == Sequence;
Sell = BarsSince(Buy) >= 1;