Hello!
I implemented a simple mean reversion strategy. I sum prices of 2 FX pairs. Using rolling window I check if the sum was decling or increasing over the last few days. If the price goes down 3 days in a row I open a long position and hold for 2 days, and vice versa. Everything works well, as I receive correct signals to buy/sell pairs.
What concerns me however, is the difference between prices, which are printed from the RollingWindow function, and opening prices of my positions. Let me give you an example:
Here as you can see, the price of the pair has been increasing 3 days in a row. “Sum 0D” so sum of the most recent FX prices is higher than “Sum 1D" so sum of yesterday's close, etc.
Then, I print the most recent prices of my FX pairs, using the rolling window function with a value of [0] at the end. The most recent price of the first pair is 6.10194 and price of the second pair is 3.70235. I receive these prices at 19:00:00 on 2021/01/10. After receiving this signal, the code then automatically sends sell orders, which are executed at the same time, at 19:00:00 on 2021-01-10
However as you can see, the price at which my orders were executed, were significantly different to those obtained from rolling window function, which were supposed to be the most recent prices. Instead of opening a short USDDKK at 6.10194, the position was opened at 6.11578. Same issue with the second pair. Instead of being executed at 3.70235, it was shorted at 3.71829.
What could be the reason of such difference? Could it be caused by the difference in Bid and Ask? So that the rollingwindow shows the last price which could be for example mean of ASK and BID prices, however the trade is executed using BID prices, causing the divergence?
# region imports
from AlgorithmImports import *
# endregion
class CustomIndexStrategy(QCAlgorithm):
def Initialize(self):
self.Pair_1_Multiplier = 4
self.Pair_2_Multiplier = 1
self.Pair_1 = "USDDKK"
self.Pair_2 = "USDPLN"
self.holdingDays = 2
self.SetStartDate(2021, 1, 3)
self.SetEndDate(2022,6,24)
self.SetCash(1000000)
self.SetBrokerageModel(BrokerageName.OandaBrokerage)
self.EURSEK = self.AddForex(self.Pair_1, Resolution.Daily, Market.Oanda)
self.GBPSGD = self.AddForex(self.Pair_2, Resolution.Daily, Market.Oanda)
self.SetBenchmark("SPY")
self.symbols = [self.Pair_1, self.Pair_2]
self.prevPrices = { symbol : RollingWindow[QuoteBar](7) for symbol in self.symbols }
self.ticketPair1 = None
self.ticketPair2 = None
def OnData(self,data):
for symbol in self.symbols:
if data.ContainsKey(symbol):
self.prevPrices[symbol].Add( data[symbol] )
if not all([ window.IsReady for window in self.prevPrices.values() ]):
return
Pair1_window = self.prevPrices[self.Pair_1]
Pair1_6D = Pair1_window[6].Close
Pair1_5D = Pair1_window[5].Close
Pair1_4D = Pair1_window[4].Close
Pair1_3D = Pair1_window[3].Close
Pair1_2D = Pair1_window[2].Close
Pair1_1D = Pair1_window[1].Close
Pair1_0D = Pair1_window[0].Close
Pair2_window = self.prevPrices[self.Pair_2]
Pair2_6D = Pair2_window[6].Close
Pair2_5D = Pair2_window[5].Close
Pair2_4D = Pair2_window[4].Close
Pair2_3D = Pair2_window[3].Close
Pair2_2D = Pair2_window[2].Close
Pair2_1D = Pair2_window[1].Close
Pair2_0D = Pair2_window[0].Close
Sum_6D = (Pair1_6D * self.Pair_1_Multiplier) + (Pair2_6D * self.Pair_2_Multiplier)
Sum_5D = (Pair1_5D * self.Pair_1_Multiplier) + (Pair2_5D * self.Pair_2_Multiplier)
Sum_4D = (Pair1_4D * self.Pair_1_Multiplier) + (Pair2_4D * self.Pair_2_Multiplier)
Sum_3D = (Pair1_3D * self.Pair_1_Multiplier) + (Pair2_3D * self.Pair_2_Multiplier)
Sum_2D = (Pair1_2D * self.Pair_1_Multiplier) + (Pair2_2D * self.Pair_2_Multiplier)
Sum_1D = (Pair1_1D * self.Pair_1_Multiplier) + (Pair2_1D * self.Pair_2_Multiplier)
Sum_0D = (Pair1_0D * self.Pair_1_Multiplier) + (Pair2_0D * self.Pair_2_Multiplier)
if self.ticketPair1 is not None and self.UtcTime < self.ticketPair1.Time + timedelta(days=3):
return
if self.ticketPair1 is None and Sum_0D > Sum_1D > Sum_2D > Sum_3D:
self.Log(" SUM 0D: " + str(Sum_0D) + "Sum 1D: " + str(Sum_1D) + " Sum 2D: " + str(Sum_2D) + " SUM 3D: " + str(Sum_3D) + " SUM 4D: " + str(Sum_4D) + " SUM 5: " + str(Sum_5D) + " SUM 6D: " + str(Sum_6D) )
self.Log("Price increasing 3 days in a row")
self.Log("Pair1 0D: " + str(Pair1_0D) + "Pair2 0d: " + str(Pair2_0D) + "Pair1 1D: " + str(Pair1_1D) + "Pair2 1D: " + str(Pair2_1D))
self.ticketPair1 = self.StopMarketOrder(self.Pair_1, -100000 * self.Pair_1_Multiplier, 1.03 * self.Securities[self.Pair_1].Close)
self.ticketPair2 = self.StopMarketOrder(self.Pair_2, -100000 * self.Pair_2_Multiplier, 1.03 * self.Securities[self.Pair_2].Close)
if self.ticketPair1 is None and Sum_0D < Sum_1D < Sum_2D < Sum_3D:
self.Log(" SUM 0D: " + str(Sum_0D) + "Sum 1D: " + str(Sum_1D) + " Sum 2D: " + str(Sum_2D) + " SUM 3D: " + str(Sum_3D) + " SUM 4D: " + str(Sum_4D) + " SUM 5: " + str(Sum_5D) + " SUM 6D: " + str(Sum_6D) )
self.Log("Price decreasing 3 days in a row")
self.Log("Pair1 0D: " + str(Pair1_0D) + "Pair2 0d: " + str(Pair2_0D) + "Pair1 1D: " + str(Pair1_1D) + "Pair2 1D: " + str(Pair2_1D))
self.ticketPair1 = self.StopMarketOrder(self.Pair_1, 100000 * self.Pair_1_Multiplier, 0.97 * self.Securities[self.Pair_1].Close)
self.ticketPair2 = self.StopMarketOrder(self.Pair_2, 100000 * self.Pair_2_Multiplier, 0.97 * self.Securities[self.Pair_2].Close)
if self.ticketPair1 is not None and self.UtcTime >= self.ticketPair1.Time + timedelta(days = self.holdingDays):
self.Liquidate()
self.ticketPair1 = None
Adam W
That price difference does seem to be a bit large for bid-ask spread alone (note that you can use limit orders and access Bid/Ask prices in the QuoteBars if needed).
Just a stab in the dark here - might be related to the Daily resolution? For equities, using daily resolution leads to some known unintended behavior in backtests since daily data comes in at market close, and then the order is filled when the next data bar arrives. Forex is 24 hr of course but maybe there's some similar issue here.
It's generally a good idea anyways to backtest with minute/second data (or at least hourly). I would recommend moving the `OnData` logic into a consolidated daily bar (or use a scheduled function) with lower data resolution, and see if the issue persists.
Sebul
Thank you for your answer, I will definitely implement your advices!
Sebul
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!