feat(scan): add stats.momentum.tsmom@1 intraday momentum detector - #21
Conversation
New single-leg ANOM scan measuring intraday time-series momentum / return continuation as a tradeable signal (sign + magnitude + significance + hit-rate), not just an autocorrelation p-value. Per horizon k, the kernel partitions the return series into non-overlapping k-blocks and fits OLS of next-block on past-block return; the slope is the continuation coefficient (>0 momentum, <0 reversion) with a Student-t t-stat, a directional hit-rate, and the sign(past)*next TSMOM mean. The output frames the dominant hold horizon (selected_hold_bars) and per-k turnover (1/k) so the Quant agent can read natural turnover off the finding. Optional ex-ante trailing-vol scaling (default on). - Registered alphabetically in register_anom_scans (Pattern E); dispatchable on all three surfaces via the shared registry. - Kernel + scan unit tests, happy-path integration test, and a float-free insta schema snapshot. rustfmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Changes Requested
CI failed on cargo clippy --workspace --all-targets -- -D warnings — 2 errors, all in the new TSMOM files. Everything else (build, logic, tests, registration, scope) is clean. Fix the two lints below and re-push.
Issues
Error 1 — crates/miner-core/src/scan/anom/tsmom/kernel.rs:130 — clippy::many_single_char_names
error: 5 bindings with single-character names in scope
--> kernel.rs:130:51
The tsmom_continuation function has five single-char bindings in scope: n (line 131), k (parameter), m (line 150), x (line 166), y (line 167). The existing #[allow(clippy::similar_names)] does not cover this lint. Add a separate allow on the function (or on the module):
#[allow(
clippy::many_single_char_names,
reason = "n/k/m are canonical OLS/TSMOM names; x/y are the standard regressor/regressand pair"
)]
pub(crate) fn tsmom_continuation(returns: &[f64], k: usize) -> Result<TsmomResult, String> {Error 2 — crates/miner-core/src/scan/anom/tsmom/mod.rs:188 — clippy::similar_names
error: binding's name is too similar to existing binding
--> mod.rs:188:17
note: existing binding defined here
--> mod.rs:129:9 (req: &ScanRequest)
res (line 188 in the k-loop) is flagged as too similar to req (the run parameter, line 129). Either rename to something unambiguous (k_result, tsmom_k) or add #[allow(clippy::similar_names)] to the run method. The #[allow(clippy::too_many_lines)] already on run does not cover this lint — it needs its own entry:
#[allow(
clippy::too_many_lines,
reason = "…",
)]
#[allow(
clippy::similar_names,
reason = "res / req are standard kernel-result / scan-request shorthands"
)]
fn run(…) { … }or combined:
#[allow(clippy::too_many_lines, reason = "…")]
#[allow(clippy::similar_names, reason = "res is kernel result; req is the scan request — distinct roles")]
fn run(…) { … }Checks already passed
cargo build --workspace✓- Scope, logic, tests, registration (Pattern E, alphabetical), conventional commit — all pass
- No security issues, no private research data
- Will approve immediately once the two lints are suppressed and CI goes green
Add #[allow(clippy::many_single_char_names)] to tsmom_continuation (n/k/m OLS names, x/y regressor/regressand) and rename the k-loop result binding res -> k_result to clear clippy::similar_names vs the run() req parameter. Pure lint hygiene; no behaviour change.
There was a problem hiding this comment.
Approved
Re-review complete. Both lint issues from my earlier CHANGES_REQUESTED are resolved:
tsmom_continuationcarries#[allow(clippy::many_single_char_names, reason = "...")]directly on the function with a clear justification — ✅- k-loop variable renamed
res→k_resultthroughout the dispatch path — ✅
Checks run
- Scope vs RAD-3839: new
stats.momentum.tsmom@1scan only — no scope creep ✅ - Conventional commits:
feat(scan):(new user-visible feature) +style(scan):(lint fix) — both correct ✅ - CI: green on fix commit
76716a3d(12m47s push + 12m6s PR runs) ✅ - Private research data: none ✅
- Security: no exploit paths, no credential exposure ✅
- Lint suppression: scoped with
reason =attributes, not blanket silencing ✅
PR is clear to merge.
What
New single-leg ANOM scan
stats.momentum.tsmom@1(arity = Single) that measures intraday time-series momentum / return continuation as a tradeable signal — sign + magnitude + significance + directional hit-rate — rather than a bare autocorrelation p-value.Kernel
Per horizon
k, the return series is partitioned into non-overlappingk-blocks (trailing remainder dropped); each block carries its summed return. Consecutive(past, next)block pairs are fit with OLSR_{b+1} = α + β·R_b:β= continuation coefficient (>0momentum,<0mean reversion;≈0under a random walk)df = m-2)sign(past)·nextTSMOM meanNon-overlapping blocks keep the pairs ~independent so the t-stat is honest (overlapping windows would inflate it via induced autocorrelation). Optional
scaling(default on) divides each return by a look-ahead-free trailing volatility before block formation — a global rescale is OLS-scale-invariant, so the normalisation is deliberately time-varying.Surface
params:k_values(default[1,5,10,20], each≥1),scaling(bool, defaulttrue)effect.metric = "tsmom_continuation",value= β at the selected hold horizon (thekwhose positive continuation is most significant; falls back to strongest|t|)effect.p_value/effect.effect_size = {hit_rate}at that samekeffect.extra(parallel per-karrays):continuation_coefs, hit_rates, k_values, p_values, selected_hold_bars, t_stats, tsmom_means, turnover_per_bar.selected_hold_barsframes which horizon persists andturnover_per_bar = 1/klets the consumer read natural turnover straight off the finding.raw.series = {returns, timestamps_ms}Registration
Appended alphabetically inside
register_anom_scans(Pattern E —registry.rs::bootstrap()untouched); dispatchable on all three surfaces (CLI/MCP/HTTP) via the shared registry. Familymod.rsregistration assertion updated.Tests
instaschema snapshot (pins catalogue shape — scan-id, metric, effect-size kind, array names + shapes, params — without coupling to exact continuation floats, which the unit tests cover for sign + significance)rustfmtclean. nextest + clippy validated by CI (the sandbox has no C linker, so the build/test/clippy gates run there).Notes
PhaseScramblepreserves the autocorrelation function and so is not a valid null for a linear-continuation detector. Adding this scan to the per-scan hygiene matrix is a separate, centrally-governed decision.