Intraday Directional Signals from the Opening and Late-Session Returns
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.