IntradayClockEchoSignal

Incremental, causal technical analysis documentation

IntradayClockEchoSignal is a same-clock intraday periodicity signal. It learns which times of day have historically shown positive or negative residual returns and uses that clock pattern to forecast the next horizon_bars of returns.

The main paper references are:

The RTTA implementation is not a full cross-sectional replication. It is an online, per-symbol indicator that learns slot-level residual return behavior from prior bars or from explicit training days.

API

rtta.IntradayClockEchoSignal(
    slots_per_session=195,
    horizon_bars=15,
    lookback_days=40,
    min_slot_samples=10,
    calibration_window=500,
    calibration_quantile=0.80,
    entry_z=1.0,
    cost_buffer=0.0005,
    max_abs_target_fraction=0.03,
    participation_cap=0.02,
    allow_short=True,
    fillna=False,
)

For 2-minute bars in a 390-minute US session, slots_per_session=195. For 5-minute bars, use 78. The example script computes this from the interval unless it is supplied.

update(open, high, low, close, volume, vwap=nan, transactions=nan, market_return=0.0, normal_dollar_volume=nan, slot=0, reset_session=False) consumes one bar.

The train(days) method accepts a sequence of day records. Each day is an iterable of dict-like or tuple-like records containing at least open, high, low, close, volume, and optionally vwap, transactions, market_return, normal_dollar_volume, and slot.

What "Clock Echo" Means

The paper result is about time-of-day repetition. If a stock tends to rise at a particular half-hour slot, related behavior may recur at the same slot on later days. RTTA stores this as slot_echo_[slot], an exponentially weighted average of residual returns for each time-of-day slot.

The indicator does not assume raw return is the full signal. It subtracts the optional market_return first:

bar_return = log(close_t / close_{t-1})
residual_return = bar_return - market_return

This makes the learned pattern closer to idiosyncratic same-clock behavior when a market or ETF return is supplied.

Training and Online State

For each slot, the indicator stores:

The EWMA learning rate is:

alpha = 2 / (lookback_days + 1)

train(days) simply replays prior bars through update(...), resetting intraday state between days. It updates the slot-level long-lived state and the rolling prediction-error calibration state.

reset_session=True clears only intraday state:

It does not clear learned slot echoes, slot volume, or residual calibration.

Prediction

On each update, the indicator forecasts the next horizon_bars by looking at future slots:

future_slot_j = (current_slot + j) % slots_per_session

Each future slot receives:

reliability = min(1, slot_count[future_slot] / min_slot_samples)
weight = exp(-0.10 * (j - 1)) * reliability

The clock echo is the weighted average of future slot echoes:

clock_echo = sum(weight_j * slot_echo[future_slot_j]) / sum(weight_j)
prediction = clock_echo * horizon_bars

The multiplication by horizon_bars turns an average per-slot residual return into a horizon-level expected log return.

Flow and Volume Adjustments

After the clock prediction is made, current bar flow can amplify or soften it:

dollar_volume = close * max(volume, 0)
normal_dv = supplied normal_dollar_volume, or slot_volume[slot], or dollar_volume
signed_flow = sign(bar_return) * dollar_volume / normal_dv
flow_confirm = sign(prediction) * signed_flow
prediction *= 1 + 0.20 * tanh(flow_confirm)

If current dollar volume is radically different from the slot's normal dollar volume, the prediction is dampened:

volume_sync = log(dollar_volume / slot_volume[slot])
if abs(volume_sync) > 4:
    prediction *= 0.5

This prevents the same-clock pattern from being trusted too much during highly abnormal activity.

Error Band

When a prediction is ready, it is stored with:

Each subsequent update decrements the pending horizon. When a prediction matures:

realized = current_log_close - entry_log_close
realized_error = abs(realized - prediction)

The error is pushed into a rolling quantile. Once enough calibration samples exist, radius is:

radius = max(rolling_error_quantile, cost_buffer)

Before calibration is ready, fallback radius is based on the slot's absolute residual return:

radius = max(cost_buffer, slot_abs_err[slot] * horizon_bars)

Readiness

ready is true only when:

If fillna=False, not-ready outputs are intentionally blanked:

With fillna=True, fallback values are emitted earlier. That is useful for experimentation, but live trading systems should usually care about ready.

Trading Outputs

The score is:

score = prediction / (radius + cost_buffer)

The signal is:

signal = +1 if score > entry_z
signal = -1 if allow_short and score < -entry_z
signal =  0 otherwise

Target fraction is capped:

target_fraction = max_abs_target_fraction * clamp(score / 3, side bounds)

max_trade_dollars = participation_cap * normal_dollar_volume is an execution liquidity hint.

Outputs

Intended Use

Use this when bars have stable session slots. It is sensitive to missing bars, half-days, and incorrect session resets because the time-of-day slot is the feature. In the Massive/Polygon example, each symbol trains from prior day aggregate bars, then the live day is scored by aligned window start.

The indicator should generally be paired with a market-return input when available. Without market adjustment, broad intraday market moves can be learned as if they were symbol-specific clock behavior.