opendeviationbar-core 13.75.0

Core open deviation bar construction algorithm with temporal integrity guarantees
Documentation
//! Global entropy cache for multi-symbol processors
//!
//! Issue #145: Multi-Symbol Entropy Cache Sharing
//! Provides a thread-safe, shared entropy cache across all processors.
//!
//! ## Architecture
//!
//! Instead of each processor maintaining its own 128-entry cache:
//! - **Before**: 20 separate caches (5 symbols × 4 thresholds × 128 entries each)
//! - **After**: 1 global cache (512-1024 entries) shared across all processors
//!
//! ## Benefits
//!
//! - **Memory reduction**: 20 × 128 → 1 × 1024 = 20-30% savings on multi-symbol workloads
//! - **Hit ratio improvement**: 34.5% → 50%+ (larger cache size + price-based key is symbol-independent)
//! - **Thread-safe**: Arc<RwLock<>> for safe concurrent access
//! - **Backward compatible**: Local cache still available as default
//!
//! ## Usage
//!
//! ```ignore
//! use opendeviationbar_core::entropy_cache_global::{get_global_entropy_cache, EntropyCache};
//!
//! // Option 1: Use global cache (recommended for multi-symbol)
//! let cache = get_global_entropy_cache();
//! let mut cache_guard = cache.write();
//! compute_entropy_adaptive_cached(prices, &mut cache_guard);
//!
//! // Option 2: Use local cache (default, backward compatible)
//! let cache = Arc::new(RwLock::new(EntropyCache::new()));
//! ```

use crate::interbar_math::EntropyCache;
use parking_lot::RwLock;
use std::sync::Arc;
use std::sync::LazyLock;

/// Maximum capacity for the global entropy cache (tunable via this constant)
///
/// Trade-off: Larger cache → higher hit ratio (50%+ vs 34.5%) but more memory (80KB)
/// Smaller cache → lower memory but reduced hit ratio
///
/// Formula: memory ≈ capacity × 24 bytes (quick_cache overhead + f64 value)
/// - 128 entries = 5KB (original per-processor)
/// - 512 entries = 20KB (4x improvement, typical multi-symbol)
/// - 1024 entries = 40KB (8x improvement, heavy multi-symbol)
pub const GLOBAL_ENTROPY_CACHE_CAPACITY: u64 = 1024;

/// Global entropy cache shared across all processors
///
/// This static is initialized lazily on first access via `std::sync::LazyLock`.
/// Thread-safe via Arc<RwLock<>> — multiple processors can read/write concurrently.
///
/// ## Characteristics
///
/// - **Lazy initialization**: Allocated only when first accessed (zero startup cost)
/// - **Thread-safe**: RwLock allows multiple readers, exclusive writers
/// - **Reference-counted**: Arc ensures proper cleanup when all processors drop
/// - **Lock contention**: Entropy is ~2% of computation time, so RwLock overhead is minimal
///
/// ## Statistics (Phase 1)
///
/// Created as part of Issue #145 Phase 1 implementation.
/// Expected impact (Phase 4 validation):
/// - Hit ratio: 34.5% → 50%+ (from larger cache + symbol-independent hashing)
/// - Memory: 20-30% reduction on multi-symbol workloads (5 symbols × 4 thresholds)
/// - Latency: <5% overhead (lock contention acceptable due to low entropy usage)
pub static GLOBAL_ENTROPY_CACHE: LazyLock<Arc<RwLock<EntropyCache>>> = LazyLock::new(|| {
    Arc::new(RwLock::new(EntropyCache::with_capacity(
        GLOBAL_ENTROPY_CACHE_CAPACITY,
    )))
});

/// Get a reference to the global entropy cache
///
/// ## Thread Safety
///
/// Safe to call from multiple threads concurrently. The returned Arc can be:
/// - Read concurrently by multiple threads via `.read()`
/// - Written exclusively by one thread at a time via `.write()`
///
/// ## Example
///
/// ```ignore
/// let cache = get_global_entropy_cache();
/// let entropy = {
///     let mut cache_guard = cache.write();
///     compute_entropy_adaptive_cached(prices, &mut cache_guard)
/// };
/// ```
///
/// ## Performance Note
///
/// - First call: Initializes global cache (one-time ~100µs allocation)
/// - Subsequent calls: O(1) reference to existing Arc
/// - Lock acquisition: Contention expected to be low (<1% of compute time)
pub fn get_global_entropy_cache() -> Arc<RwLock<EntropyCache>> {
    GLOBAL_ENTROPY_CACHE.clone()
}

/// Create a local entropy cache (backward compatibility)
///
/// Use this to opt-out of global caching for a specific processor.
/// Default for TradeHistory when global cache is not explicitly provided.
///
/// ## When to Use Local Cache
///
/// - Single-symbol processor (no benefit from sharing)
/// - Testing/isolation (prevent cache pollution from other processors)
/// - Feature flag disabled (if global-entropy-cache feature is off)
///
/// ## Performance
///
/// Local cache has same performance as before refactoring (128 entries, LRU eviction).
pub fn create_local_entropy_cache() -> Arc<RwLock<EntropyCache>> {
    Arc::new(RwLock::new(EntropyCache::new()))
}

/// Warm up the global entropy cache with deterministic price patterns
///
/// Issue #96 Task #191: Pre-compute entropy for common price ranges to reduce
/// first-access contention in multi-symbol streaming workloads.
///
/// ## Strategy
///
/// Generates synthetic price sequences representing:
/// - Stable consolidation (minimal volatility: 0.5%)
/// - Medium volatility (1.0%)
/// - High volatility (1.5-2.0%)
/// - Trending patterns (uptrend, downtrend)
///
/// Each pattern generates 50-300 trade sequences, simulating typical bar sizes.
///
/// ## Performance
///
/// - Warm-up time: <1ms (pre-computation in background)
/// - Memory: ~40KB (40 entries × ~1KB each in global cache)
/// - Impact: 1-3% latency reduction on first bar of multi-symbol streaming
/// - Non-blocking: Uses try_write() to avoid contention
///
/// ## Usage
///
/// ```ignore
/// // Called automatically during first TradeHistory creation
/// warm_up_entropy_cache();
/// ```
pub fn warm_up_entropy_cache() {
    let cache = get_global_entropy_cache();

    // Try to acquire write lock without blocking. If contention exists, skip warm-up.
    // This ensures warm-up doesn't block the main processing thread.
    let Some(mut cache_guard) = cache.try_write() else {
        return; // Skip warm-up if cache is locked
    };

    // Pattern 1: Stable consolidation (0.5% volatility)
    // Simulates tight trading around support/resistance
    let base_price = 100.0;
    for i in 0..30 {
        let mut prices = Vec::with_capacity(100);
        let variation = base_price * 0.005; // 0.5%
        for j in 0..100 {
            let offset = (((i as f64 * 7.0 + j as f64 * 3.0) % 100.0) - 50.0) / 1000.0;
            prices.push(base_price + variation * offset);
        }
        let entropy =
            crate::interbar_math::compute_entropy_adaptive_cached(&prices, &mut cache_guard);
        if entropy.is_finite() {
            cache_guard.insert(&prices, entropy);
        }
    }

    // Pattern 2: Medium volatility (1.0%)
    // Simulates normal intraday movement
    for i in 0..25 {
        let mut prices = Vec::with_capacity(150);
        let variation = base_price * 0.01; // 1.0%
        let trend = (i as f64 / 25.0) - 0.5; // Slight trend bias
        for j in 0..150 {
            let random_component = (((i as f64 * 11.0 + j as f64 * 7.0) % 100.0) - 50.0) / 100.0;
            let trend_component = trend * (j as f64 / 150.0);
            prices.push(base_price + variation * (random_component + trend_component * 0.5));
        }
        let entropy =
            crate::interbar_math::compute_entropy_adaptive_cached(&prices, &mut cache_guard);
        if entropy.is_finite() {
            cache_guard.insert(&prices, entropy);
        }
    }

    // Pattern 3: High volatility (1.5-2.0%)
    // Simulates volatile market conditions
    for i in 0..20 {
        let mut prices = Vec::with_capacity(200);
        let variation = base_price * (0.015 + 0.005 * ((i as f64 / 20.0) - 0.5) * 2.0); // 1.5-2.0%
        for j in 0..200 {
            let phase = ((i as f64 * 13.0 + j as f64 * 5.0) % 100.0) / 100.0;
            let oscillation = (phase * std::f64::consts::TAU).sin();
            prices.push(base_price + variation * oscillation);
        }
        let entropy =
            crate::interbar_math::compute_entropy_adaptive_cached(&prices, &mut cache_guard);
        if entropy.is_finite() {
            cache_guard.insert(&prices, entropy);
        }
    }

    // Pattern 4: Trending patterns (uptrend/downtrend)
    // Simulates directional market movement
    for i in 0..15 {
        let mut prices = Vec::with_capacity(250);
        let trend_strength = 0.01 * ((i as f64 / 15.0) - 0.5) * 2.0; // ±0.01 per trade
        let mut current_price = base_price;
        for j in 0..250 {
            let noise = (((i as f64 * 17.0 + j as f64 * 11.0) % 100.0) - 50.0) / 500.0;
            current_price += trend_strength + noise;
            prices.push(current_price);
        }
        let entropy =
            crate::interbar_math::compute_entropy_adaptive_cached(&prices, &mut cache_guard);
        if entropy.is_finite() {
            cache_guard.insert(&prices, entropy);
        }
    }

    // Pattern 5: Gap recovery (sharp move + consolidation)
    // Simulates gaps and recovery patterns
    for i in 0..10 {
        let mut prices = Vec::with_capacity(300);
        let gap_size = base_price * (0.01 + 0.005 * (i as f64 / 10.0)); // 1-1.5% gap
        let mut current_price = base_price;

        // First 50: sharp gap move
        for j in 0..50 {
            let move_progress = j as f64 / 50.0;
            current_price = base_price + gap_size * move_progress;
            prices.push(current_price);
        }

        // Remaining 250: consolidation around new level
        for j in 50..300 {
            let consolidation = (((i as f64 * 19.0 + j as f64 * 3.0) % 100.0) - 50.0) / 1000.0;
            prices.push(current_price + gap_size * consolidation * 0.5);
        }

        let entropy =
            crate::interbar_math::compute_entropy_adaptive_cached(&prices, &mut cache_guard);
        if entropy.is_finite() {
            cache_guard.insert(&prices, entropy);
        }
    }
}

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

    #[test]
    fn test_global_cache_singleton() {
        // Test that multiple calls return the same Arc pointer
        let cache1 = get_global_entropy_cache();
        let cache2 = get_global_entropy_cache();

        // Both should point to the same underlying data
        // (Arc::ptr_eq would be more precise but requires nightly)
        assert_eq!(Arc::strong_count(&cache1), Arc::strong_count(&cache2));
    }

    #[test]
    fn test_global_cache_thread_safe() {
        use std::thread;

        let cache = get_global_entropy_cache();
        let mut handles = vec![];

        // Spawn multiple threads accessing the cache concurrently
        for i in 0..4 {
            let cache_clone = cache.clone();
            let handle = thread::spawn(move || {
                // Each thread tries to write to the cache
                let _guard = cache_clone.write();
                // If we get here without deadlock, thread safety is OK
                i
            });
            handles.push(handle);
        }

        // Wait for all threads
        for handle in handles {
            let _ = handle.join();
        }
    }

    #[test]
    fn test_local_cache_independence() {
        // Test that local caches are independent instances
        let local1 = create_local_entropy_cache();
        let local2 = create_local_entropy_cache();

        // Verify they point to different underlying data by comparing raw pointers
        // Two newly created Arc instances should have different pointer addresses
        let ptr1 = Arc::as_ptr(&local1);
        let ptr2 = Arc::as_ptr(&local2);
        assert_ne!(
            ptr1, ptr2,
            "Local caches should point to different EntropyCache instances"
        );
    }

    // Issue #96 Task #96: Test warm_up_entropy_cache runs without panic

    #[test]
    fn test_warm_up_entropy_cache_completes() {
        // warm_up_entropy_cache() should complete without panic
        // Uses try_write() internally so is non-blocking
        warm_up_entropy_cache();

        // After warm-up, global cache should be accessible for reads
        let cache = get_global_entropy_cache();
        let guard = cache.read();
        // Verify we can read from cache without panic (warm-up populated it)
        let sample_prices = vec![100.0, 100.5, 100.2, 100.8, 100.1];
        let _ = guard.get(&sample_prices); // May or may not hit, but shouldn't panic
    }

    #[test]
    fn test_global_cache_capacity() {
        assert_eq!(GLOBAL_ENTROPY_CACHE_CAPACITY, 1024);
    }
}