opendeviationbar-core 13.78.0

Core open deviation bar construction algorithm with temporal integrity guarantees
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
//! Cost-realism forward-window labels (`label_fwd_[exit_]{H}s_*` family).
//!
//! ADR: `docs/adr/2026-07-08-crypto-cost-realism-labels.md` · tracking issue #614.
//!
//! These are **LABELS, never features** (HANDOFF hard-req 1): they read the FORWARD
//! tick window after a bar closes and therefore encode look-ahead by construction.
//! They live in the separate ClickHouse table `open_deviation_bar_labels` (written
//! post-hoc by the backfill runner over the Tier-1 Parquet tick cache) and are NEVER
//! wired into the bar serialization surfaces — `ODB_RECORD_BATCH_NUM_COLUMNS`,
//! `CORE_COLUMNS`, and the feature manifest stay untouched (guard F).
//!
//! Window semantics (ADR D4, pinned — half-open integer µs, breach excluded by TID):
//!
//! ```text
//! entry(tick, bar, H) ⟺ tick.ref_id > bar.last_agg_trade_id
//!                     ∧ tick.timestamp <  bar.close_time + H
//! exit(tick, bar, H)  ⟺ tick.ref_id > bar.last_agg_trade_id
//!                     ∧ bar.close_time + H <= tick.timestamp
//!                     ∧ tick.timestamp < bar.close_time + H + 3s
//! ```
//!
//! Per-column semantics (divergences from the forex twin recorded deliberately):
//!
//! | label                  | definition                                              | oracle gate |
//! |------------------------|---------------------------------------------------------|-------------|
//! | `worst_buy_price`      | max(price) over entry window (worst LONG-entry fill)    | bit-exact   |
//! | `worst_sell_price`     | min(price) over entry window (worst SHORT-entry fill)   | bit-exact   |
//! | `arrival_price`        | FIRST print at/after close+H, bounded by close+H+3s     | bit-exact   |
//! | `twap`                 | Σ(pᵢ·Δtᵢ)/Σ(Δtᵢ), right-endpoint, Δt=0 skipped; fallback = last entry print (forex `HorizonAccum` rule) | ≤1e-9 |
//! | `vwap`                 | Σ(pᵢ·qᵢ)/Σ(qᵢ) — TRUE volume-weighted (ADR §5)          | ≤1e-9       |
//! | `exit_worst_buy_price` | max(price) over exit window (worst short-cover fill)    | bit-exact   |
//! | `exit_worst_sell_price`| min(price) over exit window (worst long-liquidate fill) | bit-exact   |
//! | `roundtrip_cost_bps`   | (worst_buy − exit_worst_sell)/arrival·1e4 + 2·fee       | ≤1e-9       |
//!
//! `arrival_price` pins HANDOFF §3.1 ("first trade at/after close+H") over the forex
//! code's last-tick-before-target reading (ADR §5 pins the handoff). The bound at
//! close+H+3s is exactly what the backfill runner's 6-second day-boundary carry
//! guarantees is present.
//!
//! `roundtrip_cost_bps` normalizes by `arrival_price` (the canonical TCA
//! implementation-shortfall reference) because the ClickHouse DEFAULT expression must
//! be pure over label-table columns (ADR D5); forex normalized by `bid_close`, which
//! is same-table there only because forex labels are inline. NULL-consistent: arrival
//! is NULL ⟺ the exit window is empty ⟹ the exit fill is NULL ⟹ roundtrip NULL.
//!
//! Fee constant (re-pinned at implementation time per ADR D5): Binance global spot
//! VIP5 taker 0.0310% × 0.75 (25% BNB fee deduction) = 0.02325% per side = 2.325 bps
//! per side; both legs taker. Retrieved 2026-07-14 from
//! <https://www.binance.com/en/fee/spotMaker>. Swappable in ClickHouse via
//! `ALTER MODIFY COLUMN DEFAULT` without touching this kernel.
//!
//! Empty window → `None` (→ NULL in ClickHouse), never 0, never a copied close
//! (ADR D4). The transcendental-free rule holds: max/min/select pick existing IEEE
//! elements (residual exactly 0 vs numpy); TWAP/VWAP/roundtrip are division/summation
//! only — no `ln`/`exp` (the bartels lesson).

use crate::trade::Tick;

/// Decided horizon (ADR §9 Q1): H = 3s. Kernel stays horizon-parameterized so
/// 1s/5s/10s are an add-later backfill, not a redesign.
pub const DEFAULT_HORIZON_US: i64 = 3_000_000;

/// Exit-leg EXECUTION window length — fixed 3s for every H (mirrors the forex
/// pending-queue exit accumulators `[close+H, close+H+3s)`).
pub const EXIT_EXECUTION_WINDOW_US: i64 = 3_000_000;

/// Binance global spot VIP5 taker 0.0310% × 0.75 (25% BNB deduction) = 2.325 bps per
/// side. Retrieved 2026-07-14, <https://www.binance.com/en/fee/spotMaker> (ADR D5 —
/// re-pinned from the official schedule, not copied from the handoff estimate).
pub const TAKER_FEE_BPS_PER_SIDE: f64 = 2.325;

/// All 8 labels for one bar anchor. `None` ⇔ empty-window NULL (ADR D4).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CostRealismLabels {
    /// max(price) over the entry window — worst LONG-entry fill. Bit-exact leg.
    pub worst_buy_price: Option<f64>,
    /// min(price) over the entry window — worst SHORT-entry fill. Bit-exact leg.
    pub worst_sell_price: Option<f64>,
    /// First print at/after close+H (bounded by close+H+3s). Bit-exact leg.
    pub arrival_price: Option<f64>,
    /// Right-endpoint time-weighted average price over the entry window. ≤1e-9 leg.
    pub twap: Option<f64>,
    /// True volume-weighted average price over the entry window. ≤1e-9 leg.
    pub vwap: Option<f64>,
    /// max(price) over the exit window — worst short-cover fill. Bit-exact leg.
    pub exit_worst_buy_price: Option<f64>,
    /// min(price) over the exit window — worst long-liquidate fill. Bit-exact leg.
    pub exit_worst_sell_price: Option<f64>,
    /// (worst_buy − exit_worst_sell)/arrival × 1e4 + 2 × TAKER_FEE_BPS_PER_SIDE. ≤1e-9 leg.
    pub roundtrip_cost_bps: Option<f64>,
}

/// Compute all 8 cost-realism labels for ONE bar anchor over a TID-ordered tick
/// slice. Single left-to-right pass; the accumulation order is pinned identical to
/// the Python oracle (`scripts/gen_labels_oracle.py`) so even the continuous legs
/// stay ~1e-13, four orders under the 1e-9 gate.
///
/// * `ticks` — TID-ordered slice covering at least the span from the anchor TID to
///   `close + horizon + 3s`. Ticks at/before the anchor TID or past the exit end
///   are skipped by predicate, so handing a wider slice is correct, just slower.
/// * `last_agg_trade_id` — the completed bar's breach-trade TID (excluded, ADR §5).
/// * `close_time_us` — the completed bar's close time, integer µs.
/// * `horizon_us` — H in µs (`DEFAULT_HORIZON_US` = 3s slice shipping now).
pub fn compute_cost_realism_labels(
    ticks: &[Tick],
    last_agg_trade_id: i64,
    close_time_us: i64,
    horizon_us: i64,
) -> CostRealismLabels {
    debug_assert!(horizon_us > 0, "horizon must be positive µs");
    let entry_end_us = close_time_us + horizon_us;
    let exit_end_us = entry_end_us + EXIT_EXECUTION_WINDOW_US;

    // Entry-leg accumulators.
    let mut entry_seen = false;
    let mut worst_buy = f64::NAN;
    let mut worst_sell = f64::NAN;
    let mut twap_sum = 0.0_f64;
    let mut twap_dt: i64 = 0;
    let mut prev_entry_t: Option<i64> = None;
    let mut last_entry_price = f64::NAN;
    let mut vwap_num = 0.0_f64;
    let mut vwap_den = 0.0_f64;

    // Exit-leg accumulators.
    let mut exit_seen = false;
    let mut arrival = f64::NAN;
    let mut exit_worst_buy = f64::NAN;
    let mut exit_worst_sell = f64::NAN;

    for tick in ticks {
        if tick.ref_id <= last_agg_trade_id {
            continue; // at/before the breach trade — excluded by TID (ADR D4)
        }
        let ts = tick.timestamp;
        if ts < entry_end_us {
            let p = tick.price.to_f64();
            let q = tick.volume.to_f64();
            if entry_seen {
                if p > worst_buy {
                    worst_buy = p;
                }
                if p < worst_sell {
                    worst_sell = p;
                }
            } else {
                worst_buy = p;
                worst_sell = p;
                entry_seen = true;
            }
            // TWAP: right-endpoint rule — the first entry print carries no weight,
            // Δt=0 pairs (same-µs prints) are skipped (forex HorizonAccum::ingest).
            if let Some(pt) = prev_entry_t {
                let dt = ts - pt;
                if dt > 0 {
                    twap_sum += p * dt as f64;
                    twap_dt += dt;
                }
            }
            prev_entry_t = Some(ts);
            last_entry_price = p;
            // VWAP: true volume-weighted, left-to-right.
            vwap_num += p * q;
            vwap_den += q;
        } else if ts < exit_end_us {
            let p = tick.price.to_f64();
            if exit_seen {
                if p > exit_worst_buy {
                    exit_worst_buy = p;
                }
                if p < exit_worst_sell {
                    exit_worst_sell = p;
                }
            } else {
                arrival = p; // FIRST print at/after close+H (HANDOFF §3.1)
                exit_worst_buy = p;
                exit_worst_sell = p;
                exit_seen = true;
            }
        }
        // ts >= exit_end_us: past both windows. No early break — TID order does not
        // formally guarantee timestamp order, and correctness beats micro-optimization
        // here; production hands bounded slices anyway.
    }

    let (worst_buy_price, worst_sell_price, twap, vwap) = if entry_seen {
        let twap_v = if twap_dt > 0 {
            twap_sum / twap_dt as f64
        } else {
            // Single print / all prints at one µs: forex arrival_mid fallback —
            // the LAST entry-window print (pending_queue.rs:128-131).
            last_entry_price
        };
        let vwap_v = if vwap_den > 0.0 {
            Some(vwap_num / vwap_den)
        } else {
            None // zero-volume window: undefined, NULL — never 0, never inf
        };
        (Some(worst_buy), Some(worst_sell), Some(twap_v), vwap_v)
    } else {
        (None, None, None, None)
    };

    let (arrival_price, exit_worst_buy_price, exit_worst_sell_price) = if exit_seen {
        (Some(arrival), Some(exit_worst_buy), Some(exit_worst_sell))
    } else {
        (None, None, None)
    };

    let roundtrip_cost_bps = match (worst_buy_price, exit_worst_sell_price, arrival_price) {
        (Some(wb), Some(ews), Some(arr)) => {
            Some((wb - ews) / arr * 1e4 + 2.0 * TAKER_FEE_BPS_PER_SIDE)
        }
        _ => None,
    };

    CostRealismLabels {
        worst_buy_price,
        worst_sell_price,
        arrival_price,
        twap,
        vwap,
        exit_worst_buy_price,
        exit_worst_sell_price,
        roundtrip_cost_bps,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::fixed_point::FixedPoint;

    /// Hand-built tick: `is_best_match`/quotes irrelevant to the label kernel.
    fn t(ref_id: i64, timestamp: i64, price: f64, qty: f64) -> Tick {
        Tick {
            ref_id,
            price: FixedPoint::from_f64(price),
            volume: FixedPoint::from_f64(qty),
            first_sub_id: ref_id,
            last_sub_id: ref_id,
            timestamp,
            is_buyer_maker: false,
            is_best_match: Some(true),
            best_bid: None,
            best_ask: None,
        }
    }

    const CLOSE: i64 = 1_000_000_000; // arbitrary µs anchor
    const H: i64 = DEFAULT_HORIZON_US;

    #[test]
    fn labels_empty_window_is_all_null_never_zero() {
        // The load-bearing NULL pin (ADR D4): a zero-tick window must yield None on
        // every label — never 0.0, never a copied close, never inf.
        let out = compute_cost_realism_labels(&[], 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, None);
        assert_eq!(out.worst_sell_price, None);
        assert_eq!(out.arrival_price, None);
        assert_eq!(out.twap, None);
        assert_eq!(out.vwap, None);
        assert_eq!(out.exit_worst_buy_price, None);
        assert_eq!(out.exit_worst_sell_price, None);
        assert_eq!(out.roundtrip_cost_bps, None);

        // Ticks exist but ALL are at/before the breach TID → still all-NULL.
        let ticks = [t(99, CLOSE + 1, 100.0, 1.0), t(100, CLOSE + 2, 101.0, 1.0)];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, None);
        assert_eq!(out.roundtrip_cost_bps, None);

        // Ticks exist but all past close+H+3s → still all-NULL.
        let ticks = [t(101, CLOSE + H + EXIT_EXECUTION_WINDOW_US, 100.0, 1.0)];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, None);
        assert_eq!(out.arrival_price, None);
        assert_eq!(out.roundtrip_cost_bps, None);
    }

    #[test]
    fn labels_entry_only_leaves_exit_family_null() {
        let ticks = [
            t(101, CLOSE + 10, 100.0, 2.0),
            t(102, CLOSE + 20, 102.0, 1.0),
            t(103, CLOSE + 30, 99.0, 1.0),
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, Some(102.0));
        assert_eq!(out.worst_sell_price, Some(99.0));
        assert!(out.twap.is_some() && out.vwap.is_some());
        assert_eq!(out.arrival_price, None);
        assert_eq!(out.exit_worst_buy_price, None);
        assert_eq!(out.exit_worst_sell_price, None);
        assert_eq!(out.roundtrip_cost_bps, None, "no exit leg → no roundtrip");
    }

    #[test]
    fn labels_exit_only_leaves_entry_family_null() {
        let ticks = [
            t(101, CLOSE + H + 10, 101.0, 1.0),
            t(102, CLOSE + H + 20, 103.0, 1.0),
            t(103, CLOSE + H + 30, 100.0, 1.0),
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, None);
        assert_eq!(out.worst_sell_price, None);
        assert_eq!(out.twap, None);
        assert_eq!(out.vwap, None);
        assert_eq!(out.arrival_price, Some(101.0), "arrival = FIRST exit-window print");
        assert_eq!(out.exit_worst_buy_price, Some(103.0));
        assert_eq!(out.exit_worst_sell_price, Some(100.0));
        assert_eq!(out.roundtrip_cost_bps, None, "no entry leg → no roundtrip");
    }

    #[test]
    fn labels_breach_trade_excluded_by_tid_even_inside_time_window() {
        // The breach trade (ref_id == last_agg_trade_id) sits inside the entry time
        // window but MUST be excluded (ADR §5 pins TID exclusion).
        let ticks = [
            t(100, CLOSE, 999.0, 5.0), // breach trade — excluded
            t(101, CLOSE + 10, 100.0, 1.0),
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, Some(100.0), "999.0 breach print must not leak in");
        assert_eq!(out.worst_sell_price, Some(100.0));
    }

    #[test]
    fn labels_boundary_microsecond_membership_is_half_open() {
        // ts == close+H belongs to the EXIT window (entry is strict <); ts ==
        // close+H+3s belongs to NEITHER (exit is strict < at its end).
        let ticks = [
            t(101, CLOSE + H - 1, 100.0, 1.0),                        // entry (last µs)
            t(102, CLOSE + H, 200.0, 1.0),                            // exit (first µs) + arrival
            t(103, CLOSE + H + EXIT_EXECUTION_WINDOW_US - 1, 50.0, 1.0), // exit (last µs)
            t(104, CLOSE + H + EXIT_EXECUTION_WINDOW_US, 999.0, 1.0), // outside both
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.worst_buy_price, Some(100.0));
        assert_eq!(out.arrival_price, Some(200.0));
        assert_eq!(out.exit_worst_buy_price, Some(200.0), "999.0 must be outside");
        assert_eq!(out.exit_worst_sell_price, Some(50.0));
    }

    #[test]
    fn labels_twap_single_print_falls_back_to_last_entry_price() {
        // One entry print → Σ(Δt)==0 → TWAP = that print (forex arrival_mid rule).
        let ticks = [t(101, CLOSE + 10, 123.5, 1.0)];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.twap, Some(123.5));

        // Several prints all at ONE µs → still Σ(Δt)==0 → fallback = LAST print.
        let ticks = [
            t(101, CLOSE + 10, 100.0, 1.0),
            t(102, CLOSE + 10, 105.0, 1.0),
            t(103, CLOSE + 10, 102.0, 1.0),
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.twap, Some(102.0), "fallback is the LAST entry print, not first/mean");
    }

    #[test]
    fn labels_twap_hand_pinned_right_endpoint_weighting() {
        // Prints: (t=+0µs, 100), (t=+100µs, 110), (t=+300µs, 120).
        // Right-endpoint: TWAP = (110·100 + 120·200) / 300 = 35000/300 = 116.666…
        // The first print carries NO weight (forex HorizonAccum rule).
        let ticks = [
            t(101, CLOSE, 100.0, 1.0),
            t(102, CLOSE + 100, 110.0, 1.0),
            t(103, CLOSE + 300, 120.0, 1.0),
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        let expect = (110.0 * 100.0 + 120.0 * 200.0) / 300.0;
        assert_eq!(out.twap, Some(expect));
    }

    #[test]
    fn labels_vwap_hand_pinned_true_volume_weighting() {
        // (100, q=1), (110, q=3) → VWAP = (100·1 + 110·3)/4 = 107.5 (NOT mean 105).
        let ticks = [t(101, CLOSE + 10, 100.0, 1.0), t(102, CLOSE + 20, 110.0, 3.0)];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out.vwap, Some(107.5));
    }

    #[test]
    fn labels_roundtrip_hand_pinned_formula_and_fee_constant() {
        // entry worst_buy = 102, exit worst_sell = 98, arrival = 100.
        // slippage = (102 − 98)/100 × 1e4 = 400 bps; + 2×2.325 = 404.65 bps.
        let ticks = [
            t(101, CLOSE + 10, 102.0, 1.0),  // entry
            t(102, CLOSE + 20, 101.0, 1.0),  // entry
            t(103, CLOSE + H, 100.0, 1.0),   // arrival + exit
            t(104, CLOSE + H + 10, 98.0, 1.0), // exit worst sell
        ];
        let out = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        let expect = (102.0 - 98.0) / 100.0 * 1e4 + 2.0 * TAKER_FEE_BPS_PER_SIDE;
        assert_eq!(out.roundtrip_cost_bps, Some(expect));
        assert!((expect - 404.65).abs() < 1e-12);
    }

    #[test]
    fn labels_horizon_parameterized_one_second_slice() {
        // H=1s: a print at +2s is OUTSIDE the 1s entry window but INSIDE its exit
        // window [close+1s, close+4s) — the ladder extension is a parameter, not a
        // redesign (ADR §9 Q1).
        let h1 = 1_000_000;
        let ticks = [
            t(101, CLOSE + 500_000, 100.0, 1.0),   // entry @H=1s
            t(102, CLOSE + 2_000_000, 105.0, 1.0), // exit @H=1s (entry @H=3s)
        ];
        let out1 = compute_cost_realism_labels(&ticks, 100, CLOSE, h1);
        assert_eq!(out1.worst_buy_price, Some(100.0));
        assert_eq!(out1.arrival_price, Some(105.0));
        let out3 = compute_cost_realism_labels(&ticks, 100, CLOSE, H);
        assert_eq!(out3.worst_buy_price, Some(105.0));
        assert_eq!(out3.arrival_price, None);
    }

    #[test]
    fn labels_never_inf_on_degenerate_windows() {
        let degenerate: Vec<Vec<Tick>> = vec![
            vec![],
            vec![t(101, CLOSE + 1, 0.00000001, 0.00000001)],
            vec![t(101, CLOSE + 1, 1e8, 1e-8), t(102, CLOSE + 2, 1e-8, 1e8)],
            vec![t(101, CLOSE + H, 42.0, 1.0)], // exit-only single print
        ];
        for ticks in &degenerate {
            let out = compute_cost_realism_labels(ticks, 100, CLOSE, H);
            for v in [
                out.worst_buy_price,
                out.worst_sell_price,
                out.arrival_price,
                out.twap,
                out.vwap,
                out.exit_worst_buy_price,
                out.exit_worst_sell_price,
                out.roundtrip_cost_bps,
            ] {
                if let Some(x) = v {
                    assert!(x.is_finite(), "label must never be inf/NaN when Some: {x}");
                }
            }
        }
    }

    #[test]
    fn labels_monotonicity_worst_buy_vwap_worst_sell_hand_battery() {
        // Guard-H property (kernel-side leg): worst_buy ≥ vwap ≥ worst_sell per
        // window; entry/exit windows are disjoint → no cross-leg assertion.
        let batteries: Vec<Vec<Tick>> = vec![
            vec![
                t(101, CLOSE + 10, 100.0, 1.0),
                t(102, CLOSE + 20, 104.0, 2.0),
                t(103, CLOSE + 30, 96.0, 3.0),
                t(104, CLOSE + 40, 101.0, 0.5),
            ],
            vec![t(101, CLOSE + 10, 50.0, 1.0)],
            vec![t(101, CLOSE + 10, 7.0, 2.0), t(102, CLOSE + 11, 7.0, 5.0)],
        ];
        for ticks in &batteries {
            let out = compute_cost_realism_labels(ticks, 100, CLOSE, H);
            let (wb, vw, ws) = (
                out.worst_buy_price.unwrap(),
                out.vwap.unwrap(),
                out.worst_sell_price.unwrap(),
            );
            assert!(wb >= vw && vw >= ws, "monotonicity violated: {wb} {vw} {ws}");
        }
    }
}