diff --git a/strategies/PairsTradingBot.cs b/strategies/PairsTradingBot.cs new file mode 100644 index 0000000..40ff92c --- /dev/null +++ b/strategies/PairsTradingBot.cs @@ -0,0 +1,115 @@ +#region Using declarations +using NinjaTrader.Cbi; +using NinjaTrader.NinjaScript.Indicators; +using System; +using System.ComponentModel.DataAnnotations; +#endregion + +//This namespace holds Strategies in this folder and is required. Do not change it. +namespace NinjaTrader.NinjaScript.Strategies +{ + public class PairsTradingBot : Strategy + { + private const int FirstSymbolBars = 0; + private const int SecondSymbolBars = 1; + + private const double EntryZScore = 2.0; + private const double ExitZScore = 0.5; + + private double spread; + private double meanSpread; + private double stdDevSpread; + private Series spreadSeries; + + protected override void OnStateChange() + { + if (State == State.SetDefaults) + { + Description = @"Simple pairs trading strategy"; + Name = "Pairs Trading Bot"; + Calculate = Calculate.OnBarClose; + EntriesPerDirection = 1; + EntryHandling = EntryHandling.AllEntries; + IsExitOnSessionCloseStrategy = true; + ExitOnSessionCloseSeconds = 30; + IsFillLimitOnTouch = false; + MaximumBarsLookBack = MaximumBarsLookBack.TwoHundredFiftySix; + OrderFillResolution = OrderFillResolution.Standard; + Slippage = 0; + StartBehavior = StartBehavior.WaitUntilFlat; + TimeInForce = TimeInForce.Gtc; + TraceOrders = false; + RealtimeErrorHandling = RealtimeErrorHandling.StopCancelClose; + StopTargetHandling = StopTargetHandling.PerEntryExecution; + BarsRequiredToTrade = 20; + IsInstantiatedOnEachOptimizationIteration = true; + + SecondSymbol = "AAPL"; + LookbackPeriod = 20; + } + else if (State == State.Configure) + { + // First symbol in the pair is the primary data series. + AddDataSeries(SecondSymbol, BarsPeriod); + } + else if (State == State.DataLoaded) + { + spreadSeries = new Series(this); + } + } + + protected override void OnBarUpdate() + { + if (BarsInProgress != 0) + return; + + double instrument1Price = Closes[FirstSymbolBars][0]; + double instrument2Price = Closes[SecondSymbolBars][0]; + + spread = instrument1Price - instrument2Price; + spreadSeries[0] = spread; + + if (CurrentBar < LookbackPeriod) + return; + + meanSpread = SMA(spreadSeries, LookbackPeriod)[0]; + stdDevSpread = StdDev(spreadSeries, LookbackPeriod)[0]; + + double zScore = (spread - meanSpread) / stdDevSpread; + + if (zScore > EntryZScore) + { + if (Position.MarketPosition != MarketPosition.Short) + { + EnterShort(FirstSymbolBars, 1, "Symbol 1 Short"); + EnterLong(SecondSymbolBars, 1, "Symbol 2 Short"); + } + } + else if (zScore < -EntryZScore) + { + if (Position.MarketPosition != MarketPosition.Long) + { + EnterLong(FirstSymbolBars, 1, "Symbol 1 Long"); + EnterShort(SecondSymbolBars, 1, "Symbol 2 Long"); + } + } + else if (Math.Abs(zScore) < ExitZScore) + { + ExitLong(); + ExitShort(); + } + } + + public override string DisplayName + { + get { return Name; } + } + + [Display(Name = "Second Symbol", GroupName = "Pairs Trading Bot", Order = 1)] + public string SecondSymbol { get; set; } + + [Range(1, int.MaxValue), NinjaScriptProperty] + [Display(Name = "Lookback Period", GroupName = "Pairs Trading Bot", Order = 2)] + public int LookbackPeriod { get; set; } + } +}