Repository navigation
More rules - #23
Merged
Merged
More rules#23
Conversation
… derive crate The buildkite clippy step was red before any rule work: the derive crate failed on needless borrows, a collapsible else-if and and_then(Some), and the library on manual assign operations and a legacy f64 constant. Fixing them first lets every following revision prove it adds no new clippy findings.
…ng trade GenericAggregator::new(rule, true) puts the trade that triggers the rule into both the closing and the next candle, on purpose, so that the close equals the next open. Components that sum over trades (Volume, NumTrades, ...) then count that trade twice, which the docs did not say. with_trigger_trade(rule, TriggerTrade) adds a third mode, ClosingCandle, that ends a candle with its triggering trade and counts every trade once. new() keeps its meaning (false = NextCandle, true = Both), so this is not a breaking change. Tests check trade and volume conservation for all three modes.
The literature on information-driven bars uses hybrid bars (e.g. dollar volume OR volatility, whichever fires first) and bounds on the duration of a bar, so that a burst does not produce a flood of tiny bars and a quiet market still produces bars. These combinators express that over any existing rule. Combined stateful rules must restart when another rule closes the candle, otherwise a volume rule keeps the volume of a candle that a price rule closed and fires early in the next one. AggregationRule gets a provided reset() method (default no-op, so existing implementations keep compiling); TickRule, VolumeRule and RelativePriceRule implement it. Time rules keep their grid. Durations use the trade's own timestamp_resolution().
A fixed tick or volume threshold drifts with the market regime: on Binance BTCUSDT the same 20k trades span about 2 h in 2018 and 3 min in late 2022. Adaptive<R> recalibrates the threshold of the inner rule after each of its triggers to an EWMA of the activity per millisecond times a target candle duration, clamped to [min, max] (the calibration of dollar bars in AFML ch. 2). ThresholdRule exposes threshold()/set_threshold() so any rule can be wrapped; TickRule, VolumeRule and RelativePriceRule implement it. VolumeRule empties a finished bucket with the old threshold before switching, so the carried excess volume stays correct. Adaptive::new checks that the rule accepts both bounds, which makes every clamped threshold valid later.
Range bars close when (high - low) / open of the candle would reach a fraction. They are a standard bar type of trading platforms (Sierra Chart, NinjaTrader) and one of the information bars in the 2026 BTCUSDT comparison (arXiv 2608.26158). Unlike RelativePriceRule, which measures the distance from the open, a candle that swings both ways closes earlier. The trade that would reach the range starts the next range, so with TriggerTrade::NextCandle every closed candle stays below the threshold; a test checks that on the Bitmex sample. Implements reset (new range on the next trade) and ThresholdRule, so Adaptive and the combinators work with it.
Directional-change sampling (Guillaume et al. 1997; Glattfelder, Dupuis, Olsen 2011; Tsang's formal definition) cuts the price path where it reverses by a fraction from the last extreme. It has well-studied scaling laws (the overshoot is about the threshold on average) and needs little state: the trend and its extreme. Candles split at the confirmation points, because an extreme is only known in hindsight. reset() keeps the trend, since it follows the price path rather than the candle. The threshold must be in (0, 1) so that a downward change is reachable. Tests cover both first directions, a pure trend, reset, and that larger thresholds confirm fewer changes on the Bitmex sample.
The symmetric CUSUM filter (Lam & Yam 1997; AFML 2.5.2.1) samples when the price has drifted in one direction: small pullbacks do not restart it and noise around a level does not add up. In a 2025 Financial Innovation study on BTC/ETH 2018-2023, CUSUM sampling with triple-barrier labels beat time bars after costs. reset() restarts both sums but keeps the last price as the reference for the next return. Tests cover drifts with pullbacks, both directions, pure noise, the restart of both sums after firing, and reset.
Volatility bars close when the sum of absolute log returns between trades reaches a threshold, so sampling follows realized price movement. In the BTCUSDT comparison of arXiv 2608.26158, volatility bars from trades had about 69% less serial dependence than bars from minute data. The docs warn that bid-ask bounce adds up without real movement and point to CusumRule, which ignores such noise; a test shows the difference on bouncing prices. reset() restarts the sum and keeps the last price as reference.
Signed-volume bars close when the net taker volume (buys minus sells, from the sign of TakerTrade::size) reaches a threshold in either direction, so balanced flow keeps a candle open and one-sided, informed flow closes it. This is the fixed-threshold form of AFML's volume imbalance bars, whose expected-imbalance threshold is known to collapse or explode (quant.SE #53038); Adaptive gives a threshold that follows the market instead. By::Quote / By::Base work as for VolumeRule. Tests compare it with VolumeRule on balanced flow and cover both directions, By::Base and reset.
Imbalance bars (AFML ch. 2.3.2) close when the net signed flow of a bar exceeds E[T] * |E[b * x]|: the expected bar length times the expected imbalance per trade. The side comes from the sign of TakerTrade::size, which is exact, unlike the book's tick rule. The book's threshold is known to collapse or explode (quant.SE #53038): with balanced flow |E[b * x]| goes to 0 and every trade closes a bar, and learning E[T] from closed bars has an unstable fixed point. FlowBarParams therefore - keeps E[T] fixed by default (mlfinlab's const variant), - floors |E[b * x]| at min_flow_fraction * E[|x|]; a balanced market then gives bars of about (min_flow_fraction * E[T])^2 trades, - clamps a learned E[T] (with_bar_alpha) to [min_ticks, max_ticks]. ThresholdRule reads and sets E[T], so Adaptive can calibrate it to a target duration instead. The shared flow_bars module (FlowSignal, FlowBarParams, Ewma) is reused by the run bars in the next revision. Tests: hand-computed tick and volume cases, seeded random flow (one-sided bars of about E[T], balanced bars set by the floor, learned E[T] within bounds).
Run bars (AFML ch. 2.3.2.3-4) close when the flow of one side within a bar, max(buys, sells), exceeds E[T] * max(E[x * 1_buy], E[x * 1_sell]). The trades of a side need not be consecutive, so a large order executed in slices closes a bar even when other trades are mixed in, where the net flow of an ImbalanceRule partly cancels; a test shows this on alternating flow. Reuses FlowSignal/FlowBarParams/Ewma from the imbalance revision. The expected flow per side is at least half the average flow, so no floor is needed, but balanced flow gives bars somewhat shorter than E[T] and a learned E[T] drifts to its lower bound; the docs say so and recommend a fixed E[T], or Adaptive via ThresholdRule (= E[T]). A test checks that Adaptive<RunRule> settles at bars of the target duration.
The revisions since 13.0.1 add public API (TriggerTrade, the combinators, Adaptive/ThresholdRule, five new rules, the provided AggregationRule::reset) without breaking any, so this is a minor bump. The README usage snippet still said version 12; it now says 13. The derive crate keeps 0.4.1: its changes are lint fixes without any change to the generated code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes:
TriggerTradeenum to choose which candle gets the triggering tradeAnyRule,AllRule,MinDurationandMaxDurationcombinatorsAdaptivewrapper andThresholdRuletraitRangeRulefor range barsDirectionalChangeRuleCusumRule, a symmetric CUSUM filter on log returnsVolatilityRulefor volatility barsSignedVolumeRulefor order flow (delta) barsImbalanceRulefor tick, volume or dollar imbalance barsRunRulefor tick, volume and dollar imbalance bars