//@Name:Cross Finder (periods ago) //@Description:Identifies a number of classic technical crosses over any number of periods. //@Returns:Number //@Width:90 //@Env:Production //@Update:Periodic,30 // Care has been taken in preparing this code but it is provided without guarantee. // You are welcome to modify and extend it. Please add your name as a modifier if you distribute it. var var1 = 13; var var2 = 26; var var3 = 9; var var4 = 3; var crossType = 0; var MAtype1 = 0; var MAtype2 = 0; var dataSource = 15; var crossList = ["Price above MA","Price below MA","Golden Cross","Dead Cross","MACD above Signal","MACD below Signal", "Stoch.Osc. above Signal","Stoch.Osc. below Signal","Stoch.Osc. above Level","Stoch.Osc. below Level","DI+ above DI-","DI+ below DI-", "Momentum above level","Momentum below level","ADX above level","ADX below level","RSI above level","RSI below level", "PosVolIndex above Signal","PosVolIndex below signal","NegVolIndex above Signal","NegVolIndex below signal", "%b above level","%b below level","ZLTCD above Signal","ZLTCD below Signal","MACD above Zero","MACD below Zero", "MFI above MFI","MFI below MFI","CCI above level","CCI below level","WilliamsA/D above Signal","WilliamsA/D below Signal", "DI+ above level","DI+ below level","DI- above level","DI- below level"]; var maList = ["Simple","Exponential","Weighted","Triangular","VariableVHF","VariableCMO","VIDYA","Hull","TEMA","VWAP"]; var maTitleList = ["SMA","EMA","WMA","TMA","VVHF","VCMO","VIDYA","HMA","TEMA","VWAP"]; var dataList = ["1m","2m","2.5m","3m","4m","5m","6m","10m","15m","20m","30m","1h","2h","3h","4h","Daily","Weekly","Monthly"]; var useIntra = false; var periodsAgo = 5; function init(status) { if (status == Loading || status == Editing) { var store1 = restore(storage.getAt(0),[[crossType,0,37],[dataSource,0,17],[useIntra,0,1]]); MAtype1 = storage.getAt(1); MAtype2 = storage.getAt(2); //dataSource = storage.getAt(3); var4 = storage.getAt(3); var1 = storage.getAt(4); var2 = storage.getAt(5); var3 = storage.getAt(6); periodsAgo = storage.getAt(7); crossType = store1[0][0]; dataSource = store1[1][0]; useIntra = store1[2][0]; } if (status == Adding || status == Editing) { // creates the first dialog where the type of cross and the time period are chosen dlg = new Dialog("Select Cross Type", 180, 85); dlg.addOkButton(-1,-1,-1,-1,"Next >"); dlg.addCancelButton(); dlg.addDropList("DL1",8,-1,100,-1,crossList,"","",crossType); dlg.addDropList("DL3",8,-1,100,-1,dataList,"","",dataSource); dlg.addTickBox("TB1",8,50,120,-1,"Use intraday data",useIntra); dlg.addIntEdit("INT4",82,66,-1,-1,"Crossed within the last","periods",periodsAgo,1,100); //dlg.addText(8,50,144,16,"NOTE: weekly and monthly crosses will take coniderably more time to calculate than daily"); if (dlg.show()==Dialog.Cancel) return false; crossType = dlg.getValue("DL1"); useIntra = dlg.getValue("TB1"); dataSource = dlg.getValue("DL3"); periodsAgo = dlg.getValue("INT4"); storage.setAt(0, compress([[crossType,0,37],[dataSource,0,17],[useIntra,0,1]])); // creates the second dialog where the periods are set switch (crossType) { case 0: dialog1(status);break; case 1: dialog1(status);break; case 2: dialog2(status);break; case 3: dialog2(status);break; case 4: dialog3(status);break; case 5: dialog3(status);break; case 6: dialog4(status);break; case 7: dialog4(status);break; case 8: dialog9(status);break; case 9: dialog9(status);break; case 10: dialog5(status);break; case 11: dialog5(status);break; case 12: dialog6(status);break; case 13: dialog6(status);break; case 14: dialog6(status);break; case 15: dialog6(status);break; case 16: dialog6(status);break; case 17: dialog6(status);break; case 18: dialog5(status);break; case 19: dialog5(status);break; case 20: dialog5(status);break; case 21: dialog5(status);break; case 22: dialog7(status);break; case 23: dialog7(status);break; case 24: dialog8(status);break; case 25: dialog8(status);break; case 26: dialog3(status);break; case 27: dialog3(status);break; case 28: dialog10(status);break; case 29: dialog10(status);break; case 30: dialog6(status);break; case 31: dialog6(status);break; } storage.setAt(1, MAtype1); storage.setAt(2, MAtype2); storage.setAt(3, var4); storage.setAt(4, var1); storage.setAt(5, var2); storage.setAt(6, var3); storage.setAt(7, periodsAgo); } var inputList = ["(O)","(H)","(L)",""] // sets the title of the column switch (crossType) { case 0: setTitle("CrossFinder: Price above "+var1+" "+maTitleList[MAtype1]+" "+inputList[var3]+" "+dataList[dataSource]); break; case 1: setTitle("CrossFinder: Price below "+var1+" "+maTitleList[MAtype1]+" "+inputList[var3]+" "+dataList[dataSource]); break; case 2: setTitle("CrossFinder: Golden Cross "+var1+" "+maTitleList[MAtype1]+" "+var2+" "+maTitleList[MAtype2]+" "+dataList[dataSource]); break; case 3: setTitle("CrossFinder: Dead Cross "+var1+" "+maTitleList[MAtype1]+" "+var2+" "+maTitleList[MAtype2]+" "+dataList[dataSource]); break; case 4: setTitle("CrossFinder: "+var1+" "+var2+" MACD above "+var3+" signal"+" "+dataList[dataSource]); break; case 5: setTitle("CrossFinder: "+var1+" "+var2+" MACD below "+var3+" signal"+" "+dataList[dataSource]); break; case 6: setTitle("CrossFinder: "+var1+" "+var2+" Stoch.Osc. above "+var3+" signal"+" "+dataList[dataSource]); break; case 7: setTitle("CrossFinder: "+var1+" "+var2+" Stoch.Osc. below "+var3+" signal"+" "+dataList[dataSource]); break; case 8: setTitle("CrossFinder: "+var1+" "+var2+" Stoch.Osc. above "+var3+" level"+" "+dataList[dataSource]); break; case 9: setTitle("CrossFinder: "+var1+" "+var2+" Stoch.Osc. below "+var3+" level"+" "+dataList[dataSource]); break; case 10: setTitle("CrossFinder: "+var1+" ADX DI+ above DI-"+" "+dataList[dataSource]); break; case 11: setTitle("CrossFinder: "+var1+" ADX DI+ below DI-"+" "+dataList[dataSource]); break; case 12: setTitle("CrossFinder: "+var1+" Momentum above "+var2+" level"+" "+dataList[dataSource]); break; case 13: setTitle("CrossFinder: "+var1+" Momentum below "+var2+" level"+" "+dataList[dataSource]); break; case 14: setTitle("CrossFinder: "+var1+" ADX above "+var2+" level"+" "+dataList[dataSource]); break; case 15: setTitle("CrossFinder: "+var1+" ADX below "+var2+" level"+" "+dataList[dataSource]); break; case 16: setTitle("CrossFinder: "+var1+" RSI above "+var2+" level"+" "+dataList[dataSource]); break; case 17: setTitle("CrossFinder: "+var1+" RSI below "+var2+" level"+" "+dataList[dataSource]); break; case 18: setTitle("CrossFinder: PosVolIndex above "+var1+" signal "+dataList[dataSource]); break; case 19: setTitle("CrossFinder: PosVolIndex below "+var1+" signal "+dataList[dataSource]); break; case 20: setTitle("CrossFinder: NegVolIndex above "+var1+" signal "+dataList[dataSource]); break; case 21: setTitle("CrossFinder: NegVolIndex below "+var1+" signal "+dataList[dataSource]); break; case 22: setTitle("CrossFinder: %b ("+var1+" "+maTitleList[MAtype1]+", "+var3+" dev) above "+var2+" ("+dataList[dataSource]+")"); break; case 23: setTitle("CrossFinder: %b ("+var1+" "+maTitleList[MAtype1]+", "+var3+" dev) below "+var2+" ("+dataList[dataSource]+")"); break; case 24: setTitle("CrossFinder: "+var1+" ZLTCD above "+var3+" signal "+dataList[dataSource]); break; case 25: setTitle("CrossFinder: "+var1+" ZLTCD below "+var3+" signal "+dataList[dataSource]); break; case 26: setTitle("CrossFinder: "+var1+" "+var2+" MACD above Zero "+dataList[dataSource]); break; case 27: setTitle("CrossFinder: "+var1+" "+var2+" MACD below Zero "+dataList[dataSource]); break; case 28: setTitle("CrossFinder: "+var1+" MFI above "+var2+" MFI "+dataList[dataSource]); break; case 29: setTitle("CrossFinder: "+var1+" MFI below "+var2+" MFI "+dataList[dataSource]); break; case 30: setTitle("CrossFinder: "+var1+" CCI above "+var2+" level "+dataList[dataSource]); break; case 31: setTitle("CrossFinder: "+var1+" CCI below "+var2+" level "+dataList[dataSource]); break; case 32: setTitle("CrossFinder: WillAD above "+var1+" signal "+dataList[dataSource]); break; case 33: setTitle("CrossFinder: WillAD below "+var1+" signal "+dataList[dataSource]); break; case 34: setTitle("CrossFinder: "+var1+" DI+ above "+var2+" level"+" "+dataList[dataSource]); break; case 35: setTitle("CrossFinder: "+var1+" DI+ below "+var2+" level"+" "+dataList[dataSource]); break; case 36: setTitle("CrossFinder: "+var1+" DI- above "+var2+" level"+" "+dataList[dataSource]); break; case 37: setTitle("CrossFinder: "+var1+" DI- below "+var2+" level"+" "+dataList[dataSource]); break; } } function getVal(share) { var data = getData(share,dataSource,useIntra,Math.max(var1,var2)*6) if (data.length<2) return; //create the objects needed to calculate the cross switch(crossType) { case 4: case 5: case 26: case 27: var macd1 = new MACD(var1,var2,var3);break; case 6: case 7: var ohlcvIndicator = new StochOsc(var1,var2,var3,MAtype2);break; case 8: case 9: var ohlcvIndicator = new StochOsc(var1,var2,2,MA.Simple);break; case 10: case 11: case 14: case 15: case 34: case 35: case 36: case 37: var adx1 = new ADX(var1);break; case 12: case 13: var indicator1 = new Momentum(var1);break; case 16: case 17: var indicator1 = new RSI(var1);break; case 18: case 19: var ohlcvIndicator = new PVI(var1);break; case 20: case 21: var ohlcvIndicator = new NVI(var1);break; case 22: case 23: var indicator1 = new PercB(var1,MAtype1,var3);break; case 24: case 25: var ohlcvIndicator = new ZLTCD(var1,var3);break; case 28: case 29: var indicator1 = new MFI(var1); var indicator2 = new MFI(var2); break; case 30: case 31: var indicator1 = new CCI(var1);break; case 32: case 33: var ohlcvIndicator = new WillADSig(var1);break; default: if (MAtype1 == 8) //TEMA { var ma1 = new TEMA(var1); } else if (MAtype1 == 9) //VWAP { var ma1 = new MA(var1); var ma3 = new MA(var1); } else //all other MAs { var ma1 = new MA(var1,MAtype1); } if (MAtype2 == 8) //TEMA { var ma2 = new TEMA(var2); } else if (MAtype2 == 9) //VWAP { var ma2 = new MA(var2); var ma4 = new MA(var2); } else //all other MAs { var ma2 = new MA(var2,MAtype2); } } var line1 = new Array(); var line2 = new Array(); var output = 0; // the necessary indicators are calculated for (var i=0;i17 && crossType<22) || crossType==24 || crossType==25 || crossType==32 || crossType==33) { ohlcvIndicator.next(data[i]); line1[i] = ohlcvIndicator.getMain(); line2[i] = ohlcvIndicator.getSignal(); } else if (crossType==10 || crossType==11) { adx1.next(data[i]); line1[i] = adx1.getPDI(); line2[i] = adx1.getNDI(); } else if (crossType==14 || crossType==15) { adx1.next(data[i]); line1[i] = adx1.getADX(); line2[i] = var2; } else if (crossType==12 || crossType==13 || crossType==16 || crossType==17 || crossType==22 || crossType==23) { //this works for both the RSI, %b and momentum indicators line1[i] = indicator1.getNext(data[i].close); line2[i] = var2; } else if (crossType==8 || crossType==9) { //this works for the stochastic crossing a level ohlcvIndicator.next(data[i]); line1[i] = ohlcvIndicator.getMain(); line2[i] = var3; } else if (crossType==26 || crossType==27) { macd1.next(data[i].close); line1[i] = macd1.getMain(); line2[i] = 0; } else if (crossType==28 || crossType==29) { line1[i] = indicator1.getNext(data[i]); line2[i] = indicator2.getNext(data[i]); } else if (crossType==30 || crossType==31) { line1[i] = indicator1.getNext(data[i]); line2[i] = var2; } else if (crossType==34 || crossType==35) { adx1.next(data[i]); line1[i] = adx1.getPDI(); line2[i] = var2 } else if (crossType==36 || crossType==37) { adx1.next(data[i]); line1[i] = adx1.getNDI(); line2[i] = var2 } else { var price = getOHLC(data[i],var3) var price2 = getOHLC(data[i],var4) if (MAtype1 == 8) line1[i] = ma1.getNext(data[i]); else if (MAtype1 == 9) line1[i] = ma1.getNext(price*data[i].volume) / ma3.getNext(data[i].volume); else line1[i] = ma1.getNext(price); if (MAtype2 == 8) line2[i] = ma2.getNext(data[i]); else if (MAtype2 == 9) line2[i] = ma2.getNext(price2*data[i].volume) / ma4.getNext(data[i].volume); else line2[i] = ma2.getNext(price2); } } var a = data.length-1; var b = Math.max(0,data.length-(periodsAgo+1)); // checks for crosses and sets output to 1 if true. if (crossType==0) output = data[b].closeline1[a]?1:0; else if (crossType==1) output = data[b].close>line1[b]&&data[a].closeline2[a]?1:0; else output = line1[b]>line2[b]&&line1[a]3?3:var3); if (dlg2.show()==Dialog.Cancel) return false; MAtype1 = dlg2.getValue("DL2"); var1 = dlg2.getValue("INT1"); var3 = dlg2.getValue("DL3"); } function dialog2(status) //ma crossing ma { dlg2 = new Dialog("Settings...",160,100); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addDropList("DL2",8,-1,80,-1,maList,"","",MAtype1); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Short period",var1,2,1000); dlg2.addDropList("DL4",8,-1,40,-1,["Open","High","Low","Close"],"","Data source",var3>3?3:var3); dlg2.addDropList("DL3",8,-1,80,-1,maList,"","",MAtype2); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Long period",var2,3,1000); dlg2.addDropList("DL5",8,-1,40,-1,["Open","High","Low","Close"],"","Data source",var4); if (dlg2.show()==Dialog.Cancel) return false; MAtype1 = dlg2.getValue("DL2"); var1 = dlg2.getValue("INT1"); MAtype2 = dlg2.getValue("DL3"); var2 = dlg2.getValue("INT2"); var3 = dlg2.getValue("DL4"); //PH - new data choices var4 = dlg2.getValue("DL5"); } function dialog3(status) //macd cross { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Short period",var1,2,1000); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Long period",var2,3,1000); dlg2.addIntEdit("INT3",8,-1,-1,-1,"","Signal period",var3,2,1000); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); var2 = dlg2.getValue("INT2"); var3 = dlg2.getValue("INT3"); } function dialog4(status) //stochastic cross { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?20:var1),2,1000); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Slowing",(status==Adding?2:var2),1,1000); dlg2.addDropList("DL3",8,-1,80,-1,maList,"","",MAtype2); dlg2.addIntEdit("INT3",8,-1,-1,-1,"","Signal period",var3,2,1000); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); MAtype2 = dlg2.getValue("DL3"); var2 = dlg2.getValue("INT2"); var3 = dlg2.getValue("INT3"); } function dialog5(status) //di+ crossing di-, pos and neg volume index crossing signal & Williams A/D crossing signal { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?14:var1),2,1000); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); } function dialog6(status) //momentum, rsi, CCI and ADX crossing level { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?14:var1),2,1000); dlg2.addNumEdit("NUM1",8,-1,-1,-1,"","Level",(status==Adding?30:var2),-400,400); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); var2 = dlg2.getValue("NUM1"); } function dialog7(status) //%b crossing level { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addDropList("DL2",8,-1,80,-1,maList,"","",MAtype1); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?20:var1),2,1000); dlg2.addNumEdit("NUM1",8,-1,-1,-1,"","St. Deviations",(status==Adding?1.5:var3),0.1,5); dlg2.addNumEdit("NUM2",8,-1,-1,-1,"","Level",(status==Adding?0:var2),-5,5); if (dlg2.show()==Dialog.Cancel) return false; MAtype1 = dlg2.getValue("DL2"); var1 = dlg2.getValue("INT1"); var3 = dlg2.getValue("NUM1"); var2 = dlg2.getValue("NUM2"); } function dialog8(status) //ZLTCD crossing signal { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?10:var1),2,1000); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Signal",(status==Adding?5:var3),2,1000); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); var3 = dlg2.getValue("INT2"); } function dialog9(status) //stochastic crossing level { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Period",(status==Adding?20:var1),2,1000); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Slowing",(status==Adding?2:var2),1,1000); dlg2.addIntEdit("INT3",8,-1,-1,-1,"","Level",(status==Adding?20:var3),0,100); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); var2 = dlg2.getValue("INT2"); var3 = dlg2.getValue("INT3"); } function dialog10(status) //Money Flow Index crossing Money Flow Index { dlg2 = new Dialog("Settings...",160,75); dlg2.addOkButton(); dlg2.addCancelButton(); dlg2.addIntEdit("INT1",8,-1,-1,-1,"","Short period",(status==Adding?10:var1),2,1000); dlg2.addIntEdit("INT2",8,-1,-1,-1,"","Long period",(status==Adding?20:var2),3,1000); if (dlg2.show()==Dialog.Cancel) return false; var1 = dlg2.getValue("INT1"); var2 = dlg2.getValue("INT2"); } function TEMA(p) { this.period = p; this.emaVal1; this.emaVal2; this.emaVal3; } TEMA.prototype.getNext = function (price) { var ma1 = new MA(this.period, MA.Exponential); var ma2 = new MA(this.period, MA.Exponential); var ma3 = new MA(this.period, MA.Exponential) if (this.emaVal1 != undefined) this.emaVal1 = ma1.getNext(this.emaVal1); this.emaVal1 = ma1.getNext((price.high+price.low+price.close)/3); if (this.emaVal2 != undefined) this.emaVal2 = ma2.getNext(this.emaVal2); this.emaVal2 = ma2.getNext(this.emaVal1); if (this.emaVal3 != undefined) this.emaVal3 = ma3.getNext(this.emaVal3); this.emaVal3 = ma3.getNext(this.emaVal2); var tema = 3*this.emaVal1-3*this.emaVal2+this.emaVal3; return tema; } //Negative Volume Index function function NVI(p) { this.nviValue; this.ma1 = new MA(p); this.signal; this.prevVol; this.prevC; } NVI.prototype.next = function (price) { if (price.isOHLCV) { if (this.nviValue==undefined) { this.nviValue = price.close; } else if (price.volumethis.prevVol) { this.pviValue = this.pviValue+(price.close/this.prevC-1)*this.pviValue; } this.signal = this.ma1.getNext(this.pviValue); this.prevVol = price.volume; this.prevC = price.close; } } PVI.prototype.getMain = function () { return this.pviValue; } PVI.prototype.getSignal = function () { return this.signal; } //%b indicator function function PercB(p,t,d) { this.period = p; this.ma1 = new MA(p,t); this.devs = d; this.priceArray = []; } PercB.prototype.getNext = function (price) { var av1 = this.ma1.getNext(price); //Close price MA this.priceArray[this.priceArray.length] = price; if (this.priceArray.lengththis.period) this.priceArray.shift(); var y1 = 0; //Bollinger band calculations for (var i=0;i=this.period+1) { var pmf = 0; var nmf = 0; for (var i=1;i<=this.period;i++) { if (this.typ[this.typ.length-i]>this.typ[this.typ.length-i-1]) pmf += this.mf[this.mf.length-i]; else if (this.typ[this.typ.length-i]=0;i--) { tempData = share.getIBarArray(i,dataPeriod*60); if (tempData==undefined || tempData.length<1) continue; data = data.concat(share.getIBarArray(i,dataPeriod*60)); } if (data==undefined || data[0]==undefined || data.length<2) return []; } else if (dataType==15 && useIntra==0) var data = share.getPriceArray(); else if (dataType==16 && useIntra==0) var data = share.getWeeklyBarArray(); else if (dataType==17 && useIntra==0) var data = share.getMonthlyBarArray(); //daily else if (dataType==15 && useIntra==1) { var data = share.getPriceArray(); if (data.length<2) return []; var idata = share.getIBarArray(0,86400); if (idata!=undefined && idata.length==1 && new Date().getDate()==idata[0].date.getDate() && new Date().getDate()!=data[data.length-1].date.getDate()) { data[data.length]={ open:idata[0].open, high:idata[0].high, low:idata[0].low, close:(share.getIClose()==null?share.getIMid():share.getIClose()), volume:idata[0].volume, dateNum:idata[0].dateNum}; } } //weekly else if (dataType==16 && useIntra==1) { var data = share.getWeeklyBarArray(); if (data.length<2) return []; var idata = share.getIBarArray(0,86400); if (idata!=undefined && idata.length==1 && new Date().getDate()==idata[0].date.getDate() && new Date().getDate()!=data[data.length-1].date.getDate()) { if (idata[0].date.getDay()data[data.length-1].high?idata[0].high:data[data.length-1].high), low:(idata[0].lowdata[data.length-1].high?idata[0].high:data[data.length-1].high), low:(idata[0].lowtempData[tempData.length-1].high?idata[0].high:tempData[tempData.length-1].high), low:(idata[0].lowdata[data.length-1].high?tempData[i].high:data[data.length-1].high), low:(tempData[i].low8388608) print("Too much data for single storage space") return output } function restore(storage, data) { var remainder = storage var rangeTot = 1 for (var i=0;i=0;i--) { if (i