book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Historical Data

Rolling Window

Introduction

A RollingWindow is an array of a fixed-size that holds trailing data. It's more efficient to use RollingWindow objects to hold periods of data than to make multiple historical data requests. With a RollingWindow, you just update the latest data point while a history call fetches all of the data over the period you request. RollingWindow objects operate on a first-in, first-out process to allow for reverse list access semantics. Index 0 refers to the most recent item in the window and the largest index refers to the last item in the window.

Supported Types

RollingWindow objects can store any native or C# types.

Select Language:
self._close_price_window = RollingWindow[float](4)
self._trade_bar_window = RollingWindow[TradeBar](2)
self._quote_bar_window = RollingWindow[QuoteBar](2)

To be notified when RollingWindow objects support additional types, subscribe to GitHub Issue #6199.

Add Data

To add data to a RollingWindow, call the add method.

Select Language:
self._close_price_window.add(data["SPY"].close)
self._trade_bar_window.add(data["SPY"])
self._quote_bar_window.add(data["EURUSD"])

To update the data at a specific index, set the value for that index. If the index doesn't currently exist, it increases the size and fills the empty indices with a default value (zero or None).

Select Language:
self._close_price_window[0] = data["SPY"].close
self._trade_bar_window[0] = data["SPY"]
self._quote_bar_window[0] = data["EURUSD"]

Warm Up

To warm up a RollingWindow, make a history request and then iterate through the result to add the data to the RollingWindow.

Select Language:
class WarmUpRollingWindowAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2020, 1, 1)
        spy = self.add_equity("SPY", Resolution.DAILY).symbol
        history_trade_bar = self.history[TradeBar](spy, 10, Resolution.DAILY)
        history_quote_bar = self.history[QuoteBar](spy, 10, Resolution.MINUTE)

        # Warm up the close price and trade bar rolling windows with the previous 10-day trade bar data
        close_price_window = RollingWindow[float](10)
        trade_bar_window = RollingWindow[TradeBar](10)
        for trade_bar in history_trade_bar:
            close_price_window.add(trade_bar.close)
            trade_bar_window.add(trade_bar)

        # Warm up the quote bar rolling window with the previous 10-minute quote bar data
        quote_bar_window = RollingWindow[QuoteBar](10)
        for quote_bar in history_quote_bar:
            quote_bar_window.add(quote_bar)

Check Readiness

To check if a RollingWindow is full, use its is_ready flag.

Select Language:
if not self._close_price_window.is_ready:
    return

Adjust Size

To adjust the RollingWindow size, set the size property.

Select Language:
self._close_price_window.size = 3
self._trade_bar_window.size = 3
self._quote_bar_window.size = 3

When you decrease the size, it removes the oldest values that no longer fit in the RollingWindow. When you explicitly increase the size member, it doesn't automatically add any new elements to the RollingWindow. However, if you set the value of an index in the RollingWindow and the index doesn't currently exist, it fills the empty indices with a default value (zero or None). For example, the following code increases the size to 10, sets the 10th element to 3, and sets the 4th-9th elements to the default value:

Select Language:
self._close_price_window[9] = 3

Access Data

RollingWindow objects operate on a first-in, first-out process to allow for reverse list access semantics. Index 0 refers to the most recent item in the window and the largest index refers to the last item in the window.

Select Language:
current_close = self._close_price_window[0]
previous_close = self._close_price_window[1]
oldest_close = self._close_price_window[self._close_price_window.count-1]

To get the item that was most recently removed from the RollingWindow, use the most_recently_removed property.

Select Language:
removed_close = self._close_price_window.most_recently_removed

Combine with Indicators

The window property of the indicators is a built-in RollingWindow that stores historical values. It holds a collection of IndicatorDataPoint objects, enabling quick access to the most recent historical indicator values for analysis, calculations, or comparisons in trading and financial strategies. Its default size 2, but you can adjust its size. To warm up the indicator and its RollingWindow, call the indicator_history method with the window size as an argument.

Select Language:
class IndicatorRollingWindowAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        spy = self.add_equity("SPY").symbol
        qqq = self.add_equity("QQQ").symbol
    
        # Manual SMA indicator example.
        self._sma = SimpleMovingAverage(20)
        # Automatic Beta indicator example.
        self._beta = self.b(spy, qqq, 20)
    
        # Adjust the window size to 5 to access the previous 5 indicator data points.
        self._sma.window.size = 5
        self._beta.window.size = 5
    
        # Warm up the indicator and window for immediate usage. 
        self.indicator_history(self._sma, spy, self._sma.window.size, Resolution.DAILY)
        # For indicators using 2+ symbols, use an iterable symbol list.
        self.indicator_history(self._beta, [spy, qqq], self._beta.window.size, Resolution.DAILY)

Combine with Consolidators

To store a history of consolidated bars, in the consolidation handler, add the consolidated bar to the RollingWindow.

Select Language:
self._consolidator.data_consolidated += self._on_data_consolidated

# Define consolidator handler function as a class method
def _on_data_consolidated(self, sender, consolidated_bar):
    self._trade_bar_window.add(consolidated_bar)

Cast to Other Types

You can cast a RollingWindow to a list or a DataFrame. If you cast it to a list, reverse the list so the most recent element is at the last index of the list. This is the order the elements would be in if you added the elements to the list with the Add method. To cast a RollingWindow to a DataFrame, the RollingWindow must contain Slice, Tick, QuoteBar, or TradeBar objects. If the RollingWindow contains ticks, the ticks must have unique timestamps.

Select Language:
closes = list(self._close_price_window)[::-1]  # Make sure to reverse the list.

tick_df = self.pandas_converter.get_data_frame[Tick](list(self._tick_window)[::-1])
trade_bar_df = self.pandas_converter.get_data_frame[TradeBar](list(self._trade_bar_window)[::-1])
quote_bar_df = self.pandas_converter.get_data_frame[QuoteBar](list(self._quote_bar_window)[::-1])

Delete Data

To remove all of the elements from a RollingWindow, call the reset method.

Select Language:
self._close_price_window.reset()

Examples

The following examples demonstrate some common practices for rolling windows.

Example 1: Price Actions

The following algorithm saves the trailing 3 TradeBar objects into a RollingWindow. When it identifies a volume contraction breakout price action pattern on the SPY, it buys to ride on the capital inflow. To exit positions, it places a 2% take profit and 1% stop loss order in the on_order_event method.

Select Language:
class RollingWindowAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2021, 1, 1)
        self.set_end_date(2022, 1, 1)

        # Add SPY data for signal generation and trading.
        self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol

        # Set up a rolling window to hold the last 3 trade bars for price action detection as the trade signal.
        self.windows = RollingWindow[TradeBar](3)
        # Warm up the rolling window.
        history = self.history[TradeBar](self.spy, 3, Resolution.MINUTE)
        for bar in history:
            self.windows.add(bar)

    def on_data(self, slice: Slice) -> None:
        bar = slice.bars.get(self.spy)
        if bar:
            # Trade the price action if the previous bars fulfill a contraction breakout.
            if self.contraction_action and self.breakout(bar.close):
                self.set_holdings(self.spy, 0.5)

            # Add the current bar to the window.
            self.windows.add(bar)

    def contraction_action(self) -> None:
        # We trade contraction type price action, where the buying preesure is increasing.
        # 1. The last 3 bars are green.
        # 2. The price is increasing in trend.
        # 3. The trading volume is increasing as well.
        # 4. The range of the bars are decreasing.
        return (
            self.windows[2].close > self.windows[2].open and
            self.windows[1].close > self.windows[1].open and
            self.windows[0].close > self.windows[0].open and
            self.windows[0].close > self.windows[1].close > self.windows[2].close and
            self.windows[0].volume > self.windows[1].volume > self.windows[2].volume and
            self.windows[2].close - self.windows[2].open > self.windows[1].close - self.windows[1].open > self.windows[0].close - self.windows[0].open
        )

    def breakout(self, current_close: float) -> None:
        # Trade breakout from contraction: the breakout should be much greater than the contracted range of the last bar.
        return current_close - self.windows[0].close > (self.windows[0].close - self.windows[0].open) * 2

    def on_order_event(self, order_event: OrderEvent) -> None:
        if order_event.status == OrderStatus.FILLED:
            if order_event.ticket.order_type == OrderType.MARKET:
                # Stop loss order at 1%.
                stop_price = order_event.fill_price * (0.99 if order_event.fill_quantity > 0 else 1.01)
                self.stop_market_order(self.spy, -self.portfolio[self.spy].quantity, stop_price)
                # Take profit order at 2%.
                take_profit_price = order_event.fill_price * (1.02 if order_event.fill_quantity > 0 else 0.98)
                self.limit_order(self.spy, -self.portfolio[self.spy].quantity, take_profit_price)
            elif order_event.ticket.order_type == OrderType.STOP_MARKET or order_event.ticket.order_type == OrderType.LIMIT:
                # Cancel open orders if stop loss or take profit order fills.
                self.transactions.cancel_open_orders()

Example 2: Bid-Ask Spread

The following algorithm trades the microeconomy of SPY's supply-demand relationship. It buys when the current bid-ask spread is less than average spread over the last 20 QuoteBar objects, indicating demand is approaching supply. When the spread is greater than the average, it shorts SPY. To save the last spread values, it uses a RollingWindow.

Select Language:
class RollingWindowAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2020, 2, 20)
        self.set_end_date(2020, 2, 27)
        
        # Add SPY data for signal generation and trading.
        self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol

        # Set up a rolling window to hold the last 20 bar's bid-ask spread for trade signal generation.
        self.windows = RollingWindow[float](20)
        # Warm up for the rolling window with quote data.
        history = self.history[QuoteBar](self.spy, 20, Resolution.MINUTE)
        for bar in history:
            self.windows.add(bar.ask.close - bar.bid.close)

    def on_data(self, slice: Slice) -> None:
        bar = slice.quote_bars.get(self.spy)
        if bar:
            # Update the window with the current bid-ask spread.
            spread = bar.ask.close - bar.bid.close
            self.windows.add(spread)

            # Buy if the current spread is smaller than average, indicating demand is approaching supply. Buy force will drive up price.
            if spread < np.mean(list(self.windows)):
                self.set_holdings(self.spy, -0.5)
            # Short if the current spread is larger than average, indicating supply is gradually overwhelming demand.
            elif spread > np.mean(list(self.windows)):
                self.set_holdings(self.spy, 0.5)

Example 3: EMA Acceleration

The following algorithm trades SPY based on the direction and acceleration of its exponential moving average (EMA). It buys when the EMA is increasing and accelerating upward, indicating a strong uptrend. It short sells when the EMA is decreasing and accelerating downward, indicating a strong downtrend. To compare the current EMA value to its previous values, the algorithm utilizes the EMA's internal RollingWindow.

Select Language:
class RollingWindowAlgorithm(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2021, 8, 1)
        self.set_end_date(2022, 11, 1)
        
        # Add SPY data for trading.
        self.spy = self.add_equity("SPY", Resolution.MINUTE).symbol

        # Set up an automatic EMA indicator for trade signal generation.
        self._ema = self.ema(self.spy, 20, Resolution.DAILY)
        # Extend the size of the EMA's internal rolling window so we can access its historical values.
        self._ema.window.size = 3

        # Schedule an event to rebalance SPY position at daily market open.
        self.schedule.on(
            self.date_rules.every_day(self.spy),
            self.time_rules.after_market_open(self.spy, 0),
            self.rebalance
        )

        self.set_warm_up(23, Resolution.DAILY)

    def rebalance(self) -> None:
        if self._ema.window.is_ready:
            # Buy if the current EMA is increasing with acceleration, indicating strong uptrend.
            if (self._ema.window[1].value < self._ema.current.value and
                self._ema.window[0].value - self._ema.window[1].value > self._ema.window[1].value - self._ema.window[2].value):
                self.set_holdings(self.spy, 0.5)
            # Short if the current EMA is decreasing with acceleration, indicating strong downtrend.
            elif (self._ema.window[1].value > self._ema.current.value and 
                self._ema.window[0].value - self._ema.window[1].value < self._ema.window[1].value - self._ema.window[2].value):
                self.set_holdings(self.spy, -0.5)
            # Liquidate if no strong trend indicated.
            elif self.portfolio.invested:
                self.liquidate(self.spy)

Other Examples

For more examples, see the following algorithms:

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: