Skip to content
All library documents

Intraday Directional Signals from the Opening and Late-Session Returns

Article Strategy library · Author: QuantRocket

Summary

This intraday equity strategy takes a position when two same-day return intervals agree in direction. It compares the 10:00 opening price with the previous session’s 15:59 close, so the first measure includes the overnight move. It then compares 15:30 with 15:00 to measure a later half-hour move. Both positive readings produce a long signal, while both negative readings produce a short signal; mixed readings leave the strategy unpositioned. The position is entered at 15:30 and held to the 15:59 close.

The source specifies a single US stock, per-share commission, slippage, and an optional VIX filter, but reports no backtest results. The overnight component means the first signal is not a pure first-half-hour return. The brief holding window and single-instrument setup limit what can be inferred about broader equity performance, and costs may materially affect such a short-horizon strategy.

Key ideas

  • The strategy requires the overnight-inclusive early return and the 15:00 to 15:30 return to share a direction.
  • Agreement between positive readings creates a long signal, while agreement between negative readings creates a short signal.
  • Positions enter at 15:30 and are measured through the 15:59 close.
  • An optional VIX threshold can suppress signals when volatility is below a selected level.
  • The source provides implementation assumptions but no evidence of profitability.

Tags

Full text
# FirstHalfHourPredictsLastHalfHour


# FirstHalfHourPredictsLastHalfHour









Intraday strategy that buys (sells) if the market is up (down) during the first
    and penultimate half-hour.

## Source (Apache-2.0)

```python
# Copyright QuantRocket LLC - All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pandas as pd
from moonshot import Moonshot
from moonshot.commission import PerShareCommission
from quantrocket import get_prices

class USStockCommission(PerShareCommission):
    BROKER_COMMISSION_PER_SHARE = 0.005

class FirstHalfHourPredictsLastHalfHour(Moonshot):
    """
    Intraday strategy that buys (sells) if the market is up (down) during the first
    and penultimate half-hour.
    """

    CODE = 'first-last'
    DB = 'usstock-1min'
    DB_TIMES = ['10:00:00', '15:00:00', '15:30:00', '15:59:00']
    DB_FIELDS = ['Open','Close']
    SIDS = ["FIBBG000BDTBL9"]
    COMMISSION_CLASS = USStockCommission
    SLIPPAGE_BPS = 0.5
    MIN_VIX = None
    BENCHMARK = "FIBBG000BDTBL9"
    BENCHMARK_TIME = "15:59:00"

    def prices_to_signals(self, prices: pd.DataFrame):

        closes = prices.loc["Close"]
        opens = prices.loc["Open"]

        # Calculate first half-hour returns (including overnight return)
        prior_closes = closes.xs('15:59:00', level="Time").shift()
        ten_oclock_prices = opens.xs('10:00:00', level="Time")
        first_half_hour_returns = (ten_oclock_prices - prior_closes) / prior_closes

        # Calculate penultimate half-hour returns
        fifteen_oclock_prices = opens.xs('15:00:00', level="Time")
        fifteen_thirty_prices = opens.xs('15:30:00', level="Time")
        penultimate_half_hour_returns = (fifteen_thirty_prices - fifteen_oclock_prices) / fifteen_oclock_prices

        # long when both are positive, short when both are negative
        long_signals = (first_half_hour_returns > 0) & (penultimate_half_hour_returns > 0)
        short_signals = (first_half_hour_returns < 0) & (penultimate_half_hour_returns < 0)

        # Combine long and short signals
        signals = long_signals.astype(int).where(long_signals, -short_signals.astype(int))

        # filter by VIX
        if self.MIN_VIX:
            # Query VIX at 15:30 NY time (= close of 14:00:00 bar because VIX is Chicago time)
            vix = get_prices("vix-30min",
                             fields="Close",
                             start_date=signals.index.min(),
                             end_date=signals.index.max(),
                             times="14:00:00")
            # extract VIX and squeeze single-column DataFrame to Series
            vix = vix.loc["Close"].xs("14:00:00", level="Time").squeeze()
            # reshape VIX like signals
            vix = signals.apply(lambda x: vix)
            signals = signals.where(vix >= self.MIN_VIX, 0)

        return signals

    def signals_to_target_weights(self, signals: pd.DataFrame, prices: pd.DataFrame):

        # only one instrument, so allocate all capital
        target_weights = signals.copy()
        return target_weights

    def target_weights_to_positions(self, target_weights: pd.DataFrame, prices: pd.DataFrame):

        # We enter on the same day as the signals/target_weights
        positions = target_weights.copy()
        return positions

    def positions_to_gross_returns(self, positions: pd.DataFrame, prices: pd.DataFrame):

        opens = prices.loc["Open"]
        closes = prices.loc["Close"]

        # Our signal came at 15:30 and we enter at 15:30
        entry_prices = opens.xs("15:30:00", level="Time")
        session_closes = closes.xs("15:59:00", level="Time")

        pct_changes = (session_closes - entry_prices) / entry_prices
        gross_returns = pct_changes * positions
        return gross_returns

```

Shown in full with attribution under the source's licence. Licence: Apache-2.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.