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
- Mean Reversion Trading Systems by Howard Bandy
- Quantitative Trading Systems (2nd Edition) by Howard B. Bandy
- Understanding Amibroker’s ExRem() and Flip() functions
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
- Market on Close
- Next Day Open
- Limit Order
- Stop Order
//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
//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
- Moving average
- RSI
- Z Score
- Holding period
- First Profitable open
- Profit target
- Trailing exit
//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
- https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Performance_of_An_Indicator
- https://smarttradingstrategies.com/mean-reversion-trading-systems-by-howard-bandy/#Using_Filters
// 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
//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
//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
//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
//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
//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;