use crate::trade::Tick;
pub const DEFAULT_HORIZON_US: i64 = 3_000_000;
pub const EXIT_EXECUTION_WINDOW_US: i64 = 3_000_000;
pub const TAKER_FEE_BPS_PER_SIDE: f64 = 2.325;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CostRealismLabels {
pub worst_buy_price: Option<f64>,
pub worst_sell_price: Option<f64>,
pub arrival_price: Option<f64>,
pub twap: Option<f64>,
pub vwap: Option<f64>,
pub exit_worst_buy_price: Option<f64>,
pub exit_worst_sell_price: Option<f64>,
pub roundtrip_cost_bps: Option<f64>,
}
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;
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;
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; }
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;
}
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_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; exit_worst_buy = p;
exit_worst_sell = p;
exit_seen = true;
}
}
}
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 {
last_entry_price
};
let vwap_v = if vwap_den > 0.0 {
Some(vwap_num / vwap_den)
} else {
None };
(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;
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; const H: i64 = DEFAULT_HORIZON_US;
#[test]
fn labels_empty_window_is_all_null_never_zero() {
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);
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);
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() {
let ticks = [
t(100, CLOSE, 999.0, 5.0), 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() {
let ticks = [
t(101, CLOSE + H - 1, 100.0, 1.0), t(102, CLOSE + H, 200.0, 1.0), t(103, CLOSE + H + EXIT_EXECUTION_WINDOW_US - 1, 50.0, 1.0), t(104, CLOSE + H + EXIT_EXECUTION_WINDOW_US, 999.0, 1.0), ];
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() {
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));
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() {
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() {
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() {
let ticks = [
t(101, CLOSE + 10, 102.0, 1.0), t(102, CLOSE + 20, 101.0, 1.0), t(103, CLOSE + H, 100.0, 1.0), t(104, CLOSE + H + 10, 98.0, 1.0), ];
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() {
let h1 = 1_000_000;
let ticks = [
t(101, CLOSE + 500_000, 100.0, 1.0), t(102, CLOSE + 2_000_000, 105.0, 1.0), ];
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)], ];
for ticks in °enerate {
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() {
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}");
}
}
}