I've figured out how to back test a four legged options trading strategy, specifically selling iron butterflies or iron condors. The AmiBroker AFL code is below.
I welcome any questions, comments, suggestions, fixes, or improvements. Warm regards, David // This option trading strategy is not intended to make money. // It is intended to demonstrate the use of AmiBroker (AB) for back testing option trading strategies. // This code builds option contract symbols from the underlyings, expirations, option types (puts or calls), and strike prices. // The example option trading strategy contained herein is: // Every day based on end of day data, // sell to open 1 contract of an NDX front month iron butterfly or iron condor at the Mid. // On expiration, cash settle all remaining option positions. // This code assumes Interactive Brokers (IB) symbology and data, and uses Close instead of Mid because IB doesn't supply end of day Mid or Bid/Ask data. // To produce results, you need to load IB historical end of day data for NDX-NASDAQ-IND and its put and call options, // such as NDX 100619P01975000-SMART-OPT where the underlying root is padded with spaces to 6 characters. // The commented out Plot and _TRACE statements can be uncommented for debugging purposes. // To do longer term back testing, you would need to obtain lengthier historical options data. Some sources are listed below. // Please verify this information yourself. It was gathered from their sales reps in May 2010. /* LiveVol: trade and 1 min bid/ask, starting 1/04 TickData: tick bid/ask and 1 sec close, starting 7/04 iQFeed: trade bid/ask/last, starting 1 month ago; 1 min bid/ask/last, starting 5/07 eSignal: tick bid/ask starting up to 60 days ago, bar bid/ask starting up to 120 days ago, day bid/ask starting up to 1 year ago OptionVue: 30 min bid/ask, starting 1/2/01 Ivy DB: day bid/ask, starting 1/96 iVolatility: day bid/ask, starting 11/1/00 HistoricalOptionData: day bid/ask/last, starting 2/02 Stricknet: day bid/ask/last, starting 1/2/03 */ SetFormulaName("Example of four legged option trading strategy - selling iron butterflies or iron condors"); /* Setting StrikesFromAtmToShort = 0 sells iron butterflies. Setting StrikesFromAtmToShort > 0 sells iron condors. */ StrikesFromAtmToShort = Param("StrikesFromATMToShort", 0, 0, 10, 1, sincr = 0); StrikesFromShortToLong = Param("StrikesFromShortToLong", 1, 1, 10, 1, sincr = 0); DebugView = ParamToggle("DebugView", "OFF|ON", defaultval = 0); /* Controls all debug output */ RoundLotSize = 100; CommissionPerSharePerLeg = 0.01; Underlying = "NDX-NASDAQ-IND"; SetBarsRequired( sbrAll, sbrAll ); /* Uses all bars, NOT any specified subset. Turns off QuickAFL to ensure proper behavior of BarIndex(). */ SetBacktestMode( backtestRegularRawMulti ); SetOption( "UseCustomBacktestProc", False ); //SetOption( "PortfolioReportMode", 0 ); //SetOption( "Generate", 1 ); SetOption( "InitialEquity", 1000000 ); SetOption( "AccountMargin", 100 ); SetOption( "UsePrevBarEquityForPosSizing", True ); SetOption( "MinPosValue", 0 ); SetOption( "MaxOpenPositions", 1000 ); SetOption( "MinShares", 100 ); SetOption( "PriceBoundChecking", False ); SetOption( "CommissionMode", 3 ); SetOption( "CommissionAmount", 0 ); SetOption( "InterestRate", 0); SetOption( "AllowPositionShrinking", False ); SetOption( "DisableRuinStop", True ); SetOption( "ActivateStopsImmediately", False ); SetOption( "AllowSameBarExit", False ); SetOption( "NoDefaultColumns", False ); SetOption( "FuturesMode", False ); SetOption( "WorstRankHeld", 10 ); SetOption( "ReverseSignalForcesExit", False ); SetOption( "EveryBarNullCheck", False); SetOption( "HoldMinBars", 0 ); SetOption( "EarlyExitBars", 0 ); SetOption( "EarlyExitFee", 0 ); SetOption( "HoldMinDays", 0 ); SetOption( "EarlyExitDays", 0 ); SetOption( "SeparateLongShortRank", True ); SetOption( "MaxOpenLong", 0 ); SetOption( "MaxOpenShort", 0 ); SetOption( "RefreshWhenCompleted", True ); SetOption( "RequireDeclarations", False ); SetOption( "ExtraColumnsLocation", 0 ); function ConvertDateFromAmiBrokerToOPRA(DateNumber) { _ConvertDateFromAmiBrokerToOPRA = IIf(DateNumber > 1000000, DateNumber - 1000000, DateNumber); return _ConvertDateFromAmiBrokerToOPRA; } function DaysInMonth(MonthNum, YearNum) { _DaysInMonth = IIf(MonthNum == 1 OR MonthNum == 3 OR MonthNum == 5 OR MonthNum == 7 OR MonthNum == 8 OR MonthNum == 10 OR MonthNum == 12, 31, 30); DaysInMonthFeb = IIf((YearNum % 4 == 0 AND YearNum % 100 != 0) OR (YearNum % 4 == 0 AND YearNum % 400 == 0), 29, 28); _DaysInMonth = IIf(MonthNum == 2, DaysInMonthFeb, _DaysInMonth); return _DaysInMonth; } function DaysToThirdFriday() { Dy = Day(); WeekDay = DayOfWeek(); DaysToFriday = IIf(5 - WeekDay < 0, (12 - WeekDay) % 7, (5 - WeekDay) % 7); ThirdFriday = ((Dy + DaysToFriday) % 7) + 14; ThirdFriday = IIf(ThirdFriday == 14, 21, ThirdFriday); /* Adjusts for the first of the month being a Saturday. */ _DaysToThirdFriday = ThirdFriday - Dy; _DaysToThirdFriday = IIf(_DaysToThirdFriday >= 0, _DaysToThirdFriday, ThirdFriday + IIf(ThirdFriday + 14 > DaysInMonth(Month(), Year()), 28, 35) - Dy); return _DaysToThirdFriday; } //Plot(DaysToThirdFriday() * 100, "DaysToThirdFriday()", colorBlack, styleLine); //Plot((ConvertDateFromAmiBrokerToOPRA(DateNum()) + DaysToThirdFriday()) / 100, "Next 3rd Friday", colorRed, styleLine); function FirstMonthExpiration() { _FirstMonthExpiration = ConvertDateFromAmiBrokerToOPRA(DateNum()) + DaysToThirdFriday() + 1; /* OPRA symbols use Saturday after third Friday as monthly expiration. */ Yr = floor( _FirstMonthExpiration / 10000 ); MonthDay = _FirstMonthExpiration - Yr * 10000; Mon = floor( MonthDay / 100 ); Dy = MonthDay - Mon * 100; MonthDay = IIf( MonthDay > Mon * 100 + DaysInMonth(Mon, Yr) AND MonthDay < (Mon + 1) * 100, (Mon + 1) * 100 + Dy - DaysInMonth(Mon, Yr), MonthDay); Yr = IIf( MonthDay >= 1300, Yr + 1, Yr); MonthDay = IIf( MonthDay >= 1300, MonthDay - 1200, MonthDay); _FirstMonthExpiration = Yr * 10000 + MonthDay; return _FirstMonthExpiration; } //Plot(FirstMonthExpiration() / 100, "FirstMonthExpiration()", colorBlack, styleLine); UnderlyingRoot = StrLeft(Underlying, StrFind(Underlying, "-") - 1); while(StrLen(UnderlyingRoot) < 6) UnderlyingRoot = UnderlyingRoot + " "; switch(UnderlyingRoot) { case "NDX ": StrikeIncrement = 25; SlippagePerSharePerLeg = 0.025; SettlementSymbol = "NDS-CBOE-IND"; break; case "SPX ": StrikeIncrement = 5; SlippagePerSharePerLeg = 0.050; SettlementSymbol = "SET-CBOE-IND"; break; default: StrikeIncrement = 0; SlippagePerSharePerLeg = 0.000; SettlementSymbol = "error"; break; }; Expiration = FirstMonthExpiration(); //Plot(Expiration / 100, "Expiration ", colorOrange, styleLine); SetForeign(Underlying, fixup = True, tradeprices = False); UnderlyingPrice = Close; // Could add code here to determine trend or other underlying-based indicators. RestorePriceArrays(); //Plot (UnderlyingPrice, Underlying + " UnderlyingPrice", colorRed, styleLine); //if( DebugView ) _TRACE("# UnderlyingPrice = " + NumToStr(UnderlyingPrice, 1.2, separator = True) + " (" + typeof(UnderlyingPrice) + ")" ); AtmStrike = round( UnderlyingPrice / StrikeIncrement ) * StrikeIncrement; //Plot (AtmStrike ,"AtmStrike", colorBlack, styleLine); UnderlyingToEvaluate = StrMid(Name(),0,6); ExpirationToEvaluate = IIf(UnderlyingToEvaluate == UnderlyingRoot, StrToNum(StrMid(Name(),6,6)), 0); /* Calculate only if an option is being evaluated. */ OptionTypeToEvaluate = IIf(UnderlyingToEvaluate == UnderlyingRoot, IIf(StrMid(Name(),12,1) == "P", 1, 2), 0); /* Calculate only if an option is being evaluated. */ /* 1 = put. 2 = call. */ StrikeToEvaluate = IIf(UnderlyingToEvaluate == UnderlyingRoot, StrToNum(StrMid(Name(),13,8)) / 1000, 0); /* Calculate only if an option is being evaluated. */ OptionType = 1; /* 1 = put. 2 = call. */ ShortPutStrike = AtmStrike - StrikeIncrement * StrikesFromAtmToShort; ShortPutFound = IIf(UnderlyingToEvaluate == UnderlyingRoot, True, False) AND IIf(ExpirationToEvaluate == Expiration, True, False) AND IIf(OptionTypeToEvaluate == OptionType, True, False) AND IIf(StrikeToEvaluate == ShortPutStrike, True, False); //if( DebugView ) _TRACE("# UnderlyingToEvaluate = " + UnderlyingToEvaluate + " from " + Name() ); //Plot(UnderlyingPrice, UnderlyingRoot, colorBlack, styleLine); //Plot(StrikeToEvaluate, "StrikeToEvaluate", colorGreen, styleLine); //Plot(ExpirationToEvaluate, "ExpirationToEvaluate", colorGreen, styleLine); //Plot(Expiration, "Expiration", colorRed, styleLine); //Plot(ShortPutFound, "ShortPutFound", colorBlack, styleDots); //Plot(OptionTypeToEvaluate, "OptionTypeToEvaluate", colorBlack, styleLine); //Plot(OptionType, "OptionType", colorBlack, styleLine); //Plot(ShortPutStrike, "ShortPutStrike", colorBlack, styleLine); OptionType = 2; /* 1 = put. 2 = call. */ ShortCallStrike = AtmStrike + StrikeIncrement * StrikesFromAtmToShort; ShortCallFound = IIf(UnderlyingToEvaluate == UnderlyingRoot, True, False) AND IIf(ExpirationToEvaluate == Expiration, True, False) AND IIf(OptionTypeToEvaluate == OptionType, True, False) AND IIf(StrikeToEvaluate == ShortCallStrike, True, False); //Plot(ShortCallFound, "ShortCallFound", colorBlack, styleDots); //Plot(OptionTypeToEvaluate, "OptionTypeToEvaluate", colorBlack, styleLine); //Plot(OptionType, "OptionType", colorRed, styleLine); //Plot(ShortCallStrike, "ShortCallStrike", colorBlack, styleLine); //if( DebugView ) _TRACE("# UnderlyingRoot = " + UnderlyingRoot); ForceExpiration = False; /* True means force settlment at expiration behavior for testing purposes */ Short = ShortPutFound OR ShortCallFound; //PlotColor = IIf( Short == 0, colorRed, colorGreen ); Plot(Short + 100, "Short", PlotColor, style = styleDots + styleNoLine); ShortPrice = Close - SlippagePerSharePerLeg - CommissionPerSharePerLeg; /* Close should be Mid (average of Bid and Ask), but IB doesn't supply that data end of day. */ //Plot(ShortPrice, "ShortPrice", colorBlack, styleLine); PositionSize = ShortPrice * 100 + 0.01; /* Perhaps due to AB's rounding, the additional penny is required to generate a trade every day. */ Cover = ConvertDateFromAmiBrokerToOPRA(DateNum()) > ExpirationToEvaluate /* Cash settlement is calculated on the first trading day after Saturday expiration. */ OR ForceExpiration; //PlotColor = IIf( Cover == 0, colorRed, colorGreen ); Plot(Cover, "Cover", PlotColor, style = styleDots + styleNoLine); CoverSettlementPrice = Foreign(SettlementSymbol, "Close", fixup = 1); //Plot(CoverSettlementPrice, "CoverSettlementPrice", colorBlack, styleLine); CoverSettlement = IIf(OptionTypeToEvaluate == 1, /* 1 = put. 2 = call. */ Max(StrikeToEvaluate - CoverSettlementPrice, 0), /* put settlement */ Max(CoverSettlementPrice - StrikeToEvaluate, 0) ); /* call settlement */ //Plot(CoverSettlement, "CoverSettlement", colorBlack, styleLine); CoverPrice = CoverSettlement + CommissionPerSharePerLeg * sign(CoverSettlement); //Plot(CoverPrice, "CoverPrice", colorBlack, styleLine); OptionType = 1; /* 1 = put. 2 = call. */ LongPutStrike = AtmStrike - StrikeIncrement * StrikesFromAtmToShort - StrikeIncrement * StrikesFromShortToLong; LongPutFound = IIf(UnderlyingToEvaluate == UnderlyingRoot, True, False) AND IIf(ExpirationToEvaluate == Expiration, True, False) AND IIf(OptionTypeToEvaluate == OptionType, True, False) AND IIf(StrikeToEvaluate == LongPutStrike, True, False); //if( DebugView ) _TRACE("# UnderlyingToEvaluate = " + UnderlyingToEvaluate + " from " + Name() ); //Plot(UnderlyingPrice, UnderlyingRoot, colorBlack, styleLine); //Plot(StrikeToEvaluate, "StrikeToEvaluate", colorGreen, styleLine); //Plot(ExpirationToEvaluate, "ExpirationToEvaluate", colorGreen, styleLine); //Plot(Expiration, "Expiration", colorRed, styleLine); //Plot(LongPutFound, "LongPutFound", colorBlack, styleDots); //Plot(OptionTypeToEvaluate, "OptionTypeToEvaluate", colorBlack, styleLine); //Plot(OptionType, "OptionType", colorBlack, styleLine); //Plot(LongPutStrike, "LongPutStrike", colorBlack, styleLine); OptionType = 2; /* 1 = put. 2 = call. */ LongCallStrike = AtmStrike + StrikeIncrement * StrikesFromAtmToShort + StrikeIncrement * StrikesFromShortToLong; LongCallFound = IIf(UnderlyingToEvaluate == UnderlyingRoot, True, False) AND IIf(ExpirationToEvaluate == Expiration, True, False) AND IIf(OptionTypeToEvaluate == OptionType, True, False) AND IIf(StrikeToEvaluate == LongCallStrike, True, False); //Plot(LongCallFound, "LongCallFound", colorBlack, styleDots); //Plot(OptionTypeToEvaluate, "OptionTypeToEvaluate", colorBlack, styleLine); //Plot(OptionType, "OptionType", colorRed, styleLine); //Plot(LongCallStrike, "LongCallStrike", colorBlack, styleLine); //if( DebugView ) _TRACE("# UnderlyingRoot = " + UnderlyingRoot); Buy = LongPutFound OR LongCallFound; //PlotColor = IIf( Buy == 0, colorRed, colorGreen ); Plot(Buy + 100, "Buy", PlotColor, style = styleDots + styleNoLine); BuyPrice = Close + SlippagePerSharePerLeg + CommissionPerSharePerLeg; /* Close should be Mid (average of Bid and Ask), but IB doesn't supply that data end of day. */ //Plot(BuyPrice, "BuyPrice", colorBlack, styleLine); PositionSize = BuyPrice * 100 + 0.01; /* Perhaps due to AB's rounding, the additional penny is required to generate a trade every day. */ Sell = ConvertDateFromAmiBrokerToOPRA(DateNum()) > ExpirationToEvaluate /* Cash settlement calculated first trading day after Saturday expiration. */ OR ForceExpiration; //PlotColor = IIf( Sell == 0, colorRed, colorGreen ); Plot(Sell, "Sell", PlotColor, style = styleDots + styleNoLine); SellSettlementPrice = Foreign(SettlementSymbol, "Close", fixup = 1); //Plot(SellSettlementPrice, "SellSettlementPrice", colorBlack, styleLine); SellSettlement = IIf(OptionTypeToEvaluate == 1, /* 1 = put. 2 = call. */ Max(StrikeToEvaluate - SellSettlementPrice, 0), /* put settlement */ Max(SellSettlementPrice - StrikeToEvaluate, 0) ); /* call settlement */ //Plot(SellSettlement, "SellSettlement", colorBlack, styleLine); SellPrice = SellSettlement - CommissionPerSharePerLeg * sign(SellSettlement); //Plot(SellPrice, "SellPrice", colorBlack, styleLine);