This article studies a behavioral-finance hypothesis created by a rolling statistical window. Instead of directly forecasting future prices, we calculate in advance how the 24h percentage change traders will see over the next several dozen minutes will mechanically evolve, and then observe whether price responds to that change in attention. The current strategy does not directly measure aggressive order flow, so the transmission mechanism discussed here is a plausible pathway rather than a proven causal relationship between order flow and price.
1. A Number That Looks Almost Too Ordinary to Matter
Almost every crypto trading interface places the 24-hour percentage change in a highly visible position. Top-gainer lists, top-loser lists, contract tables, and mobile market pages all keep reinforcing this number.
We usually interpret the 24h change as “what happened over the last day,” but it is actually a continuously moving window:
Its evolution is jointly determined by two components:
- What the current price does next — unknown.
- How the price from 24 hours ago, , leaves the rolling window — already observed and therefore known.
Suppose price rallied sharply yesterday between 14:00 and 14:30. When the market reaches the same time today, that rally gradually rolls out of the 24-hour window minute by minute. Even if the current price does not move at all, the displayed 24h gain will mechanically decline. Conversely, when a sharp selloff from yesterday rolls out, the displayed return will mechanically improve.
That fact by itself is only a mathematical identity, not Alpha. The hypothesis that actually needs to be tested is:
Predictable change in the displayed statistic
↓
Changes in rankings, screeners, and trader attention
↓
Trading behavior may change
↓
Potentially tradable price drift
In other words, the strategy is not directly predicting price. It is predicting what market participants are about to see.
2. What Public Research Gives Us as a Starting Point
The idea was popularized by Robot James in the article A Truly Idiotic Crypto Trade, and was later reproduced on a large historical sample by the open-source project OctopusTakopi/24h-rollout-effect, using Binance USDT perpetual-futures archives.
Robot James article:
Public replication:
The coarse-grained public rule is simple:
- When the largest positive hourly candle from exactly 24 hours ago begins to roll out, short for one hour.
- When the largest negative hourly candle from exactly 24 hours ago begins to roll out, go long for one hour.
The replication covers 788 Binance USDT perpetual contracts from 2020 to 2026, including delisted contracts. Its results support the presence of a 24h Roll-Out anomaly in the historical sample. Placebo tests over candle ages from 1 to 24 hours show that the effect is concentrated specifically around the 24th hour.
But the research also provides a conclusion that matters more than the headline return: the raw signal is thin.
High turnover allows fees to consume much of the gross edge, funding costs erode it further, and spectacular results from concentrated full-capital backtests often mix together path luck, compounding mathematics, and tail risk. In the public sample, most of the more recent edge is concentrated on the short side, while a single extreme move against a short position can wipe out the profits from a large number of ordinary trades.
So the public result is enough to justify further research. It is not enough to declare that a production-ready strategy already exists.
The original contribution of this article is not to rediscover the 24h anomaly. It is to convert the idea into a continuous minute-level model, transfer it into Binance’s TradFi perpetual market, and implement a complete FMZ Rust prototype that is observable, simulatable, and capable of live execution.
3. Why Not Simply Short Anything That Is Up a Lot Over 24 Hours?
The current 24h percentage change can be used as an attention signal, but it does not determine the trading direction.
Suppose a contract is currently up 20%, but the corresponding price path 24 hours earlier was almost flat. In that case there is no meaningful Roll-Out catalyst scheduled to occur over the next half hour. Shorting merely because the asset is “up a lot” would be nothing more than a conventional reversal strategy.
This strategy therefore keeps two concepts strictly separate:
Current 24h Change = Attention Filter
Expected Roll-Out Shock = Alpha Trigger
The first is used to reduce the universe that requires deep monitoring. The second is what determines the candidate direction.
4. Turning a Single Hourly Candle into a Continuous Roll-Out Curve
An hourly rule implicitly assumes that all the information arrives at the top of the hour. Real price paths are continuous.
Price changes from yesterday at 14:05, 14:12, and 14:27 roll out of the 24h calculation today at the corresponding times. They do not disappear all at once at 14:00.
Let the current price be , and let the current starting point of the 24-hour window be . Assume, only for the purpose of calculating the mechanical display effect, that the current price remains unchanged over the next minutes. The mechanical change in the displayed 24h return is then:
Therefore:
Shock(30) > 0: the displayed return is expected to mechanically improve, so the candidate direction is Long.Shock(30) < 0: the displayed return is expected to mechanically deteriorate, so the candidate direction is Short.Shock(5)andShock(30)must have the same sign, so the old price path does not first move against the eventual direction.- The five-minute shock must also reach a minimum intensity, so the 30-minute signal is not concentrated almost entirely at the far end of the window.
A simplified example:
Current price 120Current start of the 24h window100Price 24h ago, 30 minutes later 115Displayed 24h gain now +20.0%
Displayed gain in 30m if price is flat +4.35%
Shock(30) -15.65 percentage points
Candidate direction SHORT
The -15.65 percentage points represent the mechanical change in the displayed statistic. They do not mean that price is forecast to fall by 15.65%.
How much, if any, of that display shock is reflected in price must be answered empirically.
The strategy’s core calculation maps directly to the formula. The final direction is determined solely by the sign of Shock(30):
signal.shock_5_pct =
100.0 * current * (1.0 / old_5 - 1.0 / old_now);
signal.shock_30_pct =
100.0 * current * (1.0 / old_30 - 1.0 / old_now);
signal.direction = if signal.shock_30_pct > 0.0 {
1
} elseif signal.shock_30_pct < 0.0 {
-1
} else {
0
};
5. Why the First Version Targets TradFi Perpetuals
Binance TradFi perpetuals map equities, ETFs, commodities, and other traditional-market exposures into USDT-margined perpetual contracts.
This environment is interesting for several reasons:
- The products are still presented inside a crypto-style interface with 24h percentage changes and rankings.
- Traditional-market opens, closes, pre-market, after-hours, and overnight sessions may create more structured historical price paths.
- TradFi perpetuals have different participant mixes, liquidity profiles, and information rhythms from purely crypto-native assets, making them useful for testing whether the mechanism transfers.
- On the test account used during development, the displayed maker fee for these products was zero. That makes it easier to observe a thin gross signal at the research stage. This was only the fee state of a specific account at a specific point in time and must not be generalized to other accounts or future fee schedules.
A zero maker fee does not mean zero trading cost.
Queue failure, adverse selection, bid-ask spreads, funding, and emergency-exit slippage still exist. The prototype therefore uses real order-book data and maker-fill constraints instead of assuming that sending a resting order means it has been filled.

More importantly, the public historical replication validates Binance crypto USDT perpetuals, not the TradFi submarket studied here.
Transferring the mechanism to TradFi perpetuals is a new hypothesis that still needs to be validated. The historical crypto results cannot be borrowed as evidence of profitability in the TradFi market.
6. Data Architecture of the FMZ Rust Prototype
The first version uses a light full-market scan + deep tracking of a small candidate set architecture:
Binance exchangeInfo
│
└── Dynamically discover TradFi perpetuals
Binance all-market WebSocket 24h ticker
│
└── AttentionScanner
│
└── Top-N candidates
├── ~25h of recent 1m klines
├── ticker
├── bookTicker
├── aggTrade
└── 1m kline
│
└── Roll-OutEngine
At startup, REST is used only to read the contract directory.
During normal operation, all-market market data and incremental candidate data are primarily consumed over WebSocket. The main loop uses non-blocking reads, so waiting for market data does not stop order reconciliation or state updates.
The universe is not based on a manually maintained whitelist. Instead, the strategy reads contract metadata and keeps only instruments that are actively trading, quoted in USDT, and identified as TradFi perpetual products:
let subtype_tradfi = subtypes.map(|values| values.iter().any(|value| {
value.as_str()
.map(|text| text.eq_ignore_ascii_case("TradFi"))
.unwrap_or(false)
})).unwrap_or(false);
let tradfi_contract =
contract_type.eq_ignore_ascii_case("TRADIFI_PERPETUAL")
|| subtype_tradfi;
if !tradfi_contract || status != "TRADING" || quote != "USDT" {
continue;
}
The entire market is rescanned once per minute.
By default, the strategy first requires the absolute current 24h change to be at least 4%, then ranks candidates using the absolute 24h change plus a modest quote-volume weight. Only the top six candidates maintain minute history and detailed data streams.
This avoids repeatedly downloading roughly 1,500 one-minute bars for hundreds of contracts.
A candidate must have at least 1,475 valid one-minute bars, deduplicated by timestamp.
When calculating the Roll-Out window, the implementation prefers the actual rolling-window start and end times reported by the ticker instead of blindly assuming that the window is always aligned with the local minute boundary.
There is one v0.1.6 implementation boundary worth monitoring: if the initial history bootstrap for a candidate fails, bootstrap_requested_at is not automatically cleared within the current process, so the strategy will not automatically issue a second bootstrap request.
If SHADOW mode remains stuck at HISTORY_NOT_READY, first check the network and API response, then restart the strategy. A future version should replace this behavior with automatic retry and backoff.
7. Signal Rules in the First Version
Under the default parameters, a candidate must satisfy all of the following conditions:
abs(Current24hChange) >= 4%abs(Shock30) >= 0.8percentage pointsabs(Shock5) >= 0.8 × 0.08percentage pointsShock5andShock30have the same sign- The ticker is no more than 20 seconds old
- Immediately before entry, book and trade data are no more than 5 seconds old
- Bid-ask spread is no wider than 30 bps
The design is intentionally simple.
The original research material also discusses rank effects, open interest, funding, aggressive buy/sell flow, and Probe-then-Build position scaling. All of those modules are deliberately postponed in the first version.
Otherwise, if performance changes, it becomes difficult to determine which component was responsible.
The gating order in code is also kept interpretable: first attention, then 30-minute shock intensity, then confirmation that the first five minutes do not point in the opposite direction.
if signal.current_change_pct.abs() < AttentionThresholdPct {
signal.reason = "ATTENTION_LOW".to_string();
} elseif signal.shock_30_pct.abs() < MinShock30Pct {
signal.reason = "SHOCK_LOW".to_string();
} elseif signal.shock_5_pct.abs() < MinShock30Pct * 0.08 {
signal.reason = "EARLY_INTENSITY_LOW".to_string();
} elseif signal.shock_5_pct.signum() != signal.shock_30_pct.signum() {
signal.reason = "PATH_NOT_MONOTONIC".to_string();
} else {
signal.ready = true;
signal.reason = "READY".to_string();
}
8. Three Operating Modes
The strategy supports three modes:
| Mode | Behavior | Purpose |
|---|---|---|
SHADOW | Scan, calculate, and display signals without creating positions | Validate data and signal direction |
PAPER | Simulate maker fills using real order-book data and trade-through events | Estimate fill rate, slippage, and signal returns |
LIVE | Send real orders through the FMZ exchange object | Small-scale production validation |
PAPER mode does not simply assume that a signal is filled at the displayed quote.
In v0.1.6, the strategy first leaves a two-second propagation grace period, then compares newly observed aggTrade prices with the resting order price.
For a buy order, the observed trade price must be no higher than the resting buy price. For a sell order, the opposite condition applies.
A single entry order may wait for up to 90 seconds, but it is canceled early if the signal becomes invalid, data becomes stale, or the resting price falls more than 10 bps behind the current best quote.
let is_buy =
(execution.order_purpose == "ENTRY" && execution.direction == 1)
|| (execution.order_purpose == "EXIT" && execution.direction == -1);
if is_buy {
symbol.last_trade <= execution.order_price
} else {
symbol.last_trade >= execution.order_price
}
This is still only an approximate fill model.
It does not simulate queue position, and the current version does not record a per-order trade sequence number at order creation. Therefore, it cannot strictly prove that the trade used to infer a fill occurred after the simulated order entered the queue.
PAPER results are appropriate for screening implementation problems. They should not be treated as an exact historical reconstruction of real maker fills.
9. Maker Execution and the Single-Position State Machine
The first version allows at most one event position at a time:
IDLE
└── READY ──> ENTRY_WORKING (GTXMaker)
├── Timeout without fill ──> IDLE
└── Filled ──> POSITION
└── Exit condition ──> EXIT_WORKING
├── Makerfill ──> IDLE
└── Residualposition
──> Market cleanup
Normal entry and exit orders use GTX/Post-Only orders to prevent a limit order from accidentally becoming a taker.
The default target notional is 50 USDT.
Order quantity is not sent simply as:
notional / price
Instead, the strategy first reads CtVal, amount step, price step, and minimum notional from GetMarkets(), then converts the desired quote-currency notional into the actual number of exchange contracts.
let quote_per_contract =
contract_quote_value(spec, meta, price)?;
let amount =
round_amount(spec, notional / quote_per_contract);
let actual_notional =
amount * quote_per_contract;
if actual_notional + 1e-9 < spec.min_notional {
returnErr(format!(
"notional {} < MinNotional {}",
actual_notional,
spec.min_notional
));
}
The first version uses the following fixed exit conditions:
- Position return at or below
-0.4%: stop loss - Position return reaches
+0.6%: take profit - Maximum holding time of 30 minutes
- Signal direction reverses
- Remaining 30-minute Shock falls below 20% of its value at entry, meaning the catalyst has largely decayed
The stop loss is treated as an emergency exit and may use a market order to remove the remaining position immediately.
Normal exits first attempt to use a maker order.
The objective is to control tail risk, not to preserve a zero-fee assumption at all costs.
10. Why “Order Not Found” Immediately After Submission Does Not Mean the Order Does Not Exist
There can be a short propagation delay between:
- the exchange accepting an order;
- the order becoming visible in the open-order list;
- the order becoming queryable through historical-order endpoints.
Therefore, querying immediately after submission and receiving null or “order not found” does not prove that the order was never created.
It certainly does not justify blindly sending another identical order.
The prototype uses a two-second propagation grace period, but it does not block the strategy with Sleep(2000).
The order state machine continues running. During the grace period, temporary invisibility is treated as propagation rather than failure.
After the grace period, the strategy checks, in sequence:
- Current open orders
- Recent historical orders
- Single-order query
This allows WebSocket data, the status panel, and other reconciliation tasks to continue operating while reducing the risk of duplicate orders caused by temporary invisibility.
11. In Live Trading, the Hard Part Is Not Sending an Order — It Is Managing Uncertainty
In LIVE mode, the strategy persists the order intent before calling CreateOrder.
Only after it receives a non-empty order ID and persists the updated state does it clear the pending intent.
The core sequence is only three steps, but the order matters:
- Persist the Intent
- Send the order
- Update local order state only after the result is unambiguous
The following code is a compressed version of the critical path. The full strategy later distinguishes between an explicit maker rejection, a known failure, and an unknown result:
state.pending_intent = Some(intent);
save_state(runtime, state)?;
let create_result = exchange.CreateOrder(
symbol.meta.fmz.as_str(),
side.as_str(),
price,
amount
);
If the network fails at the exact moment when the exchange has accepted the order but the strategy has not yet received the order ID, the strategy does not blindly resend.
Instead, it enters an automatic reconciliation state and uses non-blocking queries against open orders, recent order history, and positions.
If the original order is found, the strategy takes ownership of it.
If a matching position is found, the local position state is reconstructed.
Only after three consecutive reconciliation attempts succeed and the account is confirmed clean does the strategy conclude that the request did not create an order and resume normal operation.
There is another narrower edge case.
If CreateOrder follows its success-return path but returns an empty order-ID string, v0.1.6 retains the Intent and enters a conservative halt state.
Automatic reconciliation still continues, but the halt flag may not clear automatically in exactly the same way as an ordinary “unknown result” branch.
Before unattended LIVE deployment, this empty-ID path should be tested explicitly.
If it occurs, first verify open orders, historical orders, and positions. Do not simply restart and resend.
This design addresses one of the most dangerous realities in automated execution:
A failed return value
≠
The exchange definitely did not execute
The strategy also refuses to enter automatically if the target contract already has an external order or external position.
Cancellation uses only strongly typed order IDs obtained from the current reconciliation query.
Closing direction is delegated to FMZ’s closebuy / closesell semantics instead of manually assembling reduceOnly flags.
12. Why There Are Only Four Parameters

| Parameter | Default | Description |
|---|---|---|
RunMode | SHADOW | Observe, simulate, or execute live |
AttentionThresholdPct | 4.0 | Full-market attention threshold |
MinShock30Pct | 0.8 | Minimum 30-minute mechanical display shock, in percentage points |
OrderNotionalUSDT | 50 | Target notional for the single active position |
The deep-tracking candidate count is fixed at six and is not exposed as a tuning parameter.
Runtime interaction is also deliberately limited to a single pause/resume-entry control.
Having few parameters does not mean the model is simplistic.
The goal is to make the first batch of samples interpretable.
At this stage, the most important questions are not about optimizing the backtest curve. They are:
- Does the displayed Shock in TradFi perpetuals correspond to subsequent price drift?
- Is there a stable gradient as Shock intensity increases?
- After accounting for unfilled orders, funding, and emergency exits, is any return left?
13. A Real WebSocket Failure and How It Was Fixed
A LIVE screenshot from v0.1.1 once showed:
LIVE / v0.1.1 RUNNING
TradFi 174 / selected 0
WS age = 2100 ms
reconnects = 2
scan #1 / RUN
execution = IDLE
halt = false
The REST contract-directory logic and execution state machine had indeed started.
However, continued observation showed that the reconnect counter increased approximately every 20 seconds.
After checking Binance’s current WebSocket documentation, the issue became clear.
v0.1.1 had mixed the all-market !ticker@arr, symbol ticker, aggTrade, kline, and bookTicker subscriptions on /public/stream.
The current USDⓈ-M WebSocket interface separates the stream paths: ticker, trade, and kline-style market streams belong to the market path, while order-book streams such as bookTicker belong to the public path.
Therefore, the screenshot’s WS age only showed recent connection or control-message activity. It did not prove that valid market data was being received.
Likewise, selected 0 could not be interpreted as evidence that no contract had crossed the 4% attention threshold.
This incident demonstrates an important principle:
A successful WebSocket handshake does not prove that the subscription is valid. Liveness monitoring should be refreshed only by real business data.
v0.1.2 made four changes:
- Market and Book data use two separate WebSocket connections and subscription sets.
- Subscription acknowledgements, error replies, and actual market-data messages are handled separately.
- After reconnection, status shows
WAIT_DATArather than pretending that the connection timestamp is the latest market-data timestamp. - When candidates change, detailed streams for candidates that leave the tracked set are unsubscribed so the subscription set does not grow indefinitely.
The healthy-state criteria after the fix are:
- Market status remains
DATA_OK. - Market age repeatedly falls back toward zero as new data arrives.
- Reconnect count does not increase on a fixed ~20-second cycle.
- When candidates exist, Book status should also remain
DATA_OK. - With no candidates, Book status should display
IDLE.
Subsequent LIVE validation exposed another important boundary: a GTX Maker order can be explicitly rejected because the market moves while the request is in transit.
That situation is fundamentally different from a network timeout that leaves the execution result unknown.
Starting in v0.1.3, the strategy preserves both the Rust Err and GetLastError() evidence.
An explicit maker rejection only triggers a short cooldown.
v0.1.4 then removed the manual recovery mechanism for genuinely unknown execution outcomes.
An unresolved Intent is latched, and a non-blocking state machine automatically queries open orders, recent order history, and positions.
If an order is found, the strategy adopts it.
If a matching position is found, the local position state is reconstructed.
Only after three consecutive successful reconciliation cycles show a clean account is the request classified as not having created an order, after which the strategy automatically resumes.
LIVE testing also exposed a repeated-order problem under persistent signals:
Restorderfor20 seconds
→ cancel
→ submit again
→ cancel
→ submit again
This strategy is a time-effect-driven directional strategy, not a market maker that continuously maintains the best quote.
Frequent cancel-replace behavior sacrifices queue priority and can end up chasing a price that has already moved.
v0.1.6 therefore changed the logic so that each continuous signal may create only one maker entry order.
That order may wait for up to 90 seconds.
It is canceled early if:
- the signal becomes invalid;
- market data becomes stale;
- the order falls more than 10 bps behind the current best quote.
Whether the order fills or not, the strategy must then wait until either:
- the direction reverses; or
- attention/Shock falls back into a lower hysteresis reset zone
before the signal is armed again.
This prevents threshold noise from repeatedly generating new orders.
Partial fills are converted directly into a position. The strategy does not chase the remaining target quantity.
The Paper PnL field remains zero in LIVE mode.
It is only a simulation-performance field retained in the shared status layout and should not be used to judge real-account performance.
A future version would benefit from separate LIVE realized-PnL and fee accounting.
Even after the data path is functioning correctly, a runtime screenshot proves only that the system is receiving and processing data.
It does not prove that the signal is valid, and it certainly does not prove that the strategy is profitable.
14. How the Strategy Should Be Validated Instead of Looking Only at Total Return
The most useful next step is to accumulate event-level observations and evaluate them using the following structure.
1. Shock-Intensity Gradient
Bucket events by abs(Shock30) and measure side-adjusted 5-, 15-, and 30-minute forward returns.
A credible mechanism should generally strengthen as Shock intensity rises.
If only one isolated bucket happens to be profitable, that is not stable evidence.
2. Separate Long and Short
The public crypto research shows strong directional asymmetry and regime changes.
TradFi samples must also be evaluated separately by side.
A combined average can easily hide the failure of one side.
3. 24h Placebo Tests
Repeat the same calculation using neighboring windows such as 20h, 21h, 22h, and 23h.
If every horizon appears “effective,” the observed return may simply be trend, reversal, or intraday seasonality rather than a mechanism tied specifically to the 24-hour display window.
4. Fills and Trading Costs
At minimum, record:
- Number of maker orders, fill rate, and timeout rate
- Delay from signal creation to actual fill
- Maximum favorable and adverse movement after entry
- Funding cost, share of emergency market exits, and realized slippage
- Performance under different bid-ask spread conditions
5. New Information Overwhelming the Weak Catalyst
Roll-Out is only a weak catalyst.
Earnings, macroeconomic data, company news, or broad crypto-market moves can completely dominate it.
Event-level samples should therefore tag traditional-market sessions and unusual market conditions so that stronger information shocks are not incorrectly attributed to the display statistic.
15. What the Current Version Explicitly Does Not Do
To avoid misunderstanding, v0.1.6 does not implement:
- Multi-symbol concurrent positions
- Probe / Build / Core staged scaling
- Averaging down against the signal
- Prediction of future full-market ranking positions
- Open-interest, funding-rate, or aggressive-flow filters
- A TradFi return model already proven by historical samples
- A complete queue-position and market-impact model
- Automatic backoff retry after the first historical-data bootstrap failure
- Forced retention of
bookTicker,aggTrade, and kline detailed subscriptions when an instrument with an active order or position falls out of the Top 6 candidate set
These are not “missing advanced features” that must be added immediately.
They are deliberately postponed until the basic hypothesis has been validated.
If there is no stable gradient from:
Shock → Future Return
then adding more parameters will only create more opportunities to overfit.
16. Conclusion
The most interesting part of the 24h Roll-Out effect is not that it appears strange.
It is that it turns a vague behavioral-finance story into an event that can be calculated in advance:
We already know which segment of historical price action will be removed from the rolling statistic next.
But there is still a long chain between a mathematically predictable change in a displayed statistic and genuinely tradable return:
Displayed-number change
↓
Attention
↓
Order flow
↓
Fill probability
↓
Fees and funding
↓
Tail risk
↓
Realizedreturn
Public crypto research suggests that the historical anomaly exists but is fragile.
The TradFi prototype in this article attempts to answer the next question using a more continuous, restrained, and observable implementation.
A credible first version should not rush to prove that it makes money.
It should first ensure that:
- the data path is reliable;
- the signal has a clear meaning;
- every order state can be reconciled;
- failures do not create duplicate orders;
- both positive and negative results are recorded in a form that can be explained later.
That is the role of this FMZ Rust prototype.
Strategy
The strategy is still a prototype and may be adjusted or upgraded as more real-world observations are collected.
It is shared as a research implementation for discussion and learning. Anyone considering live deployment should independently evaluate, analyze, and optimize it.
Risk Warning: This article and the accompanying strategy are intended solely for quantitative research and engineering validation and do not constitute investment advice. Perpetual contracts can experience large gaps, liquidity collapse, abnormal funding rates, and forced liquidation. As more participants discover a historical anomaly, the effect may weaken or disappear.





