Polars-based event-study tools for testing whether retail trading features predict near-term market direction:
- market structure shifts (MSS)
- change in state of delivery (CISD)
- ES/NQ smart money technique divergences (SMT)
- RSI divergences
- volume divergences
- MSS break parameters: trade-through vs close-through, momentum, relative volume, short-term vs intermediate-term structure
- MSS leg context: volume across the impulse leg and direction-aligned RSI momentum across that leg
- MSS leg momentum comparison: right-leg mean RSI, left-leg mean RSI, and right-minus-left momentum delta
- MSS leg volume comparison: right-leg relative volume, left-leg relative volume, and right-minus-left volume delta
- CISD standalone variants: close breaks through the run-start open and the run high/low extreme
- SMT sweep/fail context: which asset swept, which asset failed, reference/invalidation levels, and sweep depth
Raw data and large generated output tables are intentionally ignored by git. Lightweight matplotlib figures in outputs/figures/ are tracked so the README can show current research snapshots. The core research pipeline uses Polars; plotting still converts to pandas at the matplotlib boundary.
Expected local files:
data/es_1m.parquet
data/nq_1m.parquet
Required columns:
datetime_utc, Open, High, Low, Close, Volume
Bullish MSS only counts when price breaks a swing high that is left of the lowest swing-low extremity in the displacement leg. A swing high formed after that low is not eligible as the bullish shift level. Bearish MSS mirrors this: the broken swing low must be left of the highest swing-high extremity.
Bullish CISD anchors on a confirmed short-term swing low with at least three contiguous down-close candles into the swing-low area. Bearish CISD mirrors this at a confirmed short-term swing high with at least three contiguous up-close candles. Up/down close means close-to-close direction.
Each setup emits two standalone CISD variants when a later confirmed bar closes through the setup level:
open: bullish breaks above the open of the top candle in the down-close run; bearish breaks below the open of the bottom candle in the up-close run.extreme: bullish breaks above the maximum high of the run; bearish breaks below the minimum low of the run.
CISD event rows include cisd_break_level_type, cisd_anchor_idx, cisd_run_start_idx, cisd_run_end_idx, cisd_run_length, and cisd_break_level.
SMT events compare aligned ES and NQ bars after each instrument's short-term swing points are confirmed.
- Bullish SMT: one asset trades below its prior swing low while the other asset holds its corresponding prior swing low.
- Bearish SMT: one asset trades above its prior swing high while the other asset holds its corresponding prior swing high.
The swept asset becomes sweeping_asset; the asset that holds becomes failing_asset. Forward returns are labeled on the sweeping asset, so instrument is set to ES or NQ for the instrument actually swept. SMT rows include reference_price, invalidation_level, smt_swing_idx_sweeper, smt_swing_idx_failing, and sweep_depth.
Events are bucketed by America/New_York clock time:
asia: 18:00-00:00 ETlondon: 02:00-05:00 ETny_am: 08:30-12:00 ETny_pm: 13:30-16:00 ETother: all remaining bars
PYTHONPATH=src python3 -m mss_research run --data data --out outputs --plotsUseful faster run:
PYTHONPATH=src python3 -m mss_research run --data data --out outputs --timeframes 15min --horizons 5,15,30,60 --bootstrap 100 --plotsFor all-timeframe comparisons on memory-limited machines, run each timeframe separately and combine the summary CSVs. Use --bootstrap 0 for fast vectorized Polars summaries when confidence intervals are not needed.
Outputs:
outputs/all_events.parquet
outputs/summary.csv
outputs/figures/*.png
These figures are generated from the current local summary output and committed because they are small. Recreate them with the plots command below.
Figures were regenerated on 2026-09-29 from all three timeframes (1min, 5min, 15min; --bootstrap 0), after two labeling fixes. They include SMT.
Labeling fixes (2026-09-29) — figures before this date are not comparable:
- Divergence look-ahead removed. RSI/volume divergence events used to fire on the swing bar, but a k=1 swing is only confirmed one bar later. Forward returns therefore included the confirming bar, which by construction moves in the signal direction, and divergences plotted at 55–62% win rate. Divergence events now fire on the confirmation bar (
event_idx=swing_*_available_idx, with the swing kept indivergence_swing_idx). Divergence win rates are now 49.4–50.8%, and means are −1.1 to +0.3 bp. Every earlier divergence result was an artifact of the look-ahead.- Flat outcomes excluded from win rate.
winused to bealigned_return > 0, so flat outcomes (6–10% of 1min 5-bar returns) counted as losses and pushed everything below 50%. A flat outcome now getswin = nulland is excluded fromwin_rate, so 50% is the no-edge baseline.nand the return statistics still include flat outcomes.
Headline win rates after the fixes (all event types; the full table is written to outputs/headline_stats.csv):
| Event | 1min (5 / 60 bars) | 5min (5 / 60) | 15min (5 / 60) | Mean aligned return, bp (range over all cells) |
|---|---|---|---|---|
| MSS | 47.9 / 49.1 | 48.6 / 49.8 | 48.9 / 50.0 | −0.17 to +0.65 |
| CISD | 47.3 / 49.1 | 48.5 / 49.9 | 48.7 / 49.7 | −0.50 to +0.40 |
| SMT | 51.8 / 50.8 | 50.6 / 49.8 | 50.5 / 49.5 | −0.93 to +0.19 |
| RSI divergence | 50.6 / 50.3 | 50.8 / 49.9 | 50.1 / 49.4 | −0.91 to +0.31 |
| Volume divergence | 50.4 / 50.0 | 50.7 / 49.9 | 50.7 / 49.4 | −1.06 to +0.10 |
SMT in detail (fixed-horizon, labeled on the sweeping asset):
| Timeframe | SMT rows | Win % (5 / 15 / 30 / 60 bars) | Mean aligned bp (5 / 15 / 30 / 60) |
|---|---|---|---|
| 1min | 141,382 | 51.8 / 51.4 / 51.0 / 50.8 | +0.06 / +0.14 / +0.19 / +0.12 |
| 5min | 30,819 | 50.6 / 50.2 / 50.1 / 49.8 | −0.30 / −0.51 / −0.50 / −0.69 |
| 15min | 11,078 | 50.5 / 49.7 / 50.3 / 49.5 | −0.62 / −0.93 / −0.62 / −0.62 |
A few things worth a look, all point estimates and none tested:
- MSS and CISD sit at 47–49% at short horizons, even with flat outcomes excluded, and converge toward 50% with horizon. Both fire on a bar that closes or trades through a level, so this may be short-term reversal or bid-ask bounce off the break bar's close rather than anything structural.
- 1min SMT with ES sweeping is the highest cell, at about 52% for 5 bars. The mean return is +0.06 bp, so the win-rate tilt is not carrying any meaningful return.
The 15min SMT count moved from 10,770 (May run) to 11,078 because the data parquets were updated on 2026-07-18. SMT's largest same-bar overlap is with MSS: 15min phi≈0.097 (4,424 overlaps), 5min phi≈0.080, 1min phi≈0.073. Its overlap with divergences is now about 0 (phi −0.03 to −0.01), because divergence events moved to the confirmation bar.
Statistical caveats — read before quoting any of these numbers:
- No intervals. The run used
--bootstrap 0, so none of these point estimates have confidence intervals. Nothing here rejects, or establishes the absence of, a directional effect. Win rates near 50% are a statement about this sample, not evidence of "no edge". Claiming absence would need an equivalence test against a stated minimum effect, or a minimum detectable effect that excludes it. - Nominal n overstates information. SMT rows are not independent: ES and NQ rows fire on the same bars, SMT heavily co-occurs with MSS, and 15–60 bar forward windows overlap between nearby events. Effective n is materially smaller. Any interval should use a block/cluster bootstrap (e.g. by session day), not the row-level bootstrap.
- The labels are a proxy for the SMT trade. Returns are fixed-horizon closes on the sweeping asset. The computed
invalidation_levelis not used as a stop, there is no target, and labeling the sweeping asset (rather than the asset that held) is a design choice, not part of the SMT definition. The win rates above describe fixed-horizon drift after the event, not the outcome of an SMT trade with a stop at invalidation.
Top-level event comparisons
What it shows: weighted win rate by forward horizon for each event family. A win means the forward return was positive after aligning to event direction; flat outcomes are excluded.
How to read: the dashed 50% line is the no-edge baseline (flat outcomes are excluded from win rate). Lines consistently above it would indicate a directional tendency, subject to the caveats above.
Current read: all families sit within about ±3 points of 50%. MSS/CISD are below 50% at short horizons and SMT (ES sweeps) is slightly above. Without intervals and an effective-n correction, this neither shows nor rules out a standalone directional effect.
What it shows: average forward return after multiplying by event direction. Positive means favorable to the signal direction.
How to read: the dashed zero line is no-edge baseline. Larger positive values imply favorable average drift.
Current read: point-estimate means are close to zero and small relative to intraday return dispersion; no intervals yet.
What it shows: how many event rows contribute to each event-family aggregate.
How to read: small event families are more fragile; large event families dominate weighted charts.
Current read: MSS/CISD/volume-divergence dominate sample size; SMT is meaningful but smaller.
Timeframe and horizon comparisons
What it shows: event-family win rates split by timeframe and forward horizon.
How to read: robust effects should persist across horizons and preferably across timeframes.
Current read: no event family shows a visibly stable above-50% pattern across horizons in point estimate; this is unresolved, not a negative result.
What it shows: average event-direction return by timeframe and horizon.
How to read: positive values mean favorable drift after the event; negative values mean adverse drift.
Current read: averages remain near zero at every timeframe in point estimate; no timeframe stands out, but no intervals have been computed.
What it shows: upper-quartile favorable return by timeframe and horizon.
How to read: high P75 with weak mean/win rate suggests events may mark volatility/opportunity rather than direction.
Current read: P75 is more informative as a volatility/activity marker than as standalone prediction.
Same-bar event overlap / Pearson phi correlation
What it shows: same-bar binary Pearson phi correlation between event families: CISD, MSS, SMT, RSI divergence, and volume divergence.
How to read: positive values mean two event types often occur on the same instrument/bar more than chance; negative values mean they avoid each other. The diagonal is blank because self-correlation is not the research question.
Current read: SMT's strongest same-bar relationship is with MSS at every timeframe (15min phi≈0.097, 5min phi≈0.080), which makes sense because both are sweep/structure concepts.
Distribution / tail-shape charts
What it shows: MSS mean aligned return with P25–P75 band by horizon and MSS subtype.
How to read: a useful directional signal should have mean and quartile band shifted above zero.
Current read: MSS distributions broadly straddle zero, consistent with MSS marking activity more than direction.
What it shows: CISD mean aligned return with P25–P75 band, split by open and extreme break-level variants.
How to read: compare whether the stricter/extreme level changes the return distribution.
Current read: CISD variants need filtering; standalone distributions are not clearly directional.
What it shows: upper-quartile CISD return by timeframe and break-level type.
How to read: useful for asking whether CISD creates favorable excursion potential even if mean/win rate are weak.
Current read: P75 is better treated as opportunity/volatility context than as a complete entry rule.
Break-candle context charts
What it shows: win rate by break-candle body size relative to rolling median range.
How to read: high momentum means the break candle body was large relative to recent range.
Current read: break-candle momentum alone has not produced a strong directional filter.
What it shows: win rate by break-candle volume relative to rolling median volume.
How to read: tests whether above/below-normal volume changes directional outcomes.
Current read: relative volume is more promising as context than as a standalone signal.
MSS leg context charts
What it shows: MSS win rate by strongest direction-aligned RSI value during the shift leg.
How to read: higher bucket means stronger directional RSI during the leg into the break.
Current read: right-leg momentum is useful context to inspect, but not enough alone.
What it shows: MSS win rate by average direction-aligned RSI across the right leg.
How to read: mean RSI smooths the leg momentum rather than taking only the extreme.
Current read: helps separate persistent leg strength from one-bar spike effects.
What it shows: MSS win rate by whether the shift leg has stronger/weaker mean aligned RSI than the prior leg.
How to read: strengthening means right-leg RSI momentum exceeded prior-leg momentum by more than 5 points.
Current read: tests the “displacement should strengthen” idea; still needs conjunction with other filters.
What it shows: MSS win rate by right-leg relative volume.
How to read: high means the displacement leg traded above baseline volume after length normalization.
Current read: volume may describe regime/participation more than direct direction.
What it shows: MSS win rate by whether right-leg relative volume expanded or contracted versus the prior leg.
How to read: expanding means the shift leg had materially higher relative volume than the prior leg.
Current read: useful for testing accumulation/distribution-style hypotheses, but not standalone edge yet.
CISD and session context charts
What it shows: CISD win rate split by open and extreme setup levels.
How to read: compares loose vs stricter CISD confirmation definitions.
Current read: neither variant should be treated as a standalone directional rule without additional filters.
What it shows: win rate by New York session bucket: Asia, London, NY AM, NY PM, other.
How to read: identifies whether event directionality varies by liquidity/time regime.
Current read: session remains an important context filter candidate.
What it shows: MSS win-rate heatmap crossing session with right-leg volume bucket.
How to read: look for localized pockets where time-of-day and participation agree.
Current read: most useful for hypothesis generation; needs sample-size checks before trusting pockets.
PYTHONPATH=src python3 -m mss_research plots --summary outputs/summary.csv --out outputs/figuresGraphs include win rate, mean aligned return, timeframe-by-event comparisons, same-bar event-type correlation heatmaps for CISD/MSS/SMT/RSI-divergence/volume-divergence, P75 aligned-return comparisons, MSS and CISD P25/mean/P75 aligned-return distributions, sample size, break-candle momentum/volume buckets, MSS-leg RSI/volume buckets, right-leg mean RSI, right-vs-left RSI delta, right-vs-left relative-volume delta, CISD break-level type, time-of-day sessions, and session-by-leg-volume heatmaps.
python3 -m pytest -q


















