An intraday strategy can be profitable while remaining flat for most of the trading day. That leaves two useful questions. How fast did the strategy grow over the full research period? And how much return did it produce relative to the time it actually occupied capital?
This RTTA case study follows a long-only SPY signal through historical replay, quote-aware execution, and both forms of measurement. Over 411 trading days, the reference run returned 5.23% while holding a position on 8.3% of observed bars. The result annualizes to 3.18% on the calendar clock and 45.8% on the occupied-capital clock. Both figures describe the same P&L; the difference is the amount of time used as the denominator.
From market data to a result
RTTA provides stateful market algorithms that update one observation at a time. Each update sees the history already accumulated by the object, but nothing from the future. The same object can therefore run in a live event loop or in a chronological replay without changing its information set.
The experiment combines an RTTA signal with the massive_speedup replay and
execution tools:
historical SPY trades and quotes
|
v
110-second trade bars
|
v
causal FRI update
|
v
long/flat position rule
|
v
delayed bid/ask fills
|
v
P&L and position occupancy
Trades form the bars that drive the signal. Quotes supply the bid and ask used by the execution simulator. A marketable buy crosses to the ask and a sell crosses to the bid after 150 milliseconds of simulated latency. This is still a model of execution, but it avoids the unusually favorable assumption that every order fills immediately at the bar close or midpoint.
The signal and position rule
The signal is
FourierResidueIdentity,
or FRI. Return autocorrelation often mixes two effects: whether the direction of
successive moves is related, and whether their size is related. FRI tracks those
effects separately. This run trades from its direction channel, exposed as
z_sign.
z_sign expresses the strength of the observed directional relationship in
approximate standard-error units. It is evidence about dependence in the
stream, not a forecast of percentage price change. The kalman3d FRI variant
used here estimates its internal moments with a constant-acceleration Kalman
state rather than the standard exponentially weighted averages.
The position rule applies hysteresis to that score:
flat + z_sign >= 2.8279 -> long one share
long + z_sign <= 0.9818 -> flat
Using a lower exit threshold keeps small movements around the entry boundary from repeatedly opening and closing the position. Shorting is disabled. New entries are also rejected when the quoted spread exceeds five basis points, and the position is flattened at the end of the regular-hours window.
Despite the filename fri_1s_stack_exercise_from_massive_speedup.py, the bar
interval is configurable. The reference run uses 110-second bars. It also
leaves the optional post-filter on z_sign disabled; the Kalman model is inside
FRI rather than applied to its output a second time.
Reference run
The replay requests SPY data from 2024-11-26 through 2026-07-23. The full local database supplies 411 regular-hours sessions, equivalent to 1.631 trading years at 252 sessions per year.
The simulated position is either one share or flat. Profit is divided by the starting SPY price, approximately $599.86, to produce the reported return. In effect, the calculation treats the strategy as a fully funded one-share allocation; it is not a return on an unspecified brokerage balance.
Configuration
| Parameter | Value |
|---|---|
| Symbol | SPY |
| Start / stop date | 2024-11-26 / 2026-07-23 |
| Bar interval | 110 seconds |
| FRI variant | kalman3d |
| Post-filter | none |
| Entry z / exit z | 2.8278976035909347 / 0.9817989165901835 |
| Maximum lag | 11 |
| Horizon | 3 |
| Test lag | 4 |
| Span | 2755.4266219176297 |
| Median window | 364 |
| Minimum hold | 0 bars |
| Maximum spread | 5 bps |
| Trade latency | 150,000,000 ns |
| Trading window | 09:30-16:00 America/New_York |
| Execution | market |
| Position size | 1 share |
| Shorting | disabled |
Results
| Measurement | Reference value |
|---|---|
| Profit | $31.40 |
| Total return, \(R\) | 5.23455% (0.052345547) |
| Starting price | approximately $599.86 |
| Fills / completed cycles | 112 / 56 |
| Duty fraction, \(d\) | 8.29586% (0.0829586) |
| Held bars / observed bars | 6,918 / 83,391 |
| Trading days | 411 |
| Calendar annualized return | 3.1778% |
| Capital-time annualized return | 45.8043% |
| Growth scaled by inverse duty | 84.9701% |
The data coverage is part of the result. A database containing only about 114 SPY sessions yields a much shorter sample and a calendar annualization near 10% for this configuration. That result is not comparable to the 411-session run.
Measuring position occupancy
Duty is the fraction of regular-hours bars for which the strategy intends to be non-flat:
Current runner output includes two versions of the measurement. duty_bars
uses the strategy's intended stance after each bar. duty_fills reconstructs
holding time from the fill ledger. They may differ slightly because a decision
does not become a position until the delayed order fills. The reference
annualization uses duty_bars.
This distinction matters when results from different systems are compared. A bar-level duty measure describes signal occupancy; a fill-level measure describes executed occupancy. Either can be useful, but the definition needs to remain consistent across runs.
The same return on two clocks
Let \(R\) be total fractional return, \(d\) the duty fraction, and \(Y\) the length of the sample in trading years:
Calendar time
Calendar annualization compounds the result over the entire sample:
For the reference run:
The strategy therefore returned approximately 3.18% per calendar trading year. This is the appropriate rate when the allocation is reserved for the strategy throughout the research period, including the time it remains flat.
Capital time
Capital-time annualization counts only the fraction of the sample during which the position is non-flat:
At 8.29586% duty, 411 sessions contain about 34.10 occupied-session equivalents. Applying the same return to that shorter clock gives:
The result is approximately 45.8% per occupied-capital year. This is a measure of return intensity. It does not mean the account earned 45.8%, nor does it assume that the strategy itself can be run continuously. Its practical value depends on whether capital left idle by this strategy can be put to work elsewhere.
The runner also reports:
which is 84.97% for this sample. It scales growth by inverse duty without converting the result to a one-year period, so it should not be read as an annual return.
The three values can be recomputed directly from the summary fields:
R = 0.052345547
duty = 0.0829586
trade_days = 411
years = trade_days / 252.0
annual_calendar = (1.0 + R) ** (1.0 / years) - 1.0
annual_capital_time = (1.0 + R) ** (1.0 / (duty * years)) - 1.0
growth_per_duty = (1.0 + R) ** (1.0 / duty) - 1.0
print(f"calendar: {annual_calendar:.4%}")
print(f"capital-time: {annual_capital_time:.4%}")
print(f"inverse duty: {growth_per_duty:.4%}")
calendar: 3.1778%
capital-time: 45.8043%
inverse duty: 84.9701%
Where capital time becomes useful
A low-duty strategy is usually more interesting as one sleeve of a portfolio than as a standalone allocation. If several sleeves tend to enter at different times, the same capital base may support more than one of them. Capital-time return provides a common way to compare how efficiently each sleeve uses its occupied portion of that base.
Redeployment is not automatic. Positions can overlap, signals can fail together, and nominally different strategies can concentrate in the same instrument or risk factor. Margin, liquidity, market impact, and the time required to move capital between sleeves also constrain what can be combined.
For that reason, capital-time rates are not additive. Testing a stack requires the timestamped positions and P&L of every sleeve. The combined simulation can then measure concurrent capital demand, net exposure, correlated losses, and shared execution costs. Calendar return remains the final measure of the assembled portfolio; capital-time return helps evaluate the pieces.
Reproducing the run
The replay requires a complete local Massive-style trade and quote database with a layout similar to:
${MASSIVE_SPEEDUP_DB_PATH}/
stock_trade/YYYY-MM-DD/SPY
stock_quote/YYYY-MM-DD/SPY
From the repository root:
export MASSIVE_SPEEDUP_DB_PATH=/path/to/massive-db
# Keep missing local data from falling back to a REST source.
unset MASSIVE_API_KEY POLYGON_API_KEY
python3 examples/fri_1s_stack_exercise_from_massive_speedup.py \
--symbol SPY \
--start-date 2024-11-26 \
--stop-date 2026-07-23 \
--interval 110 \
--fri-variant kalman3d \
--z-smoother none \
--entry-z 2.8278976035909347 \
--exit-z 0.9817989165901835 \
--max-lag 11 \
--horizon 3 \
--test-lag 4 \
--span 2755.4266219176297 \
--median-window 364 \
--min-hold-bars 0 \
--max-spread-bps 5 \
--trade-latency-ns 150000000 \
--trade-start 09:30 \
--trade-end 16:00 \
--execution market \
--unit-shares 1
The summary appears in three records:
# result,...,profit_pct,...,duty_fraction,held_bars,rth_bars
# meta,...,trade_days=411,...,duty_fraction=...,cycles=56,...
# capital_time,duty_bars=...,duty_fills=...,R=...,growth_per_duty=...,ann_capital_time=...,ann_calendar=...,years=...
A matching replay should produce roughly 5% total return, 8% duty, 56 completed cycles, and at least 400 trading days. Large differences usually come from data coverage, missing quotes, a different bar interval, or changed fill settings.
Limits of the result
The configuration was selected during a parameter search and should be treated as an in-sample research result. It covers one instrument, permits only a one-share long position, and does not test scaling or market impact. Spread and latency are modeled, but the original run generally assumes zero commission.
Fifty-six completed cycles provide more evidence than a handful of isolated trades, but the report does not include drawdown, return volatility, tail behavior, or an out-of-sample period. Those measurements would be needed to say whether the effect is stable or whether its risk is acceptable.
Capital-time annualization also becomes unstable as duty approaches zero. The exponent \(1/(dY)\) can turn a small profit from very few trades into an extraordinary-looking rate. Total return, sample length, duty definition, and trade count therefore belong beside the annualized figure.
Taken on its own terms, the run shows a sparse strategy with modest calendar growth and comparatively dense returns while occupied. Whether that density is useful depends on the next level of the system: an out-of-sample portfolio test that demonstrates the idle capital can actually be redeployed.
