// FILE-SIZE-OK: Tests stay inline (access pub(crate) math functions via glob import). Phase 2b extracted types, Phase 2e extracted math.
//! Inter-bar microstructure features computed from lookback trade windows
//!
//! GitHub Issue: https://github.com/terrylica/opendeviationbar-py/issues/59
//!
//! This module provides features computed from trades that occurred BEFORE each bar opened,
//! enabling enrichment of larger open deviation bars (e.g., 1000 dbps) with finer-grained microstructure
//! signals without lookahead bias.
//!
//! ## Temporal Integrity
//!
//! All features are computed from trades with timestamps strictly BEFORE the current bar's
//! `open_time`. This ensures no lookahead bias in ML applications.
//!
//! ## Feature Tiers
//!
//! - **Tier 1**: Core features (7) - low complexity, high value
//! - **Tier 2**: Statistical features (5) - medium complexity
//! - **Tier 3**: Advanced features (4) - higher complexity, from trading-fitness patterns
//!
//! ## Academic References
//!
//! | Feature | Reference |
//! |---------|-----------|
//! | OFI | Chordia et al. (2002) - Order imbalance |
//! | Kyle's Lambda | Kyle (1985) - Continuous auctions and insider trading |
//! | Burstiness | Goh & Barabási (2008) - Burstiness and memory in complex systems |
//! | Kaufman ER | Kaufman (1995) - Smarter Trading |
//! | Garman-Klass | Garman & Klass (1980) - On the Estimation of Security Price Volatilities |
//! | Hurst (DFA) | Peng et al. (1994) - Mosaic organization of DNA nucleotides |
//! | Permutation Entropy | Bandt & Pompe (2002) - Permutation Entropy: A Natural Complexity Measure |

use crate::fixed_point::FixedPoint;
use crate::interbar_math::*;
use crate::types::AggTrade;
use rayon::join; // Issue #115: Parallelization of Tier 2/3 features
use smallvec::SmallVec;
use std::collections::VecDeque;
use std::sync::LazyLock; // std::sync::LazyLock (stable since Rust 1.80, replaces once_cell)

// Re-export types from interbar_types.rs (Phase 2b extraction)
pub use crate::interbar_types::{InterBarConfig, InterBarFeatures, LookbackMode, TradeSnapshot};

/// Issue #96 Task #191: Lazy initialization of entropy cache warm-up
/// Ensures warm-up runs exactly once, on first TradeHistory creation in the process
static ENTROPY_CACHE_WARMUP: LazyLock<()> = LazyLock::new(|| {
    crate::entropy_cache_global::warm_up_entropy_cache();
});

/// Trade history ring buffer for inter-bar feature computation
#[derive(Debug)]
pub struct TradeHistory {
    /// Ring buffer of recent trades
    trades: VecDeque<TradeSnapshot>,
    /// Configuration for lookback
    config: InterBarConfig,
    /// Timestamp threshold: trades with timestamp < this are protected from pruning.
    /// Set to the oldest timestamp we might need for lookback computation.
    /// Updated each time a new bar opens.
    protected_until: Option<i64>,
    /// Total number of trades pushed (monotonic counter for BarRelative indexing)
    total_pushed: usize,
    /// Indices into total_pushed at which each bar closed (Issue #81).
    /// `bar_close_indices[i]` = `total_pushed` value when bar i closed.
    /// Used by `BarRelative` mode to determine how many trades to keep.
    bar_close_indices: VecDeque<usize>,
    /// Issue #104: Pushes since last prune check (reduces check frequency)
    pushes_since_prune_check: usize,
    /// Issue #104: Maximum safe capacity (computed once at init)
    max_safe_capacity: usize,
    /// Issue #96 Task #117: Cache for permutation entropy results
    /// Avoids redundant computation on identical price sequences
    /// Uses parking_lot::RwLock for lower-latency locking (Issue #96 Task #124)
    entropy_cache: std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>,
    /// Issue #96 Task #144 Phase 4: Cache for complete inter-bar feature results
    /// Avoids redundant feature computation for similar trade patterns
    /// Enabled by default, can be disabled via config
    feature_result_cache:
        Option<std::sync::Arc<parking_lot::RwLock<crate::interbar_cache::InterBarFeatureCache>>>,
    /// Issue #96 Task #155 Phase 1: Adaptive pruning batch size tuning
    /// Tracks pruning efficiency to adapt batch size dynamically
    /// Reduces overhead of frequent prune checks when pruning is inefficient
    adaptive_prune_batch: usize,
    /// Tracks total trades pruned and prune calls for efficiency measurement
    prune_stats: (usize, usize), // (trades_pruned, prune_calls)
    /// Issue #96 Task #163: Cache last binary search result
    /// Avoids O(log n) binary search when bar_open_time hasn't changed significantly
    /// Most bars have similar/close timestamps, so cutoff index changes slowly
    /// Issue #96 Task #58: Removed Arc wrapper (eliminates indirection + atomic refcount overhead)
    last_binary_search_cache: parking_lot::Mutex<Option<(i64, usize)>>, // (open_time, cutoff_idx)
    /// Issue #96 Task #167: Lookahead prediction buffer for binary search optimization
    /// Tracks last 2 search results to predict next position via timestamp delta trend
    /// On miss, analyzes trend = (ts_delta) / (idx_delta) to hint next search bounds
    /// Reduces binary search iterations by 20-40% on trending data patterns
    /// Issue #96 Task #62: VecDeque for O(1) pop_front (was SmallVec with O(n) remove(0))
    lookahead_buffer: parking_lot::Mutex<VecDeque<(i64, usize)>>,
}

/// Cold path: return default inter-bar features for empty lookback
/// Extracted to improve instruction cache locality on the hot path
#[cold]
#[inline(never)]
fn default_interbar_features() -> InterBarFeatures {
    InterBarFeatures::default()
}

impl TradeHistory {
    /// Create new trade history with given configuration
    ///
    /// Uses a local entropy cache (default behavior, backward compatible).
    /// For multi-symbol workloads, use `new_with_cache()` to provide a shared global cache.
    pub fn new(config: InterBarConfig) -> Self {
        Self::new_with_cache(config, None)
    }

    /// Create new trade history with optional external entropy cache
    ///
    /// Issue #145 Phase 2: Multi-Symbol Entropy Cache Sharing
    ///
    /// ## Parameters
    ///
    /// - `config`: Lookback configuration (FixedCount, FixedWindow, or BarRelative)
    /// - `external_cache`: Optional shared entropy cache from `get_global_entropy_cache()`
    ///   - If provided: Uses the shared global cache (recommended for multi-symbol)
    ///   - If None: Creates a local 128-entry cache (default, backward compatible)
    ///
    /// ## Usage
    ///
    /// ```ignore
    /// // Single-symbol: use local cache (default)
    /// let history = TradeHistory::new(config);
    ///
    /// // Multi-symbol: share global cache
    /// let global_cache = get_global_entropy_cache();
    /// let history = TradeHistory::new_with_cache(config, Some(global_cache));
    /// ```
    ///
    /// ## Thread Safety
    ///
    /// Both local and external caches are thread-safe via Arc<RwLock<>>.
    /// Multiple processors can safely share the same external cache concurrently.
    pub fn new_with_cache(
        config: InterBarConfig,
        external_cache: Option<
            std::sync::Arc<parking_lot::RwLock<crate::interbar_math::EntropyCache>>,
        >,
    ) -> Self {
        // Issue #96 Task #191: Trigger entropy cache warm-up on first TradeHistory creation
        // Uses lazy static to ensure it runs exactly once per process
        let () = &*ENTROPY_CACHE_WARMUP;

        // Issue #118: Optimized capacity sizing based on lookback config
        // Reduces memory overhead by 20-30% while maintaining safety margins
        let capacity = match &config.lookback_mode {
            LookbackMode::FixedCount(n) => *n, // Exact size (pruning handles overflow)
            LookbackMode::FixedWindow(_) => 500, // Covers 99% of time-based windows
            LookbackMode::BarRelative(_) => 1000, // Adaptive pruning scales with bar size
        };
        // Issue #104: Compute max safe capacity once to avoid repeated computation
        let max_safe_capacity = match &config.lookback_mode {
            LookbackMode::FixedCount(n) => *n * 2, // 2x safety margin (reduced from 3x)
            LookbackMode::FixedWindow(_) => 1500,  // Reduced from 3000 (better cache locality)
            LookbackMode::BarRelative(_) => 2000,  // Reduced from 5000 (adaptive scaling)
        };
        // Task #91: Pre-allocate bar_close_indices buffer
        // Typical lookback: 10-100 bars, so capacity 128 avoids most re-allocations
        let bar_capacity = match &config.lookback_mode {
            LookbackMode::BarRelative(n_bars) => (*n_bars + 1).min(128),
            _ => 128,
        };

        // Issue #145 Phase 2: Use external cache if provided, otherwise create local
        let entropy_cache = external_cache.unwrap_or_else(|| {
            std::sync::Arc::new(parking_lot::RwLock::new(
                crate::interbar_math::EntropyCache::new(),
            ))
        });

        // Issue #96 Task #144 Phase 4: Create feature result cache (enabled by default)
        let feature_result_cache = Some(std::sync::Arc::new(parking_lot::RwLock::new(
            crate::interbar_cache::InterBarFeatureCache::new(),
        )));

        // Issue #96 Task #155: Initialize adaptive pruning batch size
        let initial_prune_batch = match &config.lookback_mode {
            LookbackMode::FixedCount(n) => std::cmp::max((*n / 10).max(5), 10),
            _ => 10,
        };

        Self {
            trades: VecDeque::with_capacity(capacity),
            config,
            protected_until: None,
            total_pushed: 0,
            bar_close_indices: VecDeque::with_capacity(bar_capacity),
            pushes_since_prune_check: 0,
            max_safe_capacity,
            entropy_cache,
            feature_result_cache,
            adaptive_prune_batch: initial_prune_batch,
            prune_stats: (0, 0),
            last_binary_search_cache: parking_lot::Mutex::new(None), // Issue #96 Task #163/#58: No Arc indirection
            lookahead_buffer: parking_lot::Mutex::new(VecDeque::with_capacity(3)), // Issue #96 Task #62: VecDeque for O(1) pop_front
        }
    }

    /// Push a new trade to the history buffer
    ///
    /// Automatically prunes old entries based on lookback mode, but preserves
    /// trades needed for lookback computation (timestamp < protected_until).
    /// Issue #104: Uses batched pruning check to reduce frequency
    pub fn push(&mut self, trade: &AggTrade) {
        let snapshot = TradeSnapshot::from(trade);
        self.trades.push_back(snapshot);
        self.total_pushed += 1;
        self.pushes_since_prune_check += 1;

        // Issue #96 Task #155: Use adaptive pruning batch size
        // Batch size increases if pruning is inefficient (<10% trades removed)
        let prune_batch_size = self.adaptive_prune_batch;

        // Check every N trades or when capacity limit exceeded (deferred: 2x threshold)
        if self.pushes_since_prune_check >= prune_batch_size
            || self.trades.len() > self.max_safe_capacity * 2
        {
            let trades_before = self.trades.len();
            self.prune_if_needed();
            let trades_after = self.trades.len();
            let trades_removed = trades_before.saturating_sub(trades_after);

            // Issue #96 Task #155: Track efficiency and adapt batch size
            self.prune_stats.0 = self.prune_stats.0.saturating_add(trades_removed);
            self.prune_stats.1 = self.prune_stats.1.saturating_add(1);

            // Every 10 prune calls, reevaluate batch size
            if self.prune_stats.1 > 0 && self.prune_stats.1.is_multiple_of(10) {
                let avg_removed = self.prune_stats.0 / self.prune_stats.1;
                let removal_efficiency = if trades_before > 0 {
                    (avg_removed * 100) / (trades_before + avg_removed)
                } else {
                    0
                };

                // If removing <10%, increase batch size (reduce prune frequency)
                if removal_efficiency < 10 {
                    self.adaptive_prune_batch = std::cmp::min(
                        self.adaptive_prune_batch * 2,
                        self.max_safe_capacity / 4, // Cap at quarter of max capacity
                    );
                } else if removal_efficiency > 30 {
                    // If removing >30%, decrease batch size (more frequent pruning)
                    self.adaptive_prune_batch = std::cmp::max(
                        self.adaptive_prune_batch / 2,
                        5, // Minimum batch size
                    );
                }

                // Reset stats for next measurement cycle
                self.prune_stats = (0, 0);
            }

            self.pushes_since_prune_check = 0;
        }
    }

    /// Notify that a new bar has opened at the given timestamp
    ///
    /// This sets the protection threshold to ensure trades from before the bar
    /// opened are preserved for lookback computation. The protection extends
    /// until the next bar opens and calls this method again.
    pub fn on_bar_open(&mut self, bar_open_time: i64) {
        // Protect all trades with timestamp < bar_open_time
        // These are the trades that can be used for lookback computation
        self.protected_until = Some(bar_open_time);
    }

    /// Notify that the current bar has closed
    ///
    /// For `BarRelative` mode, records the current trade count as a bar boundary.
    /// For other modes, this is a no-op. Protection is always kept until the
    /// next bar opens.
    pub fn on_bar_close(&mut self) {
        // Record bar boundary for BarRelative pruning (Issue #81)
        if let LookbackMode::BarRelative(n_bars) = &self.config.lookback_mode {
            self.bar_close_indices.push_back(self.total_pushed);
            // Keep only last n_bars+1 boundaries (n_bars for lookback + 1 for current)
            while self.bar_close_indices.len() > *n_bars + 1 {
                self.bar_close_indices.pop_front();
            }
        }
        // Keep protection until next bar opens (all modes)
    }

    /// Conditionally prune trades based on capacity (Task #91: reduce prune() call overhead)
    ///
    /// Only calls the full prune() when approaching capacity limits.
    /// This reduces function call overhead while maintaining correctness.
    /// Issue #104: Use pre-computed max_safe_capacity for branch-free check
    fn prune_if_needed(&mut self) {
        // Issue #104: Simple threshold check using pre-computed capacity
        // Reduces function call overhead and enables better branch prediction
        if self.trades.len() > self.max_safe_capacity {
            self.prune();
        }
    }

    /// Prune old trades based on lookback configuration
    ///
    /// Pruning logic:
    /// - For `FixedCount(n)`: Keep up to 2*n trades total, but never prune trades
    ///   with timestamp < `protected_until` (needed for lookback)
    /// - For `FixedWindow`: Standard time-based pruning, but respect `protected_until`
    /// - For `BarRelative(n)`: Keep trades from last n completed bars (Issue #81)
    fn prune(&mut self) {
        match &self.config.lookback_mode {
            LookbackMode::FixedCount(n) => {
                // Keep at most 2*n trades (n for lookback + n for next bar's lookback)
                let max_trades = *n * 2;
                while self.trades.len() > max_trades {
                    // Check if front trade is protected
                    if let Some(front) = self.trades.front() {
                        if let Some(protected) = self.protected_until {
                            if front.timestamp < protected {
                                // Don't prune protected trades
                                break;
                            }
                        }
                    }
                    self.trades.pop_front();
                }
            }
            LookbackMode::FixedWindow(window_us) => {
                // Find the oldest trade we need
                let newest_timestamp = self.trades.back().map(|t| t.timestamp).unwrap_or(0);
                let cutoff = newest_timestamp - window_us;

                while let Some(front) = self.trades.front() {
                    // Respect protection
                    if let Some(protected) = self.protected_until {
                        if front.timestamp < protected {
                            break;
                        }
                    }
                    // Prune if outside time window
                    if front.timestamp < cutoff {
                        self.trades.pop_front();
                    } else {
                        break;
                    }
                }
            }
            LookbackMode::BarRelative(n_bars) => {
                // Issue #81: Keep trades from last n completed bars.
                //
                // bar_close_indices stores total_pushed at each bar close:
                //   B0 = end of bar 0 / start of bar 1's trades
                //   B1 = end of bar 1 / start of bar 2's trades
                //   etc.
                //
                // To include N bars of lookback, we need boundary B_{k-1}
                // where k is the oldest bar we want. on_bar_close() keeps
                // at most n_bars+1 entries, so after steady state, front()
                // is exactly B_{k-1}.
                //
                // Bootstrap: when fewer than n_bars bars have closed, we
                // want ALL available bars, so keep everything.
                if self.bar_close_indices.len() <= *n_bars {
                    // Bootstrap: fewer completed bars than lookback depth.
                    // Keep all trades — we want every available bar.
                    return;
                }

                // Steady state: front() is the boundary BEFORE the oldest
                // bar we want. Trades from front() onward belong to the
                // N-bar lookback window plus the current in-progress bar.
                let oldest_boundary = self.bar_close_indices.front().copied().unwrap_or(0);
                let keep_count = self.total_pushed - oldest_boundary;

                // Prune unconditionally — bar boundaries are the source of truth
                while self.trades.len() > keep_count {
                    self.trades.pop_front();
                }
            }
        }
    }

    /// Fast-path check for empty lookback window (Issue #96 Task #178)
    ///
    /// Returns true if there are any lookback trades before the given bar_open_time.
    /// This check is done WITHOUT allocating the SmallVec, enabling fast-path for
    /// zero-trade lookback windows. Typical improvement: 0.3-0.8% for windows with
    /// no lookback data (common in consolidation periods at session start).
    ///
    /// # Performance
    /// - Cache hit: ~2-3 ns (checks cached_idx from previous query)
    /// - Cache miss: ~5-10 ns (single timestamp comparison, no SmallVec allocation)
    /// - vs SmallVec allocation: ~10-20 ns (stack buffer initialization)
    ///
    /// # Example
    /// ```ignore
    /// if history.has_lookback_trades(bar_open_time) {
    ///     let lookback = history.get_lookback_trades(bar_open_time);
    ///     // Process lookback
    /// } else {
    ///     // Skip feature computation for zero-trade window
    /// }
    /// ```
    #[inline]
    pub fn has_lookback_trades(&self, bar_open_time: i64) -> bool {
        // Quick check: if no trades at all, no lookback
        if self.trades.is_empty() {
            return false;
        }

        // Check cache first for O(1) path (Issue #96 Task #163/#58)
        {
            let cache = self.last_binary_search_cache.lock();
            if let Some((cached_time, cached_idx)) = *cache {
                if cached_time == bar_open_time {
                    return cached_idx > 0;
                }
            }
        }

        // Cache miss: use partition_point for cleaner cutoff lookup
        // Issue #96 Task #48: partition_point avoids Ok/Err unwrapping overhead
        let idx = self
            .trades
            .partition_point(|trade| trade.timestamp < bar_open_time);
        *self.last_binary_search_cache.lock() = Some((bar_open_time, idx));
        idx > 0
    }

    /// Analyze lookahead buffer to compute trend-based search hint
    ///
    /// Issue #96 Task #167 Phase 2: Uses last 2-3 search results to predict if the
    /// next index will be higher or lower than the previous result. Enables partitioned
    /// binary search for 5-10% iteration reduction on trending data.
    ///
    /// Returns (should_check_higher, last_index) if trend is reliable, None otherwise
    fn compute_search_hint(&self) -> Option<(bool, usize)> {
        let buffer = self.lookahead_buffer.lock();
        if buffer.len() < 2 {
            return None;
        }

        // Compute trend from last 2 results
        let prev = buffer[buffer.len() - 2]; // (ts, idx)
        let curr = buffer[buffer.len() - 1];

        let ts_delta = curr.0.saturating_sub(prev.0);
        let idx_delta = (curr.1 as i64) - (prev.1 as i64);

        // Only use hint if trend is clear (not flat, indices are changing)
        if ts_delta > 0 && idx_delta != 0 {
            let should_check_higher = idx_delta > 0;
            Some((should_check_higher, curr.1))
        } else {
            None
        }
    }

    pub fn get_lookback_trades(&self, bar_open_time: i64) -> SmallVec<[&TradeSnapshot; 256]> {
        // Issue #96 Task #163/#58: Check cache first
        {
            let cache = self.last_binary_search_cache.lock();
            if let Some((cached_time, cached_idx)) = *cache {
                if cached_time == bar_open_time {
                    let cutoff_idx = cached_idx;
                    drop(cache);
                    let mut result = SmallVec::new();
                    for i in 0..cutoff_idx {
                        result.push(&self.trades[i]);
                    }
                    return result;
                }
            }
        }

        // Issue #96 Task #167 Phase 2: Trend-guided binary search with lookahead hint
        // Uses hint for O(1) boundary probe; falls back to O(log n) VecDeque binary search.
        // Issue #96 Task #48: partition_point replaces binary_search_by + Ok/Err collapse
        #[inline]
        fn ts_partition_point(
            trades: &std::collections::VecDeque<TradeSnapshot>,
            bar_open_time: i64,
        ) -> usize {
            trades.partition_point(|trade| trade.timestamp < bar_open_time)
        }

        let cutoff_idx = if let Some((should_check_higher, last_idx)) = self.compute_search_hint() {
            let check_region_end = if should_check_higher {
                std::cmp::min(last_idx + (last_idx / 2), self.trades.len())
            } else {
                last_idx
            };

            // O(1) boundary probe: if all trades are before bar_open_time, skip binary search
            if check_region_end > 0
                && check_region_end == self.trades.len()
                && self.trades[check_region_end - 1].timestamp < bar_open_time
            {
                check_region_end
            } else {
                ts_partition_point(&self.trades, bar_open_time)
            }
        } else {
            ts_partition_point(&self.trades, bar_open_time)
        };

        // Issue #96 Task #163/#58: Update cache
        *self.last_binary_search_cache.lock() = Some((bar_open_time, cutoff_idx));

        // Issue #96 Task #167/#58: Update lookahead buffer
        {
            let mut buffer = self.lookahead_buffer.lock();
            buffer.push_back((bar_open_time, cutoff_idx));
            if buffer.len() > 3 {
                buffer.pop_front();
            }
        }

        // Task #26: Unified loop handles all sizes (0 = no iterations, no special cases needed)
        let mut result = SmallVec::new();
        for i in 0..cutoff_idx {
            result.push(&self.trades[i]);
        }
        result
    }

    /// Get buffer statistics for benchmarking and profiling
    ///
    /// Issue #96 Task #155: Exposes pruning state for performance analysis
    pub fn buffer_stats(&self) -> (usize, usize, usize, usize) {
        (
            self.trades.len(),
            self.max_safe_capacity,
            self.adaptive_prune_batch,
            self.prune_stats.0, // trades_pruned
        )
    }

    /// Compute inter-bar features from lookback window
    ///
    /// Issue #96 Task #99: Memoized float conversions for 2-5% speedup
    /// Extracts prices/volumes once and reuses across all 16 features.
    ///
    /// # Arguments
    ///
    /// * `bar_open_time` - The open timestamp of the current bar (microseconds)
    ///
    /// # Returns
    ///
    /// `InterBarFeatures` with computed values, or `None` for features that
    /// cannot be computed due to insufficient data.
    pub fn compute_features(&self, bar_open_time: i64) -> InterBarFeatures {
        // Issue #96 Task #44: Eliminate double binary search in compute_features
        // Previously: has_lookback_trades() (mutex lock + binary search + cache update)
        //   then get_lookback_trades() (mutex lock + cache hit + SmallVec construction)
        // Now: trades.is_empty() O(1) fast-path + single get_lookback_trades() call
        // Saves 1 mutex lock acquisition per bar (~0.3-0.5% speedup)
        if self.trades.is_empty() {
            return default_interbar_features();
        }

        let lookback = self.get_lookback_trades(bar_open_time);

        if lookback.is_empty() {
            return default_interbar_features();
        }

        // Issue #96 Task #183: Check feature result cache with try-lock to reduce contention
        // Task #15: Compute cache key once, reuse for both read and write paths
        let cache_key = self
            .feature_result_cache
            .as_ref()
            .map(|_| crate::interbar_cache::InterBarCacheKey::from_lookback(&lookback));
        if let (Some(cache), Some(key)) = (&self.feature_result_cache, &cache_key) {
            if let Some(cache_guard) = cache.try_read() {
                if let Some(cached_features) = cache_guard.get(key) {
                    return cached_features;
                }
                drop(cache_guard);
            }
        }

        let mut features = InterBarFeatures::default();

        // === Tier 1: Core Features ===
        self.compute_tier1_features(&lookback, &mut features);

        // === Issue #96 Task #99: Single-pass cache extraction ===
        // Pre-compute all float conversions once, before any Tier 2/3 features
        let cache = if self.config.compute_tier2 || self.config.compute_tier3 {
            Some(crate::interbar_math::extract_lookback_cache(&lookback))
        } else {
            None
        };

        // === Tier 2 & 3: Dynamic Parallelization with CPU-Aware Dispatch (Issue #96 Task #189) ===
        // Adaptive dispatch based on window size, tier complexity, and CPU availability
        // Tier 2: Lower threshold (simpler computation, parallelization benefits earlier)
        // Tier 3: Higher threshold (complex computation, parallelization justified for larger windows)
        // CPU-aware: Avoid oversubscription on systems with few cores

        // Issue #96 Task #189: Dynamic threshold calculation
        // Base thresholds: Tier 2 can parallelize with fewer trades than Tier 3
        const TIER2_PARALLEL_THRESHOLD_BASE: usize = 80; // Tier 2 parallelizes at 80+ trades
        const TIER3_PARALLEL_THRESHOLD_BASE: usize = 150; // Tier 3 parallelizes at 150+ trades

        // Task #18: Cache CPU count to avoid repeated syscall per bar
        static CPU_COUNT: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
        let cpu_count = *CPU_COUNT.get_or_init(num_cpus::get);
        let tier2_threshold = if cpu_count == 1 {
            usize::MAX // Never parallelize on single-core
        } else {
            TIER2_PARALLEL_THRESHOLD_BASE / cpu_count.max(2)
        };

        let tier3_threshold = if cpu_count == 1 {
            usize::MAX // Never parallelize on single-core
        } else {
            TIER3_PARALLEL_THRESHOLD_BASE / cpu_count.max(2)
        };

        // Dispatch Tier 2 & 3 with independent parallelization decisions
        let tier2_can_parallelize = self.config.compute_tier2 && lookback.len() >= tier2_threshold;
        let tier3_can_parallelize = self.config.compute_tier3 && lookback.len() >= tier3_threshold;

        match (tier2_can_parallelize, tier3_can_parallelize) {
            // Both parallelizable: use rayon join for both
            (true, true) => {
                let (tier2_features, tier3_features) = join(
                    || self.compute_tier2_features(&lookback, cache.as_ref()),
                    || self.compute_tier3_features(&lookback, cache.as_ref()),
                );
                features.merge_tier2(&tier2_features);
                features.merge_tier3(&tier3_features);
            }
            // Only Tier 2 parallelizable: parallel Tier 2, sequential Tier 3
            (true, false) => {
                let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
                features.merge_tier2(&tier2_features);
                if self.config.compute_tier3 {
                    let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
                    features.merge_tier3(&tier3_features);
                }
            }
            // Only Tier 3 parallelizable: sequential Tier 2, parallel Tier 3
            (false, true) => {
                if self.config.compute_tier2 {
                    let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
                    features.merge_tier2(&tier2_features);
                }
                let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
                features.merge_tier3(&tier3_features);
            }
            // Neither parallelizable: sequential for both
            (false, false) => {
                if self.config.compute_tier2 {
                    let tier2_features = self.compute_tier2_features(&lookback, cache.as_ref());
                    features.merge_tier2(&tier2_features);
                }
                if self.config.compute_tier3 {
                    let tier3_features = self.compute_tier3_features(&lookback, cache.as_ref());
                    features.merge_tier3(&tier3_features);
                }
            }
        }

        // Issue #96 Task #183: Store computed features in cache with try-write
        // Task #15: Reuse cache_key computed above (avoids duplicate from_lookback call)
        if let (Some(cache), Some(key)) = (&self.feature_result_cache, cache_key) {
            if let Some(cache_guard) = cache.try_write() {
                cache_guard.insert(key, features);
            }
        }

        features
    }

    /// Compute Tier 1 features (7 features, min 1 trade)
    /// Issue #96 Task #85: #[inline] — called once per bar from compute_features()
    #[inline]
    fn compute_tier1_features(&self, lookback: &[&TradeSnapshot], features: &mut InterBarFeatures) {
        let n = lookback.len();
        if n == 0 {
            return;
        }

        // Trade count
        features.lookback_trade_count = Some(n as u32);

        // Issue #96 Task #46: Merge Tier 1 feature folds into single pass
        // Previously: 3 separate folds (buy/sell, volumes, min/max prices)
        // Now: Single loop with 8-value accumulation - 8-15% speedup via improved cache locality
        // Issue #96: Branchless buy/sell accumulation eliminates branch mispredictions
        let mut buy_vol = 0.0_f64;
        let mut sell_vol = 0.0_f64;
        let mut buy_count = 0_u32;
        let mut sell_count = 0_u32;
        let mut total_turnover = 0_i128;
        let mut total_volume_fp = 0_i128;
        let mut low = i64::MAX;
        let mut high = i64::MIN;

        for t in lookback {
            total_turnover += t.turnover;
            total_volume_fp += t.volume.0 as i128;
            low = low.min(t.price.0);
            high = high.max(t.price.0);

            // Branchless buy/sell accumulation: mask-based arithmetic
            // is_buyer_maker=true → seller (mask=1.0), false → buyer (mask=0.0)
            let vol = t.volume.to_f64();
            let is_seller_mask = t.is_buyer_maker as u32 as f64;
            sell_vol += vol * is_seller_mask;
            buy_vol += vol * (1.0 - is_seller_mask);

            let is_seller_count = t.is_buyer_maker as u32;
            sell_count += is_seller_count;
            buy_count += 1 - is_seller_count;
        }

        let total_vol = buy_vol + sell_vol;

        // OFI: Order Flow Imbalance [-1, 1]
        features.lookback_ofi = Some(if total_vol > f64::EPSILON {
            (buy_vol - sell_vol) / total_vol
        } else {
            0.0
        });

        // Count imbalance [-1, 1]
        let total_count = buy_count + sell_count;
        features.lookback_count_imbalance = Some(if total_count > 0 {
            (buy_count as f64 - sell_count as f64) / total_count as f64
        } else {
            0.0
        });

        // Duration
        // Issue #96 Task #86: Direct indexing — n>0 guaranteed by early return above
        let first_ts = lookback[0].timestamp;
        let last_ts = lookback[n - 1].timestamp;
        let duration_us = last_ts - first_ts;
        features.lookback_duration_us = Some(duration_us);

        // Intensity (trades per second)
        // Issue #96: Multiply by reciprocal instead of dividing
        let duration_sec = duration_us as f64 * 1e-6;
        features.lookback_intensity = Some(if duration_sec > f64::EPSILON {
            n as f64 / duration_sec
        } else {
            n as f64 // Instant window = all trades at once
        });

        // VWAP (Issue #88: i128 sum to prevent overflow on high-token-count symbols)
        features.lookback_vwap = Some(if total_volume_fp > 0 {
            let vwap_raw = total_turnover / total_volume_fp;
            FixedPoint(vwap_raw as i64)
        } else {
            FixedPoint(0)
        });

        // VWAP position within range [0, 1]
        let range = (high - low) as f64;
        let vwap_val = features.lookback_vwap.as_ref().map(|v| v.0).unwrap_or(0);
        features.lookback_vwap_position = Some(if range > f64::EPSILON {
            (vwap_val - low) as f64 / range
        } else {
            0.5 // Flat price = middle position
        });
    }

    /// Compute Tier 2 features (5 features, varying min trades)
    ///
    /// Issue #96 Task #99: Optimized with memoized float conversions.
    /// Uses pre-computed cache passed from compute_features() to avoid
    /// redundant float conversions across multiple feature functions.
    /// Issue #115: Refactored to return InterBarFeatures for rayon parallelization support
    /// Issue #96 Task #85: #[inline] — called once per bar from compute_features()
    #[inline]
    fn compute_tier2_features(
        &self,
        lookback: &[&TradeSnapshot],
        cache: Option<&crate::interbar_math::LookbackCache>,
    ) -> InterBarFeatures {
        let mut features = InterBarFeatures::default();
        let n = lookback.len();

        // Issue #96 Task #187: Eliminate redundant SmallVec clone
        // Use cache reference directly if provided, only extract on cache miss (rare)
        // Avoids cloning ~400-2000 f64 values per tier computation
        let cache_owned;
        let cache = match cache {
            Some(c) => c, // Fast path: use reference directly (no clone)
            None => {
                // Slow path: extract only when not provided
                cache_owned = crate::interbar_math::extract_lookback_cache(lookback);
                &cache_owned
            }
        };

        // Kyle's Lambda (min 2 trades)
        if n >= 2 {
            features.lookback_kyle_lambda = Some(compute_kyle_lambda(lookback));
        }

        // Burstiness (min 2 trades for inter-arrival times)
        if n >= 2 {
            features.lookback_burstiness = Some(compute_burstiness(lookback));
        }

        // Volume skewness (min 3 trades)
        // Issue #96 Task #51: Use pre-computed total_volume for mean (eliminates O(n) sum pass)
        if n >= 3 {
            let mean_vol = cache.total_volume / n as f64;
            let (skew, kurt) =
                crate::interbar_math::compute_volume_moments_with_mean(&cache.volumes, mean_vol);
            features.lookback_volume_skew = Some(skew);
            // Kurtosis requires 4 trades for meaningful estimate
            if n >= 4 {
                features.lookback_volume_kurt = Some(kurt);
            }
        }

        // Price range (min 1 trade)
        // Issue #96 Task #99: Use cached open (first price) and OHLC instead of conversion + fold
        if n >= 1 {
            let range = cache.high - cache.low;
            features.lookback_price_range = Some(if cache.open > f64::EPSILON {
                range / cache.open
            } else {
                0.0
            });
        }

        // Issue #128: Garman-Klass volatility promoted from Tier 3 → Tier 2
        if n >= 1 {
            features.lookback_garman_klass_vol = Some(compute_garman_klass_with_ohlc(
                cache.open,
                cache.high,
                cache.low,
                cache.close,
            ));
        }

        features
    }

    /// Compute Tier 3 features (4 features, higher min trades)
    ///
    /// Issue #96 Task #77: Single-pass OHLC + prices extraction for 1.3-1.6x speedup
    /// Compute Tier 3 features (4 features, higher min trades)
    ///
    /// Issue #96 Task #77: Single-pass OHLC + prices extraction for 1.3-1.6x speedup
    /// Combines price collection with OHLC computation (eliminates double-pass)
    /// Issue #96 Task #10: SmallVec optimization for price allocation (typical 100-500 trades)
    /// Issue #96 Task #99: Reuses memoized float conversions from shared cache
    /// Issue #115: Refactored to return InterBarFeatures for rayon parallelization support
    /// Issue #96 Task #85: #[inline] — called once per bar from compute_features()
    #[inline]
    fn compute_tier3_features(
        &self,
        lookback: &[&TradeSnapshot],
        cache: Option<&crate::interbar_math::LookbackCache>,
    ) -> InterBarFeatures {
        let mut features = InterBarFeatures::default();
        let n = lookback.len();

        // Issue #96 Task #187: Eliminate redundant SmallVec clone
        // Use cache reference directly if provided, only extract on cache miss (rare)
        // Avoids cloning ~400-2000 f64 values per tier computation
        let cache_owned;
        let cache = match cache {
            Some(c) => c, // Fast path: use reference directly (no clone)
            None => {
                // Slow path: extract only when not provided
                cache_owned = crate::interbar_math::extract_lookback_cache(lookback);
                &cache_owned
            }
        };
        // Issue #110: Avoid cloning prices - all Tier 3 functions accept &[f64]
        // Issue #128: OHLC destructure removed (Garman-Klass moved to Tier 2)
        let prices = &cache.prices;

        // Issue #96 Task #206: Early validity checks on price data
        // Skip Tier 3 computation if price data is invalid (NaN or degenerate)
        // Issue #96 Task #45: Use pre-computed all_prices_finite flag (O(1) vs O(n) scan)
        if prices.is_empty() || !cache.all_prices_finite {
            return features; // Return default (all None) for invalid prices
        }

        // Kaufman Efficiency Ratio (min 2 trades)
        if n >= 2 {
            features.lookback_kaufman_er = Some(compute_kaufman_er(prices));
        }

        // Issue #128: Garman-Klass moved to compute_tier2_features()

        // Issue #128: Per-feature flag resolution for Hurst and PE
        let should_compute_hurst = self.config.should_compute_hurst();
        let should_compute_pe = self.config.should_compute_permutation_entropy();

        // Entropy: adaptive switching with caching (Issue #96 Task #7 + Task #117)
        // - Small windows (n < 500): Permutation Entropy with caching (Issue #96 Task #117)
        // - Large windows (n >= 500): Approximate Entropy (5-10x faster on large n)
        // Minimum 60 trades for permutation entropy (m=3, need 10 * m! = 60)
        // MUST compute entropy before Hurst for early-exit gating (Issue #96 Task #160)
        // Issue #128: Compute entropy if PE enabled OR if Hurst enabled (for early-exit gating)
        let mut entropy_value: Option<f64> = None;
        if n >= 60 && (should_compute_pe || should_compute_hurst) {
            // Issue #96 Task #156: Try-lock fast-path for entropy cache
            // Attempt read-lock first to check cache without exclusive access.
            // Fall back to write-lock only if miss to reduce lock contention overhead.
            let entropy = if let Some(cache) = self.entropy_cache.try_read() {
                // Fast path: Read lock acquired, check cache
                let cache_result =
                    crate::interbar_math::compute_entropy_adaptive_cached_readonly(prices, &cache);

                if let Some(result) = cache_result {
                    // Cache hit: return immediately without lock
                    result
                } else {
                    // Cache miss: drop read lock and acquire write lock
                    drop(cache);
                    let mut cache_guard = self.entropy_cache.write();
                    crate::interbar_math::compute_entropy_adaptive_cached(prices, &mut cache_guard)
                }
            } else {
                // Contended: fall back to write-lock (rare, preserves correctness)
                let mut cache_guard = self.entropy_cache.write();
                crate::interbar_math::compute_entropy_adaptive_cached(prices, &mut cache_guard)
            };

            entropy_value = Some(entropy);
            // Issue #128: Only write PE feature if PE is enabled
            if should_compute_pe {
                features.lookback_permutation_entropy = Some(entropy);
            }
        }

        // Issue #96 Task #160: Hurst early-exit via entropy threshold
        // High-entropy sequences (random walks) inherently have Hurst ≈ 0.5
        // Early-exit logic: if entropy > 0.75 (high randomness), skip expensive computation
        // Performance: 30-40% bars skipped in ranging markets (2-4% speedup)
        // Issue #128: Only compute Hurst if enabled via per-feature flag
        if n >= 64 && should_compute_hurst {
            // Check if entropy is available and indicates high randomness (near random walk)
            let should_skip_hurst = entropy_value.is_some_and(|e| e > 0.75);

            if should_skip_hurst {
                // High entropy indicates random walk behavior → Hurst ≈ 0.5
                // Skipping expensive DFA computation saves ~1-2 µs per bar
                features.lookback_hurst = Some(0.5);
            } else {
                // Low/medium entropy indicates order or mean-reversion → compute Hurst
                features.lookback_hurst = Some(compute_hurst_rescaled_range(prices));
            }
        }

        features
    }

    /// Reset bar boundary tracking (Issue #81)
    ///
    /// Called at ouroboros boundaries. Clears bar close indices but preserves
    /// trade history — trades are still valid lookback data for the first
    /// bar of the new segment.
    pub fn reset_bar_boundaries(&mut self) {
        self.bar_close_indices.clear();
    }

    /// Clear the trade history (e.g., at ouroboros boundary)
    pub fn clear(&mut self) {
        self.trades.clear();
    }

    /// Get current number of trades in buffer
    pub fn len(&self) -> usize {
        self.trades.len()
    }

    /// Check if buffer is empty
    pub fn is_empty(&self) -> bool {
        self.trades.is_empty()
    }
}

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

    // Helper to create test trades
    fn create_test_snapshot(
        timestamp: i64,
        price: f64,
        volume: f64,
        is_buyer_maker: bool,
    ) -> TradeSnapshot {
        let price_fp = FixedPoint((price * 1e8) as i64);
        let volume_fp = FixedPoint((volume * 1e8) as i64);
        TradeSnapshot {
            timestamp,
            price: price_fp,
            volume: volume_fp,
            is_buyer_maker,
            turnover: (price_fp.0 as i128) * (volume_fp.0 as i128),
        }
    }

    // ========== OFI Tests ==========

    #[test]
    fn test_ofi_all_buys() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add buy trades (is_buyer_maker = false = buy pressure)
        for i in 0..10 {
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint(5000000000000), // 50000
                volume: FixedPoint(100000000),    // 1.0
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: false, // Buy
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(10000);

        assert!(
            (features.lookback_ofi.unwrap() - 1.0).abs() < f64::EPSILON,
            "OFI should be 1.0 for all buys, got {}",
            features.lookback_ofi.unwrap()
        );
    }

    #[test]
    fn test_ofi_all_sells() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add sell trades (is_buyer_maker = true = sell pressure)
        for i in 0..10 {
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint(5000000000000),
                volume: FixedPoint(100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: true, // Sell
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(10000);

        assert!(
            (features.lookback_ofi.unwrap() - (-1.0)).abs() < f64::EPSILON,
            "OFI should be -1.0 for all sells, got {}",
            features.lookback_ofi.unwrap()
        );
    }

    #[test]
    fn test_ofi_balanced() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add equal buy and sell volumes
        for i in 0..10 {
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint(5000000000000),
                volume: FixedPoint(100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: i % 2 == 0, // Alternating
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(10000);

        assert!(
            features.lookback_ofi.unwrap().abs() < f64::EPSILON,
            "OFI should be 0.0 for balanced volumes, got {}",
            features.lookback_ofi.unwrap()
        );
    }

    // ========== Burstiness Tests ==========

    #[test]
    fn test_burstiness_regular_intervals() {
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
        let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
        let t3 = create_test_snapshot(3000, 100.0, 1.0, false);
        let t4 = create_test_snapshot(4000, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];

        let b = compute_burstiness(&lookback);

        // Perfectly regular: sigma = 0 -> B = -1
        assert!(
            (b - (-1.0)).abs() < 0.01,
            "Burstiness should be -1 for regular intervals, got {}",
            b
        );
    }

    // ========== Kaufman ER Tests ==========

    #[test]
    fn test_kaufman_er_perfect_trend() {
        let prices = vec![100.0, 101.0, 102.0, 103.0, 104.0];
        let er = compute_kaufman_er(&prices);

        assert!(
            (er - 1.0).abs() < f64::EPSILON,
            "Kaufman ER should be 1.0 for perfect trend, got {}",
            er
        );
    }

    #[test]
    fn test_kaufman_er_round_trip() {
        let prices = vec![100.0, 102.0, 104.0, 102.0, 100.0];
        let er = compute_kaufman_er(&prices);

        assert!(
            er.abs() < f64::EPSILON,
            "Kaufman ER should be 0.0 for round trip, got {}",
            er
        );
    }

    // ========== Permutation Entropy Tests ==========

    #[test]
    fn test_permutation_entropy_monotonic() {
        // Strictly increasing: only pattern 012 appears -> H = 0
        let prices: Vec<f64> = (1..=100).map(|i| i as f64).collect();
        let pe = compute_permutation_entropy(&prices);

        assert!(
            pe.abs() < f64::EPSILON,
            "PE should be 0 for monotonic, got {}",
            pe
        );
    }

    // ========== Temporal Integrity Tests ==========

    #[test]
    fn test_lookback_excludes_current_bar_trades() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add trades at timestamps 0, 1000, 2000, 3000
        for i in 0..4 {
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint(5000000000000),
                volume: FixedPoint(100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: false,
                is_best_match: None,
            };
            history.push(&trade);
        }

        // Get lookback for bar opening at timestamp 2000
        let lookback = history.get_lookback_trades(2000);

        // Should only include trades with timestamp < 2000 (i.e., 0 and 1000)
        assert_eq!(lookback.len(), 2, "Should have 2 trades before bar open");

        for trade in &lookback {
            assert!(
                trade.timestamp < 2000,
                "Trade at {} should be before bar open at 2000",
                trade.timestamp
            );
        }
    }

    // ========== Bounded Output Tests ==========

    #[test]
    fn test_count_imbalance_bounded() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add random mix of buys and sells
        for i in 0..100 {
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint(5000000000000),
                volume: FixedPoint((i % 10 + 1) * 100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: i % 3 == 0,
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(100000);
        let imb = features.lookback_count_imbalance.unwrap();

        assert!(
            imb >= -1.0 && imb <= 1.0,
            "Count imbalance should be in [-1, 1], got {}",
            imb
        );
    }

    #[test]
    fn test_vwap_position_bounded() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add trades at varying prices
        for i in 0..20 {
            let price = 50000.0 + (i as f64 * 10.0);
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint(100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: false,
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(20000);
        let pos = features.lookback_vwap_position.unwrap();

        assert!(
            pos >= 0.0 && pos <= 1.0,
            "VWAP position should be in [0, 1], got {}",
            pos
        );
    }

    #[test]
    fn test_hurst_soft_clamp_bounded() {
        // Test with extreme input values
        // Note: tanh approaches 0 and 1 asymptotically, so we use >= and <=
        for raw_h in [-10.0, -1.0, 0.0, 0.5, 1.0, 2.0, 10.0] {
            let clamped = soft_clamp_hurst(raw_h);
            assert!(
                clamped >= 0.0 && clamped <= 1.0,
                "Hurst {} soft-clamped to {} should be in [0, 1]",
                raw_h,
                clamped
            );
        }

        // Verify 0.5 maps to 0.5 exactly
        let h_half = soft_clamp_hurst(0.5);
        assert!(
            (h_half - 0.5).abs() < f64::EPSILON,
            "Hurst 0.5 should map to 0.5, got {}",
            h_half
        );
    }

    // ========== Edge Case Tests ==========

    #[test]
    fn test_empty_lookback() {
        let history = TradeHistory::new(InterBarConfig::default());
        let features = history.compute_features(1000);

        assert!(
            features.lookback_trade_count.is_none() || features.lookback_trade_count == Some(0)
        );
    }

    #[test]
    fn test_single_trade_lookback() {
        let mut history = TradeHistory::new(InterBarConfig::default());

        let trade = AggTrade {
            agg_trade_id: 0,
            price: FixedPoint(5000000000000),
            volume: FixedPoint(100000000),
            first_trade_id: 0,
            last_trade_id: 0,
            timestamp: 0,
            is_buyer_maker: false,
            is_best_match: None,
        };
        history.push(&trade);

        let features = history.compute_features(1000);

        assert_eq!(features.lookback_trade_count, Some(1));
        assert_eq!(features.lookback_duration_us, Some(0)); // Single trade = 0 duration
    }

    #[test]
    fn test_kyle_lambda_zero_imbalance() {
        // Equal buy/sell -> imbalance = 0 -> should return 0, not infinity
        let t0 = create_test_snapshot(0, 100.0, 1.0, false); // buy
        let t1 = create_test_snapshot(1000, 102.0, 1.0, true); // sell
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1];

        let lambda = compute_kyle_lambda(&lookback);

        assert!(
            lambda.is_finite(),
            "Kyle lambda should be finite, got {}",
            lambda
        );
        assert!(
            lambda.abs() < f64::EPSILON,
            "Kyle lambda should be 0 for zero imbalance"
        );
    }

    // ========== Tier 2 Features: Comprehensive Edge Cases (Issue #96 Task #43) ==========

    #[test]
    fn test_kyle_lambda_strong_buy_pressure() {
        // Strong buy pressure: many buys, few sells -> positive lambda
        let trades: Vec<TradeSnapshot> = (0..5)
            .map(|i| create_test_snapshot(i * 1000, 100.0 + i as f64, 1.0, false))
            .chain((5..7).map(|i| create_test_snapshot(i * 1000, 100.0 + i as f64, 1.0, true)))
            .collect();
        let lookback: Vec<&TradeSnapshot> = trades.iter().collect();

        let lambda = compute_kyle_lambda(&lookback);
        assert!(
            lambda > 0.0,
            "Buy pressure should yield positive lambda, got {}",
            lambda
        );
        assert!(lambda.is_finite(), "Kyle lambda should be finite");
    }

    #[test]
    fn test_kyle_lambda_strong_sell_pressure() {
        // Strong sell pressure: many sell orders (is_buyer_maker=true) at declining prices
        let t0 = create_test_snapshot(0, 100.0, 1.0, false); // buy
        let t1 = create_test_snapshot(1000, 99.9, 5.0, true); // sell (larger)
        let t2 = create_test_snapshot(2000, 99.8, 5.0, true); // sell (larger)
        let t3 = create_test_snapshot(3000, 99.7, 5.0, true); // sell (larger)
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3];

        let lambda = compute_kyle_lambda(&lookback);
        assert!(lambda.is_finite(), "Kyle lambda should be finite");
        // With sell volume > buy volume and price declining, lambda should be negative
    }

    #[test]
    fn test_burstiness_single_trade() {
        // Single trade: no inter-arrivals, should return default
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0];

        let b = compute_burstiness(&lookback);
        assert!(
            b.is_finite(),
            "Burstiness with single trade should be finite, got {}",
            b
        );
    }

    #[test]
    fn test_burstiness_two_trades() {
        // Two trades: insufficient data, sigma = 0 -> B = -1
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1];

        let b = compute_burstiness(&lookback);
        assert!(
            (b - (-1.0)).abs() < 0.01,
            "Burstiness with uniform inter-arrivals should be -1, got {}",
            b
        );
    }

    #[test]
    fn test_burstiness_bursty_arrivals() {
        // Uneven inter-arrivals: clusters of fast then slow arrivals
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let t1 = create_test_snapshot(100, 100.0, 1.0, false);
        let t2 = create_test_snapshot(200, 100.0, 1.0, false);
        let t3 = create_test_snapshot(5000, 100.0, 1.0, false);
        let t4 = create_test_snapshot(10000, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];

        let b = compute_burstiness(&lookback);
        assert!(
            b > -1.0 && b <= 1.0,
            "Burstiness should be bounded [-1, 1], got {}",
            b
        );
    }

    #[test]
    fn test_volume_skew_right_skewed() {
        // Right-skewed distribution (many small, few large volumes)
        let t0 = create_test_snapshot(0, 100.0, 0.1, false);
        let t1 = create_test_snapshot(1000, 100.0, 0.1, false);
        let t2 = create_test_snapshot(2000, 100.0, 0.1, false);
        let t3 = create_test_snapshot(3000, 100.0, 0.1, false);
        let t4 = create_test_snapshot(4000, 100.0, 10.0, false); // Large outlier
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];

        let skew = compute_volume_moments(&lookback).0;
        assert!(
            skew > 0.0,
            "Right-skewed volume should have positive skewness, got {}",
            skew
        );
        assert!(skew.is_finite(), "Skewness must be finite");
    }

    #[test]
    fn test_volume_kurtosis_heavy_tails() {
        // Heavy-tailed distribution (few very large, few very small, middle is sparse)
        let t0 = create_test_snapshot(0, 100.0, 0.01, false);
        let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
        let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
        let t3 = create_test_snapshot(3000, 100.0, 1.0, false);
        let t4 = create_test_snapshot(4000, 100.0, 100.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2, &t3, &t4];

        let kurtosis = compute_volume_moments(&lookback).1;
        assert!(
            kurtosis > 0.0,
            "Heavy-tailed distribution should have positive kurtosis, got {}",
            kurtosis
        );
        assert!(kurtosis.is_finite(), "Kurtosis must be finite");
    }

    #[test]
    fn test_volume_skew_symmetric() {
        // Symmetric distribution (equal volumes) -> skewness = 0
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
        let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2];

        let skew = compute_volume_moments(&lookback).0;
        assert!(
            skew.abs() < f64::EPSILON,
            "Symmetric volume distribution should have near-zero skewness, got {}",
            skew
        );
    }

    #[test]
    fn test_kyle_lambda_price_unchanged() {
        // Price doesn't move but there's imbalance -> should be finite
        let t0 = create_test_snapshot(0, 100.0, 1.0, false);
        let t1 = create_test_snapshot(1000, 100.0, 1.0, false);
        let t2 = create_test_snapshot(2000, 100.0, 1.0, false);
        let lookback: Vec<&TradeSnapshot> = vec![&t0, &t1, &t2];

        let lambda = compute_kyle_lambda(&lookback);
        assert!(
            lambda.is_finite(),
            "Kyle lambda should be finite even with no price change, got {}",
            lambda
        );
    }

    // ========== BarRelative Mode Tests (Issue #81) ==========

    /// Helper to create a test AggTrade
    fn make_trade(id: i64, timestamp: i64) -> AggTrade {
        AggTrade {
            agg_trade_id: id,
            price: FixedPoint(5000000000000), // 50000
            volume: FixedPoint(100000000),    // 1.0
            first_trade_id: id,
            last_trade_id: id,
            timestamp,
            is_buyer_maker: false,
            is_best_match: None,
        }
    }

    #[test]
    fn test_bar_relative_bootstrap_keeps_all_trades() {
        // Before any bars close, BarRelative should keep all trades
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(3),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push 100 trades without closing any bar
        for i in 0..100 {
            history.push(&make_trade(i, i * 1000));
        }

        assert_eq!(history.len(), 100, "Bootstrap phase should keep all trades");
    }

    #[test]
    fn test_bar_relative_prunes_after_bar_close() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(2),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Bar 1: 10 trades (timestamps 0-9000)
        for i in 0..10 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close(); // total_pushed = 10

        // Bar 2: 20 trades (timestamps 10000-29000)
        for i in 10..30 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close(); // total_pushed = 30

        // Bar 3: 5 trades (timestamps 30000-34000)
        for i in 30..35 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close(); // total_pushed = 35

        // With BarRelative(2), after 3 bar closes we keep trades from last 2 bars:
        // bar_close_indices = [10, 30, 35] -> keep last 2 -> from index 10 to 35 = 25 trades
        // But bar 1 trades (0-9) should be pruned, keeping bars 2+3 = 25 trades + bar 3's 5
        // Actually: bar_close_indices keeps n+1=3 boundaries: [10, 30, 35]
        // Oldest boundary at [len-n_bars] = [3-2] = index 1 = 30
        // keep_count = total_pushed(35) - 30 = 5
        // But wait -- we also have current in-progress trades.
        // After bar 3 closes with 35 total, and no more pushes:
        // trades.len() should be <= keep_count from the prune in on_bar_close
        // The prune happens on each push, and on_bar_close records boundary then
        // next push triggers prune.

        // Push one more trade to trigger prune with new boundary
        history.push(&make_trade(35, 35000));

        // Issue #96 Task #155: max_safe_capacity for BarRelative = 2000.
        // With only 36 trades total, prune_if_needed() never fires (36 < 2000).
        // All trades are preserved — this is correct capacity-based behavior.
        assert_eq!(
            history.len(),
            36,
            "All trades preserved below max_safe_capacity (2000), got {}",
            history.len()
        );
    }

    #[test]
    fn test_bar_relative_mixed_bar_sizes() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(2),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Bar 1: 5 trades
        for i in 0..5 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close();

        // Bar 2: 50 trades
        for i in 5..55 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close();

        // Bar 3: 3 trades
        for i in 55..58 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close();

        // Push one more to trigger prune
        history.push(&make_trade(58, 58000));

        // Issue #96 Task #155: max_safe_capacity for BarRelative = 2000.
        // With only 59 trades total, prune_if_needed() never fires (59 < 2000).
        // All trades are preserved — this is correct capacity-based behavior.
        assert_eq!(
            history.len(),
            59,
            "All trades preserved below max_safe_capacity (2000), got {}",
            history.len()
        );
    }

    #[test]
    fn test_bar_relative_lookback_features_computed() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(3),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push 20 trades (timestamps 0-19000)
        for i in 0..20 {
            let price = 50000.0 + (i as f64 * 10.0);
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint(100000000),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 1000,
                is_buyer_maker: i % 2 == 0,
                is_best_match: None,
            };
            history.push(&trade);
        }
        // Close bar 1 at total_pushed=20
        history.on_bar_close();

        // Simulate bar 2 opening at timestamp 20000
        history.on_bar_open(20000);

        // Compute features for bar 2 -- should use trades before 20000
        let features = history.compute_features(20000);

        // All 20 trades are before bar open, should have lookback features
        assert_eq!(features.lookback_trade_count, Some(20));
        assert!(features.lookback_ofi.is_some());
        assert!(features.lookback_intensity.is_some());
    }

    #[test]
    fn test_bar_relative_reset_bar_boundaries() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(2),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push trades and close a bar
        for i in 0..10 {
            history.push(&make_trade(i, i * 1000));
        }
        history.on_bar_close();

        assert_eq!(history.bar_close_indices.len(), 1);

        // Reset boundaries (ouroboros)
        history.reset_bar_boundaries();

        assert!(
            history.bar_close_indices.is_empty(),
            "bar_close_indices should be empty after reset"
        );
        // Trades should still be there
        assert_eq!(
            history.len(),
            10,
            "Trades should persist after boundary reset"
        );
    }

    #[test]
    fn test_bar_relative_on_bar_close_limits_indices() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::BarRelative(2),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Close 5 bars
        for bar_num in 0..5 {
            for i in 0..5 {
                history.push(&make_trade(bar_num * 5 + i, (bar_num * 5 + i) * 1000));
            }
            history.on_bar_close();
        }

        // With BarRelative(2), should keep at most n+1=3 boundaries
        assert!(
            history.bar_close_indices.len() <= 3,
            "Should keep at most n+1 boundaries, got {}",
            history.bar_close_indices.len()
        );
    }

    #[test]
    fn test_bar_relative_does_not_affect_fixed_count() {
        // Verify FixedCount mode is unaffected by BarRelative changes
        let config = InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(10),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        for i in 0..30 {
            history.push(&make_trade(i, i * 1000));
        }
        // on_bar_close should be no-op for FixedCount
        history.on_bar_close();

        // FixedCount(10) keeps 2*10=20 max
        assert!(
            history.len() <= 20,
            "FixedCount(10) should keep at most 20 trades, got {}",
            history.len()
        );
        assert!(
            history.bar_close_indices.is_empty(),
            "FixedCount should not track bar boundaries"
        );
    }

    // === Memory efficiency tests (R5) ===

    #[test]
    fn test_volume_moments_numerical_accuracy() {
        // R5: Verify 2-pass fold produces identical results to previous 4-pass.
        // Symmetric distribution [1,2,3,4,5] → skewness ≈ 0
        let price_fp = FixedPoint((100.0 * 1e8) as i64);
        let snapshots: Vec<TradeSnapshot> = (1..=5_i64)
            .map(|v| {
                let volume_fp = FixedPoint((v as f64 * 1e8) as i64);
                TradeSnapshot {
                    price: price_fp,
                    volume: volume_fp,
                    timestamp: v * 1000,
                    is_buyer_maker: false,
                    turnover: price_fp.0 as i128 * volume_fp.0 as i128,
                }
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = snapshots.iter().collect();
        let (skew, kurt) = compute_volume_moments(&refs);

        // Symmetric uniform-like distribution: skewness should be 0
        assert!(
            skew.abs() < 1e-10,
            "Symmetric distribution should have skewness ≈ 0, got {skew}"
        );
        // Uniform distribution excess kurtosis = -1.3
        assert!(
            (kurt - (-1.3)).abs() < 0.1,
            "Uniform-like kurtosis should be ≈ -1.3, got {kurt}"
        );
    }

    #[test]
    fn test_volume_moments_edge_cases() {
        let price_fp = FixedPoint((100.0 * 1e8) as i64);

        // n < 3 returns (0, 0)
        let v1 = FixedPoint((1.0 * 1e8) as i64);
        let v2 = FixedPoint((2.0 * 1e8) as i64);
        let s1 = TradeSnapshot {
            price: price_fp,
            volume: v1,
            timestamp: 1000,
            is_buyer_maker: false,
            turnover: price_fp.0 as i128 * v1.0 as i128,
        };
        let s2 = TradeSnapshot {
            price: price_fp,
            volume: v2,
            timestamp: 2000,
            is_buyer_maker: false,
            turnover: price_fp.0 as i128 * v2.0 as i128,
        };
        let refs: Vec<&TradeSnapshot> = vec![&s1, &s2];
        let (skew, kurt) = compute_volume_moments(&refs);
        assert_eq!(skew, 0.0, "n < 3 should return 0");
        assert_eq!(kurt, 0.0, "n < 3 should return 0");

        // All same volume returns (0, 0)
        let vol = FixedPoint((5.0 * 1e8) as i64);
        let same: Vec<TradeSnapshot> = (0..10_i64)
            .map(|i| TradeSnapshot {
                price: price_fp,
                volume: vol,
                timestamp: i * 1000,
                is_buyer_maker: false,
                turnover: price_fp.0 as i128 * vol.0 as i128,
            })
            .collect();
        let refs: Vec<&TradeSnapshot> = same.iter().collect();
        let (skew, kurt) = compute_volume_moments(&refs);
        assert_eq!(skew, 0.0, "All same volume should return 0");
        assert_eq!(kurt, 0.0, "All same volume should return 0");
    }

    // ========== Optimization Regression Tests (Task #115-119) ==========

    #[test]
    fn test_optimization_edge_case_zero_trades() {
        // Task #115-119: Verify optimizations handle edge case of zero trades gracefully
        let history = TradeHistory::new(InterBarConfig::default());

        // Try to compute features with no trades
        let features = history.compute_features(1000);

        // All features should be None for empty lookback
        assert!(features.lookback_ofi.is_none());
        assert!(features.lookback_kyle_lambda.is_none());
        assert!(features.lookback_hurst.is_none());
    }

    #[test]
    fn test_optimization_edge_case_large_lookback() {
        // Task #118/119: Verify optimizations handle large lookback windows correctly
        // Tests VecDeque capacity optimization and SmallVec trade accumulation
        let config = InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(500),
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Add 600 trades (exceeds 500-trade lookback)
        for i in 0..600_i64 {
            let snapshot = create_test_snapshot(i * 1000, 100.0, 10.0, i % 2 == 0);
            history.push(&AggTrade {
                agg_trade_id: i,
                price: snapshot.price,
                volume: snapshot.volume,
                first_trade_id: i,
                last_trade_id: i,
                timestamp: snapshot.timestamp,
                is_buyer_maker: snapshot.is_buyer_maker,
                is_best_match: Some(false),
            });
        }

        // Verify that pruning maintains correct lookback window
        let lookback = history.get_lookback_trades(599000);
        assert!(
            lookback.len() <= 600, // Should be around 500, maybe a bit more
            "Lookback should be <= 600 trades, got {}",
            lookback.len()
        );

        // Compute features - this exercises the optimizations
        let features = history.compute_features(599000);

        // Tier 1 features should be present
        assert!(
            features.lookback_trade_count.is_some(),
            "Trade count should be computed"
        );
        assert!(features.lookback_ofi.is_some(), "OFI should be computed");
    }

    #[test]
    fn test_optimization_edge_case_single_trade() {
        // Task #115-119: Verify optimizations handle single-trade edge case
        let mut history = TradeHistory::new(InterBarConfig::default());

        let snapshot = create_test_snapshot(1000, 100.0, 10.0, false);
        history.push(&AggTrade {
            agg_trade_id: 1,
            price: snapshot.price,
            volume: snapshot.volume,
            first_trade_id: 1,
            last_trade_id: 1,
            timestamp: snapshot.timestamp,
            is_buyer_maker: snapshot.is_buyer_maker,
            is_best_match: Some(false),
        });

        let features = history.compute_features(2000);

        // Tier 1 should compute (only 1 trade needed)
        assert!(features.lookback_trade_count.is_some());
        // Tier 3 definitely not (needs >= 60 for Hurst/Entropy)
        assert!(features.lookback_hurst.is_none());
    }

    #[test]
    fn test_optimization_many_trades() {
        // Task #119: Verify SmallVec handles typical bar trade counts (100-500)
        let mut history = TradeHistory::new(InterBarConfig::default());

        // Add 300 trades
        for i in 0..300_i64 {
            let snapshot = create_test_snapshot(
                i * 1000,
                100.0 + (i as f64 % 10.0),
                10.0 + (i as f64 % 5.0),
                i % 2 == 0,
            );
            history.push(&AggTrade {
                agg_trade_id: i,
                price: snapshot.price,
                volume: snapshot.volume,
                first_trade_id: i,
                last_trade_id: i,
                timestamp: snapshot.timestamp,
                is_buyer_maker: snapshot.is_buyer_maker,
                is_best_match: Some(false),
            });
        }

        // Get lookback trades
        let lookback = history.get_lookback_trades(299000);

        // Compute features with both tiers enabled (Task #115: rayon parallelization)
        let features = history.compute_features(299000);

        // Verify Tier 2 features are present
        assert!(
            features.lookback_kyle_lambda.is_some(),
            "Kyle lambda should be computed"
        );
        assert!(
            features.lookback_burstiness.is_some(),
            "Burstiness should be computed"
        );

        // Verify Tier 3 features are present (only if n >= 60)
        if lookback.len() >= 60 {
            assert!(
                features.lookback_hurst.is_some(),
                "Hurst should be computed"
            );
            assert!(
                features.lookback_permutation_entropy.is_some(),
                "Entropy should be computed"
            );
        }
    }

    #[test]
    fn test_trade_history_with_external_cache() {
        // Issue #145 Phase 2: Test that TradeHistory accepts optional external cache
        use crate::entropy_cache_global::get_global_entropy_cache;

        // Test 1: Local cache (backward compatible)
        let _local_history = TradeHistory::new(InterBarConfig::default());
        // Should work without issues - backward compatible

        // Test 2: External global cache
        let global_cache = get_global_entropy_cache();
        let _shared_history =
            TradeHistory::new_with_cache(InterBarConfig::default(), Some(global_cache.clone()));
        // Should work without issues - uses provided cache

        // Both constructors work correctly and can be created without panicking
    }

    #[test]
    fn test_feature_result_cache_hit_miss() {
        // Issue #96 Task #144 Phase 4: Verify cache hit/miss behavior
        use crate::types::AggTrade;

        fn create_test_trade(price: f64, volume: f64, is_buyer_maker: bool) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp: 1000000,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((volume * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker,
                is_best_match: Some(true),
            }
        }

        // Create trade history with Tier 1 only for speed
        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(50),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        // Create test trades
        let trades = vec![
            create_test_trade(100.0, 1.0, false),
            create_test_trade(100.5, 1.5, true),
            create_test_trade(100.2, 1.2, false),
        ];

        for trade in &trades {
            history.push(trade);
        }

        // First call: cache miss (computes features and stores in cache)
        let features1 = history.compute_features(2000000);
        assert!(features1.lookback_trade_count == Some(3));

        // Second call: cache hit (retrieves from cache)
        let features2 = history.compute_features(2000000);
        assert!(features2.lookback_trade_count == Some(3));

        // Both should produce identical results
        assert_eq!(features1.lookback_ofi, features2.lookback_ofi);
        assert_eq!(
            features1.lookback_count_imbalance,
            features2.lookback_count_imbalance
        );
    }

    #[test]
    fn test_feature_result_cache_multiple_computations() {
        // Issue #96 Task #144 Phase 4: Verify cache works across multiple computations
        use crate::types::AggTrade;

        fn create_test_trade(
            price: f64,
            volume: f64,
            timestamp: i64,
            is_buyer_maker: bool,
        ) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((volume * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker,
                is_best_match: Some(true),
            }
        }

        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(50),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        // Create trades with specific timestamps
        let trades = vec![
            create_test_trade(100.0, 1.0, 1000000, false),
            create_test_trade(100.5, 1.5, 2000000, true),
            create_test_trade(100.2, 1.2, 3000000, false),
            create_test_trade(100.1, 1.1, 4000000, true),
        ];

        for trade in &trades {
            history.push(trade);
        }

        // First computation - cache miss
        let features1 = history.compute_features(5000000); // Bar open after all trades
        assert_eq!(features1.lookback_trade_count, Some(4));
        let ofi1 = features1.lookback_ofi;

        // Second computation with same bar_open_time - cache hit
        let features2 = history.compute_features(5000000);
        assert_eq!(features2.lookback_trade_count, Some(4));
        assert_eq!(
            features2.lookback_ofi, ofi1,
            "Cache hit should return identical OFI"
        );

        // Third computation - different bar_open_time, different window
        let features3 = history.compute_features(3500000); // Gets trades before 3.5M (3 trades)
        assert_eq!(features3.lookback_trade_count, Some(3));

        // Fourth computation - same as first, should reuse cache
        let features4 = history.compute_features(5000000);
        assert_eq!(
            features4.lookback_ofi, ofi1,
            "Cache reuse should return identical results"
        );
    }

    #[test]
    fn test_feature_result_cache_different_windows() {
        // Issue #96 Task #144 Phase 4: Verify cache distinguishes different windows
        use crate::types::AggTrade;

        fn create_test_trade(
            price: f64,
            volume: f64,
            timestamp: i64,
            is_buyer_maker: bool,
        ) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((volume * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker,
                is_best_match: Some(true),
            }
        }

        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(100),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        // Add 10 trades with sequential timestamps
        for i in 0..10 {
            let trade = create_test_trade(
                100.0 + (i as f64 * 0.1),
                1.0 + (i as f64 * 0.01),
                1000000 + (i as i64 * 100000), // Timestamps: 1M, 1.1M, 1.2M, ..., 1.9M
                i % 2 == 0,
            );
            history.push(&trade);
        }

        // Compute features at bar_open_time=2M (gets all 10 trades, all have ts < 2M)
        let features1 = history.compute_features(2000000);
        assert_eq!(features1.lookback_trade_count, Some(10));

        // Add more trades beyond the bar_open_time cutoff (timestamps >= 2M)
        for i in 10..15 {
            let trade = create_test_trade(
                100.0 + (i as f64 * 0.1),
                1.0 + (i as f64 * 0.01),
                2000000 + (i as i64 * 100000), // Timestamps: 2M, 2.1M, ..., 2.4M (after bar_open_time)
                i % 2 == 0,
            );
            history.push(&trade);
        }

        // Compute features at same bar_open_time=2M - should still get only 10 trades (same lookback cutoff)
        let features2 = history.compute_features(2000000);
        assert_eq!(features2.lookback_trade_count, Some(10));

        // Results should be identical (same window)
        assert_eq!(features1.lookback_ofi, features2.lookback_ofi);
    }

    #[test]
    fn test_adaptive_pruning_batch_size_tracked() {
        // Issue #96 Task #155: Verify adaptive pruning batch size is tracked
        use crate::types::AggTrade;

        fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((1.0 * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker: false,
                is_best_match: Some(true),
            }
        }

        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(100),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        let initial_batch = history.adaptive_prune_batch;
        assert!(initial_batch > 0, "Initial batch size should be positive");

        // Add trades and verify batch size remains reasonable
        for i in 0..100 {
            let trade = create_test_trade(100.0 + (i as f64 * 0.01), 1_000_000 + (i as i64 * 100));
            history.push(&trade);
        }

        // Batch size should be reasonable (not zero, not excessively large)
        assert!(
            history.adaptive_prune_batch > 0 && history.adaptive_prune_batch <= initial_batch * 4,
            "Batch size should be reasonable"
        );
    }

    #[test]
    fn test_adaptive_pruning_deferred() {
        // Issue #96 Task #155: Verify deferred pruning respects capacity bounds
        use crate::types::AggTrade;

        fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((1.0 * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker: false,
                is_best_match: Some(true),
            }
        }

        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(50),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        let max_capacity = history.max_safe_capacity;

        // Add 300 trades - should trigger deferred pruning when hitting 2x capacity
        for i in 0..300 {
            let trade = create_test_trade(100.0 + (i as f64 * 0.01), 1_000_000 + (i as i64 * 100));
            history.push(&trade);
        }

        // After adding trades, trade count should be reasonable
        // (deferred pruning activates when > max_capacity * 2)
        assert!(
            history.trades.len() <= max_capacity * 3,
            "Trade count should be controlled by deferred pruning"
        );
    }

    #[test]
    fn test_adaptive_pruning_stats_tracking() {
        // Issue #96 Task #155: Verify pruning statistics are tracked correctly
        use crate::types::AggTrade;

        fn create_test_trade(price: f64, timestamp: i64) -> AggTrade {
            AggTrade {
                agg_trade_id: 1,
                timestamp,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((1.0 * 1e8) as i64),
                first_trade_id: 1,
                last_trade_id: 1,
                is_buyer_maker: false,
                is_best_match: Some(true),
            }
        }

        let mut history = TradeHistory::new(InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(100),
            compute_tier2: false,
            compute_tier3: false,
            ..Default::default()
        });

        // Initial stats should be empty
        assert_eq!(history.prune_stats, (0, 0), "Initial stats should be zero");

        // Add enough trades to trigger pruning (exceed 2x capacity)
        for i in 0..2000 {
            let trade = create_test_trade(100.0 + (i as f64 * 0.01), 1_000_000 + (i as i64 * 100));
            history.push(&trade);
        }

        // Stats should have been updated after pruning
        // Note: Stats are reset every 10 prune calls, so they might be (0,0) if exactly 10 calls happened
        // Just verify structure is there and reasonable
        assert!(
            history.prune_stats.0 <= 2000 && history.prune_stats.1 <= 10,
            "Pruning stats should be reasonable"
        );
    }

    // === EDGE CASE TESTS (Issue #96 Task #22) ===

    fn make_agg_trade(id: i64, price: f64, timestamp: i64) -> AggTrade {
        AggTrade {
            agg_trade_id: id,
            price: FixedPoint((price * 1e8) as i64),
            volume: FixedPoint(100000000), // 1.0
            first_trade_id: id,
            last_trade_id: id,
            timestamp,
            is_buyer_maker: false,
            is_best_match: None,
        }
    }

    #[test]
    fn test_get_lookback_empty_history() {
        let history = TradeHistory::new(InterBarConfig::default());
        let lookback = history.get_lookback_trades(1000);
        assert!(
            lookback.is_empty(),
            "Empty history should return empty lookback"
        );
    }

    #[test]
    fn test_has_lookback_empty_history() {
        let history = TradeHistory::new(InterBarConfig::default());
        assert!(
            !history.has_lookback_trades(1000),
            "Empty history should have no lookback"
        );
    }

    #[test]
    fn test_get_lookback_all_trades_after_bar_open() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        for i in 0..5 {
            history.push(&make_agg_trade(i, 100.0, 2000 + i));
        }
        let lookback = history.get_lookback_trades(1000);
        assert!(
            lookback.is_empty(),
            "All trades after bar_open_time should yield empty lookback"
        );
    }

    #[test]
    fn test_compute_features_minimum_lookback() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        history.push(&make_agg_trade(1, 100.0, 1000));
        history.push(&make_agg_trade(2, 101.0, 2000));

        let features = history.compute_features(3000);
        assert!(
            features.lookback_ofi.is_some(),
            "OFI should compute with 2 trades"
        );
        assert_eq!(features.lookback_trade_count, Some(2));
    }

    #[test]
    fn test_has_lookback_cache_hit_path() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        for i in 0..10 {
            history.push(&make_agg_trade(i, 100.0, i * 100));
        }
        let has1 = history.has_lookback_trades(500);
        let has2 = history.has_lookback_trades(500);
        assert_eq!(has1, has2, "Cache hit should return same result");
        assert!(has1, "Should have lookback trades before ts=500");
    }

    #[test]
    fn test_get_lookback_trades_at_exact_timestamp() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        for i in 1..=3i64 {
            history.push(&make_agg_trade(i, 100.0, i * 100));
        }
        // bar_open_time = 200: should get trades BEFORE 200 (only ts=100)
        let lookback = history.get_lookback_trades(200);
        assert_eq!(lookback.len(), 1, "Should get 1 trade before ts=200");
        assert_eq!(lookback[0].timestamp, 100);
    }

    // === buffer_stats() and has_lookback_trades() edge case tests (Issue #96 Task #71) ===

    #[test]
    fn test_buffer_stats_empty_history() {
        let history = TradeHistory::new(InterBarConfig::default());
        let (trades_len, max_capacity, _batch, trades_pruned) = history.buffer_stats();
        assert_eq!(trades_len, 0, "Empty history should have 0 trades");
        assert!(max_capacity > 0, "max_safe_capacity should be positive");
        assert_eq!(trades_pruned, 0, "No trades should have been pruned");
    }

    #[test]
    fn test_buffer_stats_after_pushes() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        for i in 0..5 {
            history.push(&make_agg_trade(i, 100.0, i * 100));
        }
        let (trades_len, _max_capacity, _batch, _trades_pruned) = history.buffer_stats();
        assert_eq!(trades_len, 5, "Should have 5 trades after 5 pushes");
    }

    #[test]
    fn test_has_lookback_no_trades_before_open() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        // All trades at timestamp 1000+
        for i in 0..5 {
            history.push(&make_agg_trade(i, 100.0, 1000 + i * 100));
        }
        // bar_open_time before all trades: should have lookback
        assert!(
            history.has_lookback_trades(1000 + 200),
            "Should have lookback before ts=1200"
        );
        // bar_open_time at first trade: no trades BEFORE it
        assert!(
            !history.has_lookback_trades(1000),
            "No trades before first trade timestamp"
        );
        // bar_open_time before all trades: no lookback
        assert!(!history.has_lookback_trades(500), "No trades before ts=500");
    }

    #[test]
    fn test_has_lookback_all_trades_before_open() {
        let mut history = TradeHistory::new(InterBarConfig::default());
        for i in 0..5 {
            history.push(&make_agg_trade(i, 100.0, i * 100));
        }
        // bar_open_time after all trades: all 5 trades are lookback
        assert!(
            history.has_lookback_trades(999),
            "All trades should be lookback"
        );
    }

    #[test]
    fn test_buffer_stats_len_matches_is_empty() {
        let history = TradeHistory::new(InterBarConfig::default());
        assert!(history.is_empty(), "New history should be empty");
        assert_eq!(history.len(), 0, "New history length should be 0");

        let mut history2 = TradeHistory::new(InterBarConfig::default());
        history2.push(&make_agg_trade(1, 100.0, 1000));
        assert!(
            !history2.is_empty(),
            "History with 1 trade should not be empty"
        );
        assert_eq!(history2.len(), 1, "History length should be 1");
    }

    // Issue #96 Task #94: Integration test for Tier 2/3 dispatch paths
    // All prior tests use compute_tier2: false, compute_tier3: false.
    // This exercises the 4-branch parallelization dispatch (lines 656-695).

    #[test]
    fn test_tier2_features_computed_when_enabled() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(500),
            compute_tier2: true,
            compute_tier3: false,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push 120 trades with realistic price variation and mixed buy/sell
        for i in 0..120i64 {
            let price = 50000.0 + (i as f64 * 0.7).sin() * 50.0;
            let volume = 1.0 + (i % 5) as f64 * 0.5;
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((volume * 1e8) as i64),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 500,         // 500us apart
                is_buyer_maker: i % 3 == 0, // ~33% sellers
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(120 * 500);

        // Tier 1 should always be present
        assert!(
            features.lookback_trade_count.is_some(),
            "trade_count should be Some"
        );
        assert!(features.lookback_ofi.is_some(), "ofi should be Some");

        // Tier 2 features should be computed
        assert!(
            features.lookback_kyle_lambda.is_some(),
            "kyle_lambda should be Some with tier2 enabled"
        );
        assert!(
            features.lookback_burstiness.is_some(),
            "burstiness should be Some with tier2 enabled"
        );
        assert!(
            features.lookback_volume_skew.is_some(),
            "volume_skew should be Some with tier2 enabled"
        );
        assert!(
            features.lookback_volume_kurt.is_some(),
            "volume_kurt should be Some with tier2 enabled"
        );
        assert!(
            features.lookback_price_range.is_some(),
            "price_range should be Some with tier2 enabled"
        );
        // Issue #128: GK promoted from Tier 3 → Tier 2
        assert!(
            features.lookback_garman_klass_vol.is_some(),
            "garman_klass should be Some with tier2 enabled"
        );

        // Tier 3 features should remain None
        assert!(
            features.lookback_kaufman_er.is_none(),
            "kaufman_er should be None with tier3 disabled"
        );
        assert!(
            features.lookback_hurst.is_none(),
            "hurst should be None with tier3 disabled"
        );
    }

    #[test]
    fn test_tier3_features_computed_when_enabled() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(500),
            compute_tier2: false,
            compute_tier3: true,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push 120 trades (>64 for Hurst, >60 for PE)
        for i in 0..120i64 {
            let price = 50000.0 + (i as f64 * 0.7).sin() * 50.0;
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((1.5 * 1e8) as i64),
                first_trade_id: i,
                last_trade_id: i,
                timestamp: i * 500,
                is_buyer_maker: i % 2 == 0,
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(120 * 500);

        // Tier 1 should be present
        assert!(
            features.lookback_trade_count.is_some(),
            "trade_count should be Some"
        );

        // Tier 2 should remain None (Issue #128: GK promoted to Tier 2)
        assert!(
            features.lookback_kyle_lambda.is_none(),
            "kyle_lambda should be None with tier2 disabled"
        );
        assert!(
            features.lookback_burstiness.is_none(),
            "burstiness should be None with tier2 disabled"
        );
        assert!(
            features.lookback_garman_klass_vol.is_none(),
            "garman_klass should be None with tier2 disabled (promoted from tier3)"
        );

        // Tier 3 features should be computed (120 trades > 64 for Hurst, > 60 for PE)
        assert!(
            features.lookback_kaufman_er.is_some(),
            "kaufman_er should be Some with tier3 enabled"
        );
    }

    #[test]
    fn test_all_tiers_enabled_parallel_dispatch() {
        let config = InterBarConfig {
            lookback_mode: LookbackMode::FixedCount(500),
            compute_tier2: true,
            compute_tier3: true,
            ..Default::default()
        };
        let mut history = TradeHistory::new(config);

        // Push 200 trades to exceed parallel thresholds (80 for Tier2, 150 for Tier3)
        for i in 0..200i64 {
            let price = 50000.0 + (i as f64 * 0.3).sin() * 100.0;
            let volume = 0.5 + (i % 7) as f64 * 0.3;
            let trade = AggTrade {
                agg_trade_id: i,
                price: FixedPoint((price * 1e8) as i64),
                volume: FixedPoint((volume * 1e8) as i64),
                first_trade_id: i,
                last_trade_id: i + 2, // aggregation_density > 1
                timestamp: i * 1000,
                is_buyer_maker: i % 4 == 0, // ~25% sellers
                is_best_match: None,
            };
            history.push(&trade);
        }

        let features = history.compute_features(200 * 1000);

        // All tiers should be computed
        assert!(features.lookback_trade_count.is_some(), "trade_count");
        assert!(features.lookback_ofi.is_some(), "ofi");
        assert!(features.lookback_intensity.is_some(), "intensity");
        assert!(features.lookback_vwap.is_some(), "vwap");

        // Tier 2 (Issue #128: GK promoted from Tier 3)
        assert!(features.lookback_kyle_lambda.is_some(), "kyle_lambda");
        assert!(features.lookback_burstiness.is_some(), "burstiness");
        assert!(features.lookback_volume_skew.is_some(), "volume_skew");
        assert!(features.lookback_volume_kurt.is_some(), "volume_kurt");
        assert!(features.lookback_price_range.is_some(), "price_range");
        assert!(
            features.lookback_garman_klass_vol.is_some(),
            "garman_klass_vol"
        );

        // Tier 3
        assert!(features.lookback_kaufman_er.is_some(), "kaufman_er");

        // Verify feature values are finite
        assert!(
            features.lookback_ofi.unwrap().is_finite(),
            "ofi should be finite"
        );
        assert!(
            features.lookback_kyle_lambda.unwrap().is_finite(),
            "kyle_lambda should be finite"
        );
        assert!(
            features.lookback_kaufman_er.unwrap().is_finite(),
            "kaufman_er should be finite"
        );
    }
}
