use pyo3::prelude::*;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
use std::time::Instant;
use tracing::{debug, error, info, warn};

use crate::contingent::ContingentManager;
use crate::db::Database;
use crate::error::HorizonError;
use crate::exchanges::book_sim::{BookSimExchange, FillModel, ImpactModel};
use crate::exchanges::kalshi::KalshiExchange;
use crate::exchanges::paper::PaperExchange;
use crate::exchanges::polymarket::PolymarketExchange;
use crate::exchanges::Exchange;
use crate::feeds::{FeedConfig, FeedManager, FeedSnapshot};
use crate::orders::OrderManager;
use crate::positions::PositionTracker;
use crate::risk::RiskManager;
use crate::types::*;

/// Exchange backend — enum dispatch avoids trait objects + unsafe downcasts.
enum ExchangeBackend {
    Paper(PaperExchange),
    Polymarket(PolymarketExchange),
    Kalshi(KalshiExchange),
    BookSim(BookSimExchange),
}

impl ExchangeBackend {
    fn as_exchange(&self) -> &dyn Exchange {
        match self {
            ExchangeBackend::Paper(p) => p,
            ExchangeBackend::Polymarket(p) => p,
            ExchangeBackend::Kalshi(k) => k,
            ExchangeBackend::BookSim(b) => b,
        }
    }

    fn tick(&self, market_id: &str, mid_price: f64) -> usize {
        match self {
            ExchangeBackend::Paper(p) => p.tick(market_id, mid_price),
            ExchangeBackend::BookSim(b) => b.tick(market_id),
            // Live exchanges don't use tick — fills arrive via polling/websocket
            ExchangeBackend::Polymarket(_) | ExchangeBackend::Kalshi(_) => 0,
        }
    }

    fn is_paper(&self) -> bool {
        matches!(self, ExchangeBackend::Paper(_) | ExchangeBackend::BookSim(_))
    }
}

/// The core engine: owns risk, orders, positions, exchanges, feeds, and persistence.
/// Supports multiple exchanges simultaneously for cross-venue trading.
/// Exposed to Python via PyO3.
#[pyclass]
pub struct Engine {
    risk: RiskManager,
    orders: OrderManager,
    positions: Arc<PositionTracker>,
    /// All registered exchanges, keyed by name (e.g., "paper", "polymarket", "kalshi").
    exchanges: HashMap<String, ExchangeBackend>,
    /// Name of the primary exchange (first one created). Used as default for routing.
    primary_exchange: String,
    /// Shared tokio runtime for all live exchanges.
    runtime: Option<Arc<tokio::runtime::Runtime>>,
    /// Netting pairs: (market_a, market_b) pairs whose positions offset for risk purposes.
    netting_pairs: Vec<(String, String)>,
    contingent: ContingentManager,
    /// Market expiry timestamps for lifecycle management: market_id -> expiry unix timestamp.
    market_expiries: HashMap<String, f64>,
    /// Lead time (seconds) before expiry to auto-cancel orders and optionally exit.
    lifecycle_lead_secs: f64,
    feed_manager: Option<FeedManager>,
    start_time: Instant,
    fills: std::sync::Mutex<Vec<Fill>>,
    /// Seen fill IDs for engine-level dedup (defense in depth).
    seen_fill_ids: std::sync::Mutex<HashSet<String>>,
    /// FIFO order of fill IDs for eviction (oldest first).
    seen_fill_order: std::sync::Mutex<VecDeque<String>>,
    db: Option<std::sync::Mutex<Database>>,
    validated: bool,
    /// Event registry: event_id -> list of outcome market_ids.
    event_markets: HashMap<String, Vec<String>>,
    /// Reverse lookup: market_id -> event_id.
    market_to_event: HashMap<String, String>,
    /// Daily P&L baseline: captured at engine start / midnight reset.
    daily_pnl_baseline: std::sync::Mutex<f64>,
}

const MAX_FILLS: usize = 1000;

impl Engine {
    /// Append fills to the capped vector, evicting oldest when full.
    fn append_fills_capped(
        fills_mutex: &std::sync::Mutex<Vec<Fill>>,
        new_fills: Vec<Fill>,
    ) -> bool {
        match fills_mutex.lock() {
            Ok(mut fills) => {
                fills.extend(new_fills);
                if fills.len() > MAX_FILLS {
                    let drain_count = fills.len() - MAX_FILLS;
                    fills.drain(..drain_count);
                }
                true
            }
            Err(e) => {
                error!(
                    count = e.get_ref().len(),
                    "fills lock was poisoned, recovering"
                );
                let mut fills = e.into_inner();
                fills.extend(new_fills);
                if fills.len() > MAX_FILLS {
                    let drain_count = fills.len() - MAX_FILLS;
                    fills.drain(..drain_count);
                }
                true
            }
        }
    }

    /// Execute a void closure on the database, handling lock poisoning gracefully.
    fn with_db<F, E>(&self, op_name: &str, f: F)
    where
        F: FnOnce(&Database) -> Result<(), E>,
        E: std::fmt::Display,
    {
        if let Some(ref db_mutex) = self.db {
            let guard = match db_mutex.lock() {
                Ok(g) => g,
                Err(poisoned) => {
                    error!(op = %op_name, "database lock poisoned, recovering");
                    poisoned.into_inner()
                }
            };
            if let Err(e) = f(&guard) {
                error!(op = %op_name, error = %e, "database operation failed");
            }
        }
    }

    /// Execute a closure on the database that returns a value.
    fn with_db_result<F, T, E>(&self, op_name: &str, f: F) -> Option<T>
    where
        F: FnOnce(&Database) -> Result<T, E>,
        E: std::fmt::Display,
    {
        let db_mutex = self.db.as_ref()?;
        let guard = match db_mutex.lock() {
            Ok(g) => g,
            Err(poisoned) => {
                error!(op = %op_name, "database lock poisoned, recovering");
                poisoned.into_inner()
            }
        };
        match f(&guard) {
            Ok(val) => Some(val),
            Err(e) => {
                error!(op = %op_name, error = %e, "database operation failed");
                None
            }
        }
    }

    /// Resolve exchange name to backend reference.
    fn resolve_exchange<'a>(
        &'a self,
        name: Option<&'a str>,
    ) -> PyResult<(&'a str, &'a ExchangeBackend)> {
        let exch_name = name.unwrap_or(&self.primary_exchange);
        let backend = self.exchanges.get(exch_name).ok_or_else(|| {
            pyo3::exceptions::PyValueError::new_err(format!("unknown exchange: {}", exch_name))
        })?;
        Ok((exch_name, backend))
    }

    /// Get or create the shared tokio runtime for live exchanges.
    fn get_or_create_runtime(&mut self) -> PyResult<Arc<tokio::runtime::Runtime>> {
        if let Some(ref rt) = self.runtime {
            return Ok(rt.clone());
        }
        let rt = Arc::new(
            tokio::runtime::Builder::new_multi_thread()
                .worker_threads(2)
                .enable_all()
                .build()
                .map_err(|e| {
                    pyo3::exceptions::PyRuntimeError::new_err(format!(
                        "failed to create tokio runtime: {}",
                        e
                    ))
                })?,
        );
        self.runtime = Some(rt.clone());
        Ok(rt)
    }

    /// Submit an order to a specific exchange (internal routing).
    fn submit_order_to(&self, request: &OrderRequest, exch_name: &str) -> PyResult<String> {
        let backend = self.exchanges.get(exch_name).ok_or_else(|| {
            pyo3::exceptions::PyValueError::new_err(format!("unknown exchange: {}", exch_name))
        })?;

        // Snapshot position state atomically before risk check.
        // For netting: compute adjusted notional that accounts for hedged positions.
        let (exposure, notional) = self.positions.snapshot_exposure(&request.market_id);
        let adjusted_notional = self.apply_netting(notional);

        self.risk
            .check(request, exposure, adjusted_notional)
            .map_err(|e| {
                debug!(
                    market = %request.market_id,
                    exchange = %exch_name,
                    side = ?request.order_side,
                    reason = %e,
                    "risk rejection"
                );
                HorizonError::Risk(e)
            })?;

        // Event-level position limit check
        if let Some(event_id) = self.market_to_event.get(&request.market_id) {
            if let Some(market_ids) = self.event_markets.get(event_id) {
                let event_exp = self.positions.event_exposure(market_ids);
                self.risk
                    .check_event_limit(event_exp, request.size, request.order_side)
                    .map_err(|e| {
                        debug!(
                            market = %request.market_id,
                            event = %event_id,
                            reason = %e,
                            "event risk rejection"
                        );
                        HorizonError::Risk(e)
                    })?;
            }
        }

        let tracking_id = self.orders.register_new(request, exch_name);

        match backend.as_exchange().submit_order(request) {
            Ok(exchange_id) => {
                self.orders.mark_submitted(&tracking_id, &exchange_id);
                if let Some(order) = self.orders.get(&exchange_id) {
                    self.with_db("order_submitted", |db| db.insert_order_event(&order));
                }
                info!(
                    order_id = %exchange_id,
                    market = %request.market_id,
                    exchange = %exch_name,
                    side = ?request.order_side,
                    price = request.price,
                    size = request.size,
                    "order submitted"
                );
                Ok(exchange_id)
            }
            Err(e) => {
                self.orders.mark_rejected(&tracking_id, &e.to_string());
                if let Some(order) = self.orders.get(&tracking_id) {
                    self.with_db("order_rejected", |db| db.insert_order_event(&order));
                }
                warn!(
                    market = %request.market_id,
                    exchange = %exch_name,
                    error = %e,
                    "order rejected by exchange"
                );
                Err(e.into())
            }
        }
    }

    /// Process fills from a specific exchange — dedup, persist, update positions, update orders.
    fn process_exchange_fills(&self, new_fills: Vec<Fill>) -> u32 {
        if new_fills.is_empty() {
            return 0;
        }

        // Dedup fills by fill_id (defense in depth — exchange-level dedup is primary)
        let deduped = match self.seen_fill_ids.lock() {
            Ok(mut seen) => {
                let mut order = self.seen_fill_order.lock().unwrap_or_else(|e| e.into_inner());
                let mut unique = Vec::with_capacity(new_fills.len());
                for fill in new_fills {
                    if seen.insert(fill.fill_id.clone()) {
                        order.push_back(fill.fill_id.clone());
                        unique.push(fill);
                    } else {
                        debug!(fill_id = %fill.fill_id, "duplicate fill skipped in engine");
                    }
                }
                // FIFO eviction: remove oldest 10K when exceeding 50K (no gap in dedup)
                if seen.len() > 50_000 {
                    let evict_count = 10_000.min(order.len());
                    for _ in 0..evict_count {
                        if let Some(old_id) = order.pop_front() {
                            seen.remove(&old_id);
                        }
                    }
                }
                unique
            }
            Err(poisoned) => {
                error!("seen_fill_ids lock poisoned, recovering");
                let mut seen = poisoned.into_inner();
                let mut order = self.seen_fill_order.lock().unwrap_or_else(|e| e.into_inner());
                let mut unique = Vec::with_capacity(new_fills.len());
                for fill in new_fills {
                    if seen.insert(fill.fill_id.clone()) {
                        order.push_back(fill.fill_id.clone());
                        unique.push(fill);
                    }
                }
                // FIFO eviction (same logic as non-poisoned path)
                if seen.len() > 50_000 {
                    let evict_count = 10_000.min(order.len());
                    for _ in 0..evict_count {
                        if let Some(old_id) = order.pop_front() {
                            seen.remove(&old_id);
                        }
                    }
                }
                unique
            }
        };

        let count = deduped.len() as u32;
        for fill in &deduped {
            info!(
                fill_id = %fill.fill_id,
                order_id = %fill.order_id,
                market = %fill.market_id,
                exchange = %fill.exchange,
                side = ?fill.order_side,
                price = fill.price,
                size = fill.size,
                "fill processed"
            );
            self.with_db("insert_fill", |db| db.insert_fill(fill));
            self.positions.apply_fill(fill);
            self.orders.apply_fill_size(&fill.order_id, fill.size);
            if let Some(order) = self.orders.get(&fill.order_id) {
                self.with_db("order_fill", |db| db.insert_order_event(&order));
            }
        }
        Self::append_fills_capped(&self.fills, deduped);
        count
    }

    /// Calculate adjusted notional after netting correlated positions.
    /// For each netting pair (A, B), the hedged portion is subtracted from total notional.
    #[inline]
    fn apply_netting(&self, raw_notional: f64) -> f64 {
        if self.netting_pairs.is_empty() {
            return raw_notional;
        }

        let mut reduction = 0.0;
        for (market_a, market_b) in &self.netting_pairs {
            let exposure_a = self.positions.market_exposure(market_a);
            let exposure_b = self.positions.market_exposure(market_b);
            // The hedged amount is the min of the two exposures — they offset each other.
            let hedged = exposure_a.min(exposure_b);
            if hedged > 0.0 {
                // Use actual entry prices for notional reduction
                let price_a = self.positions.avg_entry_price_for_market(market_a);
                let price_b = self.positions.avg_entry_price_for_market(market_b);
                let avg_price = if price_a > 0.0 && price_b > 0.0 {
                    (price_a + price_b) / 2.0
                } else if price_a > 0.0 {
                    price_a
                } else if price_b > 0.0 {
                    price_b
                } else {
                    0.5 // fallback for prediction markets
                };
                reduction += hedged * avg_price;
            }
        }

        (raw_notional - reduction).max(0.0)
    }
}

#[pymethods]
impl Engine {
    /// Create a new engine with the given risk config.
    /// exchange_type: "paper" (default), "polymarket", or "kalshi".
    /// db_path: path to SQLite database for persistence (None = disabled).
    #[new]
    #[pyo3(signature = (risk_config=None, paper_fee_rate=0.001, exchange_type="paper",
                        exchange_key=None, exchange_secret=None, exchange_passphrase=None,
                        clob_url=None, api_url=None, email=None, password=None,
                        private_key=None, paper_partial_fill_ratio=1.0,
                        db_path=None, api_key=None,
                        fill_model=None, fill_lambda=1.0, fill_kappa=1.5,
                        fill_queue_position=0.5, fill_intensity=1.0,
                        impact_temporary_bps=0.0, impact_permanent_fraction=0.0,
                        latency_ticks=0, rng_seed=None,
                        paper_maker_fee_rate=None, paper_taker_fee_rate=None))]
    fn new(
        risk_config: Option<RiskConfig>,
        paper_fee_rate: f64,
        exchange_type: &str,
        exchange_key: Option<String>,
        exchange_secret: Option<String>,
        exchange_passphrase: Option<String>,
        clob_url: Option<String>,
        api_url: Option<String>,
        email: Option<String>,
        password: Option<String>,
        private_key: Option<String>,
        paper_partial_fill_ratio: f64,
        db_path: Option<String>,
        api_key: Option<String>,
        fill_model: Option<&str>,
        fill_lambda: f64,
        fill_kappa: f64,
        fill_queue_position: f64,
        fill_intensity: f64,
        impact_temporary_bps: f64,
        impact_permanent_fraction: f64,
        latency_ticks: u32,
        rng_seed: Option<u64>,
        paper_maker_fee_rate: Option<f64>,
        paper_taker_fee_rate: Option<f64>,
    ) -> PyResult<Self> {
        // Validate paper_partial_fill_ratio
        if paper_partial_fill_ratio.is_nan() || paper_partial_fill_ratio <= 0.0 || paper_partial_fill_ratio > 1.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "paper_partial_fill_ratio must be in (0.0, 1.0], got {}",
                paper_partial_fill_ratio
            )));
        }

        // Validate Horizon API key before anything else
        let key = api_key
            .or_else(|| std::env::var("HORIZON_API_KEY").ok())
            .unwrap_or_default();
        crate::auth::validate_api_key(&key)?;

        let config = risk_config.unwrap_or_else(|| RiskConfig {
            max_position_per_market: 100.0,
            max_portfolio_notional: 1000.0,
            max_daily_drawdown_pct: 5.0,
            max_order_size: 50.0,
            rate_limit_sustained: 50,
            rate_limit_burst: 300,
            dedup_window_ms: 1000,
            max_position_per_event: None,
        });

        let mut exchanges = HashMap::new();
        let mut runtime: Option<Arc<tokio::runtime::Runtime>> = None;

        let primary_name = match exchange_type {
            "polymarket" => {
                let rt = Arc::new(
                    tokio::runtime::Builder::new_multi_thread()
                        .worker_threads(2)
                        .enable_all()
                        .build()
                        .map_err(|e| {
                            pyo3::exceptions::PyRuntimeError::new_err(format!(
                                "failed to create runtime: {}",
                                e
                            ))
                        })?,
                );
                let exch = PolymarketExchange::new(
                    exchange_key.unwrap_or_default(),
                    exchange_secret.unwrap_or_default(),
                    exchange_passphrase.unwrap_or_default(),
                    private_key,
                    clob_url,
                    rt.clone(),
                );
                runtime = Some(rt);
                exchanges.insert("polymarket".to_string(), ExchangeBackend::Polymarket(exch));
                "polymarket".to_string()
            }
            "kalshi" => {
                let rt = Arc::new(
                    tokio::runtime::Builder::new_multi_thread()
                        .worker_threads(2)
                        .enable_all()
                        .build()
                        .map_err(|e| {
                            pyo3::exceptions::PyRuntimeError::new_err(format!(
                                "failed to create runtime: {}",
                                e
                            ))
                        })?,
                );
                let exch = KalshiExchange::new(email, password, exchange_key, api_url, rt.clone());
                runtime = Some(rt);
                exchanges.insert("kalshi".to_string(), ExchangeBackend::Kalshi(exch));
                "kalshi".to_string()
            }
            "book_sim" => {
                let fm = match fill_model.unwrap_or("deterministic") {
                    "probabilistic" => FillModel::Probabilistic {
                        lambda: fill_lambda,
                        queue_frac: fill_queue_position,
                    },
                    "glft" => FillModel::GLFT {
                        intensity: fill_intensity,
                        kappa: fill_kappa,
                    },
                    _ => FillModel::Deterministic,
                };
                let impact = ImpactModel {
                    temporary_bps: impact_temporary_bps,
                    permanent_fraction: impact_permanent_fraction,
                };
                let seed = rng_seed.unwrap_or(12345);
                let mk_fee = paper_maker_fee_rate.unwrap_or(paper_fee_rate);
                let tk_fee = paper_taker_fee_rate.unwrap_or(paper_fee_rate);
                exchanges.insert(
                    "book_sim".to_string(),
                    ExchangeBackend::BookSim(BookSimExchange::with_maker_taker_fees(
                        mk_fee,
                        tk_fee,
                        fm,
                        impact,
                        latency_ticks,
                        seed,
                    )),
                );
                "book_sim".to_string()
            }
            _ => {
                let mk_fee = paper_maker_fee_rate.unwrap_or(paper_fee_rate);
                let tk_fee = paper_taker_fee_rate.unwrap_or(paper_fee_rate);
                exchanges.insert(
                    "paper".to_string(),
                    ExchangeBackend::Paper(PaperExchange::with_maker_taker_fees(
                        mk_fee,
                        tk_fee,
                        paper_partial_fill_ratio,
                    )),
                );
                "paper".to_string()
            }
        };

        // Open persistence database if path provided
        let db = match db_path {
            Some(ref path) if !path.is_empty() => match Database::open(path) {
                Ok(database) => Some(std::sync::Mutex::new(database)),
                Err(e) => {
                    error!(path = %path, error = %e, "failed to open database, running without persistence");
                    None
                }
            },
            _ => None,
        };

        Ok(Self {
            risk: RiskManager::new(config),
            orders: OrderManager::new(),
            positions: Arc::new(PositionTracker::new()),
            exchanges,
            primary_exchange: primary_name,
            runtime,
            netting_pairs: Vec::new(),
            contingent: ContingentManager::new(),
            market_expiries: HashMap::new(),
            lifecycle_lead_secs: 300.0, // default 5 minutes
            feed_manager: None,
            start_time: Instant::now(),
            fills: std::sync::Mutex::new(Vec::new()),
            seen_fill_ids: std::sync::Mutex::new(HashSet::new()),
            seen_fill_order: std::sync::Mutex::new(VecDeque::new()),
            db,
            validated: true,
            event_markets: HashMap::new(),
            market_to_event: HashMap::new(),
            daily_pnl_baseline: std::sync::Mutex::new(0.0),
        })
    }

    // -------------------------------------------------------------------
    // Multi-exchange management
    // -------------------------------------------------------------------

    /// Add a secondary exchange to the engine.
    /// Returns the exchange name on success.
    #[pyo3(signature = (exchange_type, exchange_key=None, exchange_secret=None, exchange_passphrase=None,
                        clob_url=None, api_url=None, email=None, password=None,
                        private_key=None, paper_fee_rate=0.001, paper_partial_fill_ratio=1.0,
                        paper_maker_fee_rate=None, paper_taker_fee_rate=None))]
    fn add_exchange(
        &mut self,
        exchange_type: &str,
        exchange_key: Option<String>,
        exchange_secret: Option<String>,
        exchange_passphrase: Option<String>,
        clob_url: Option<String>,
        api_url: Option<String>,
        email: Option<String>,
        password: Option<String>,
        private_key: Option<String>,
        paper_fee_rate: f64,
        paper_partial_fill_ratio: f64,
        paper_maker_fee_rate: Option<f64>,
        paper_taker_fee_rate: Option<f64>,
    ) -> PyResult<String> {
        // Validate paper_partial_fill_ratio for paper exchanges
        if exchange_type == "paper" && (paper_partial_fill_ratio.is_nan() || paper_partial_fill_ratio <= 0.0 || paper_partial_fill_ratio > 1.0) {
            return Err(pyo3::exceptions::PyValueError::new_err(format!(
                "paper_partial_fill_ratio must be in (0.0, 1.0], got {}",
                paper_partial_fill_ratio
            )));
        }

        let name = match exchange_type {
            "polymarket" => {
                if self.exchanges.contains_key("polymarket") {
                    return Err(pyo3::exceptions::PyValueError::new_err(
                        "polymarket exchange already registered",
                    ));
                }
                let rt = self.get_or_create_runtime()?;
                let exch = PolymarketExchange::new(
                    exchange_key.unwrap_or_default(),
                    exchange_secret.unwrap_or_default(),
                    exchange_passphrase.unwrap_or_default(),
                    private_key,
                    clob_url,
                    rt,
                );
                self.exchanges
                    .insert("polymarket".to_string(), ExchangeBackend::Polymarket(exch));
                info!("added secondary exchange: polymarket");
                "polymarket".to_string()
            }
            "kalshi" => {
                if self.exchanges.contains_key("kalshi") {
                    return Err(pyo3::exceptions::PyValueError::new_err(
                        "kalshi exchange already registered",
                    ));
                }
                let rt = self.get_or_create_runtime()?;
                let exch = KalshiExchange::new(email, password, exchange_key, api_url, rt);
                self.exchanges
                    .insert("kalshi".to_string(), ExchangeBackend::Kalshi(exch));
                info!("added secondary exchange: kalshi");
                "kalshi".to_string()
            }
            "paper" => {
                if self.exchanges.contains_key("paper") {
                    return Err(pyo3::exceptions::PyValueError::new_err(
                        "paper exchange already registered",
                    ));
                }
                let mk_fee = paper_maker_fee_rate.unwrap_or(paper_fee_rate);
                let tk_fee = paper_taker_fee_rate.unwrap_or(paper_fee_rate);
                self.exchanges.insert(
                    "paper".to_string(),
                    ExchangeBackend::Paper(PaperExchange::with_maker_taker_fees(
                        mk_fee,
                        tk_fee,
                        paper_partial_fill_ratio,
                    )),
                );
                info!("added secondary exchange: paper");
                "paper".to_string()
            }
            _ => {
                return Err(pyo3::exceptions::PyValueError::new_err(format!(
                    "unknown exchange type: {}",
                    exchange_type
                )));
            }
        };
        Ok(name)
    }

    /// Get names of all registered exchanges.
    fn exchange_names(&self) -> Vec<String> {
        self.exchanges.keys().cloned().collect()
    }

    /// Get the number of registered exchanges.
    fn exchange_count(&self) -> usize {
        self.exchanges.len()
    }

    /// Register a netting pair — two market IDs whose positions offset for risk purposes.
    /// This reduces the portfolio notional for hedged positions across exchanges.
    fn set_netting_pair(&mut self, market_a: String, market_b: String) {
        info!(market_a = %market_a, market_b = %market_b, "netting pair registered");
        self.netting_pairs.push((market_a, market_b));
    }

    /// Get all registered netting pairs.
    fn netting_pairs(&self) -> Vec<(String, String)> {
        self.netting_pairs.clone()
    }

    // -------------------------------------------------------------------
    // Feeds
    // -------------------------------------------------------------------

    /// Start a feed by name and type.
    ///
    /// For new feed types (predictit, manifold, espn, nws, rest_json_path),
    /// pass structured config as JSON via `config_json`. Existing feeds ignore it.
    #[pyo3(signature = (name, feed_type, symbol="", url="", interval=5.0, config_json=""))]
    fn start_feed(
        &mut self,
        name: String,
        feed_type: &str,
        symbol: &str,
        url: &str,
        interval: f64,
        config_json: &str,
    ) -> PyResult<()> {
        if self.feed_manager.is_none() {
            self.feed_manager = Some(FeedManager::new().map_err(|e| {
                pyo3::exceptions::PyRuntimeError::new_err(format!(
                    "failed to start feed manager: {}",
                    e
                ))
            })?);
        }

        let config = match feed_type {
            "binance_ws" => FeedConfig::BinanceWS {
                symbol: symbol.to_string(),
            },
            "polymarket_book" => FeedConfig::PolymarketBook {
                market_slug: symbol.to_string(),
            },
            "kalshi_book" => FeedConfig::KalshiBook {
                ticker: symbol.to_string(),
            },
            "rest" => FeedConfig::REST {
                url: url.to_string(),
                interval_secs: interval,
            },
            "predictit" => {
                let cfg = parse_config_json(config_json, "predictit")?;
                FeedConfig::PredictIt {
                    market_id: cfg
                        .get("market_id")
                        .and_then(|v| v.as_u64())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "predictit feed requires 'market_id' (integer) in config_json",
                            )
                        })?,
                    contract_id: cfg.get("contract_id").and_then(|v| v.as_u64()),
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            "manifold" => {
                let cfg = parse_config_json(config_json, "manifold")?;
                FeedConfig::Manifold {
                    slug: cfg
                        .get("slug")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "manifold feed requires 'slug' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            "espn" => {
                let cfg = parse_config_json(config_json, "espn")?;
                FeedConfig::ESPN {
                    sport: cfg
                        .get("sport")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "espn feed requires 'sport' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    league: cfg
                        .get("league")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "espn feed requires 'league' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    event_id: cfg
                        .get("event_id")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string()),
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            "nws" => {
                let cfg = parse_config_json(config_json, "nws")?;
                let mode = cfg
                    .get("mode")
                    .and_then(|v| v.as_str())
                    .unwrap_or("forecast")
                    .to_string();
                FeedConfig::NWS {
                    mode,
                    office: cfg
                        .get("office")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string(),
                    grid_x: cfg
                        .get("grid_x")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0) as u32,
                    grid_y: cfg
                        .get("grid_y")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(0) as u32,
                    state: cfg
                        .get("state")
                        .and_then(|v| v.as_str())
                        .unwrap_or("")
                        .to_string(),
                    user_agent: cfg
                        .get("user_agent")
                        .and_then(|v| v.as_str())
                        .unwrap_or("Horizon-SDK/0.4.7")
                        .to_string(),
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            "rest_json_path" => {
                let cfg = parse_config_json(config_json, "rest_json_path")?;
                FeedConfig::RESTJsonPath {
                    url: cfg
                        .get("url")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "rest_json_path feed requires 'url' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    price_path: cfg
                        .get("price_path")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string()),
                    bid_path: cfg
                        .get("bid_path")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string()),
                    ask_path: cfg
                        .get("ask_path")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string()),
                    volume_path: cfg
                        .get("volume_path")
                        .and_then(|v| v.as_str())
                        .map(|s| s.to_string()),
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            "chainlink" => {
                let cfg = parse_config_json(config_json, "chainlink")?;
                FeedConfig::Chainlink {
                    contract_address: cfg
                        .get("contract_address")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "chainlink feed requires 'contract_address' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    rpc_url: cfg
                        .get("rpc_url")
                        .and_then(|v| v.as_str())
                        .ok_or_else(|| {
                            pyo3::exceptions::PyValueError::new_err(
                                "chainlink feed requires 'rpc_url' (string) in config_json",
                            )
                        })?
                        .to_string(),
                    decimals: cfg
                        .get("decimals")
                        .and_then(|v| v.as_u64())
                        .unwrap_or(8) as u8,
                    interval_secs: cfg
                        .get("interval")
                        .and_then(|v| v.as_f64())
                        .unwrap_or(interval),
                }
            }
            _ => {
                return Err(pyo3::exceptions::PyValueError::new_err(format!(
                    "unknown feed type: {}",
                    feed_type
                )));
            }
        };

        self.feed_manager
            .as_ref()
            .unwrap()
            .start_feed(name, config);

        Ok(())
    }

    fn feed_snapshot(&self, name: &str) -> Option<FeedSnapshot> {
        self.feed_manager.as_ref()?.snapshot(name)
    }

    fn feed_age(&self, name: &str) -> Option<f64> {
        let snap = self.feed_manager.as_ref()?.snapshot(name)?;
        if snap.timestamp <= 0.0 {
            return None;
        }
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs_f64();
        Some(now - snap.timestamp)
    }

    fn all_feed_snapshots(&self) -> HashMap<String, FeedSnapshot> {
        self.feed_manager
            .as_ref()
            .map(|fm| fm.all_snapshots())
            .unwrap_or_default()
    }

    /// Get the L2 orderbook snapshot for a feed.
    fn orderbook_snapshot(&self, name: &str) -> Option<crate::orderbook::OrderbookSnapshot> {
        self.feed_manager.as_ref()?.orderbook(name)
    }

    /// Get all L2 orderbook snapshots.
    fn all_orderbook_snapshots(&self) -> HashMap<String, crate::orderbook::OrderbookSnapshot> {
        self.feed_manager
            .as_ref()
            .map(|fm| fm.all_orderbooks())
            .unwrap_or_default()
    }

    /// Record a feed tick to the database for historical storage.
    /// Call periodically to build tick history for replay/analysis.
    fn record_tick(&self, feed_name: &str, market_id: &str) -> bool {
        let snap = match self.feed_manager.as_ref().and_then(|fm| fm.snapshot(feed_name)) {
            Some(s) if s.price > 0.0 => s,
            _ => return false,
        };
        if let Some(ref db_mutex) = self.db {
            if let Ok(db) = db_mutex.lock() {
                return db
                    .insert_tick(
                        feed_name,
                        market_id,
                        snap.price,
                        snap.bid,
                        snap.ask,
                        snap.volume_24h,
                        &snap.source,
                        snap.timestamp,
                    )
                    .is_ok();
            }
        }
        false
    }

    /// Query historical ticks for a feed within a time range.
    /// Returns list of (feed_name, price, bid, ask, volume, source, timestamp) tuples.
    #[pyo3(signature = (feed_name, start_ts, end_ts, limit=10000))]
    fn query_ticks(
        &self,
        feed_name: &str,
        start_ts: f64,
        end_ts: f64,
        limit: u32,
    ) -> Vec<(String, f64, f64, f64, f64, String, f64)> {
        if let Some(ref db_mutex) = self.db {
            if let Ok(db) = db_mutex.lock() {
                return db.query_ticks(feed_name, start_ts, end_ts, limit).unwrap_or_default();
            }
        }
        Vec::new()
    }

    /// Count of stored ticks for a feed.
    fn tick_count(&self, feed_name: &str) -> u64 {
        if let Some(ref db_mutex) = self.db {
            if let Ok(db) = db_mutex.lock() {
                return db.tick_count(feed_name).unwrap_or(0);
            }
        }
        0
    }

    /// Purge ticks older than max_age_secs. Returns number of rows deleted.
    fn purge_old_ticks(&self, max_age_secs: f64) -> usize {
        if let Some(ref db_mutex) = self.db {
            if let Ok(db) = db_mutex.lock() {
                return db.purge_old_ticks(max_age_secs).unwrap_or(0);
            }
        }
        0
    }

    /// Get metrics for a specific feed.
    fn feed_metrics(&self, name: &str) -> Option<crate::feeds::FeedMetrics> {
        self.feed_manager.as_ref()?.feed_metrics(name)
    }

    /// Get metrics for all feeds.
    fn all_feed_metrics(&self) -> HashMap<String, crate::feeds::FeedMetrics> {
        self.feed_manager
            .as_ref()
            .map(|fm| fm.all_feed_metrics())
            .unwrap_or_default()
    }

    /// Stop a single feed by name.
    fn stop_feed(&self, name: &str) {
        if let Some(ref fm) = self.feed_manager {
            fm.stop_feed(name);
        }
    }

    /// Restart a feed by name. Uses stored config if no new config provided.
    fn restart_feed(&self, name: String) {
        if let Some(ref fm) = self.feed_manager {
            fm.restart_feed(name, None);
        }
    }

    /// Check if a feed is still running.
    fn is_feed_running(&self, name: &str) -> bool {
        self.feed_manager
            .as_ref()
            .map(|fm| fm.is_feed_running(name))
            .unwrap_or(false)
    }

    fn stop_feeds(&self) {
        if let Some(ref fm) = self.feed_manager {
            fm.stop_all();
        }
    }

    /// Update the L2 orderbook for a BookSim exchange. Call before tick() each cycle.
    #[pyo3(signature = (market_id, bids, asks, timestamp=0.0, exchange=None))]
    fn update_book(
        &self,
        market_id: &str,
        bids: Vec<(f64, f64)>,
        asks: Vec<(f64, f64)>,
        timestamp: f64,
        exchange: Option<&str>,
    ) -> PyResult<()> {
        let (_, backend) = self.resolve_exchange(exchange)?;
        match backend {
            ExchangeBackend::BookSim(ref b) => {
                let book = crate::orderbook::OrderbookSnapshot::from_levels(
                    bids, asks, timestamp, "backtest".to_string(),
                );
                b.update_book(market_id, book);
                Ok(())
            }
            _ => Err(pyo3::exceptions::PyValueError::new_err(
                "update_book is only supported on BookSim exchanges",
            )),
        }
    }

    // -------------------------------------------------------------------
    // Order submission (with exchange routing)
    // -------------------------------------------------------------------

    /// Submit an order through the risk -> exchange pipeline.
    /// `exchange`: target exchange name (None = primary exchange).
    /// Releases the GIL during exchange HTTP calls so Python threads aren't blocked.
    #[pyo3(signature = (request, exchange=None))]
    fn submit_order(&self, py: Python<'_>, request: &OrderRequest, exchange: Option<&str>) -> PyResult<String> {
        let exch_name = exchange.unwrap_or(&self.primary_exchange);
        py.allow_threads(|| self.submit_order_to(request, exch_name))
    }

    /// Submit quotes (bid+ask pairs) for a market.
    /// `exchange`: target exchange name (None = primary exchange).
    /// Releases the GIL during exchange HTTP calls so Python threads aren't blocked.
    #[pyo3(signature = (market_id, quotes, side, token_id=None, neg_risk=false, exchange=None))]
    fn submit_quotes(
        &self,
        py: Python<'_>,
        market_id: &str,
        quotes: Vec<Quote>,
        side: Side,
        token_id: Option<String>,
        neg_risk: bool,
        exchange: Option<&str>,
    ) -> PyResult<Vec<String>> {
        let exch_name = exchange.unwrap_or(&self.primary_exchange);
        let market_id_owned = market_id.to_string();

        py.allow_threads(|| {
            let mut order_ids = Vec::with_capacity(quotes.len() * 2);

            for quote in &quotes {
                let bid_req = OrderRequest {
                    market_id: market_id_owned.clone(),
                    side,
                    order_side: OrderSide::Buy,
                    size: quote.size,
                    price: quote.bid,
                    order_type: OrderType::Limit,
                    time_in_force: TimeInForce::GTC,
                    post_only: true,
                    token_id: token_id.clone(),
                    neg_risk,
                };
                order_ids.push(self.submit_order_to(&bid_req, exch_name)?);

                let ask_req = OrderRequest {
                    market_id: market_id_owned.clone(),
                    side,
                    order_side: OrderSide::Sell,
                    size: quote.size,
                    price: quote.ask,
                    order_type: OrderType::Limit,
                    time_in_force: TimeInForce::GTC,
                    post_only: true,
                    token_id: token_id.clone(),
                    neg_risk,
                };
                order_ids.push(self.submit_order_to(&ask_req, exch_name)?);
            }

            Ok(order_ids)
        })
    }

    /// Submit a market order (convenience method).
    #[pyo3(signature = (market_id, side, order_side, size, token_id=None, neg_risk=false, exchange=None))]
    fn submit_market_order(
        &self,
        market_id: &str,
        side: Side,
        order_side: OrderSide,
        size: f64,
        token_id: Option<String>,
        neg_risk: bool,
        exchange: Option<&str>,
    ) -> PyResult<String> {
        let request = OrderRequest {
            market_id: market_id.to_string(),
            side,
            order_side,
            size,
            price: 0.0,
            order_type: OrderType::Market,
            time_in_force: TimeInForce::FOK,
            post_only: false,
            token_id,
            neg_risk,
        };
        let exch_name = exchange.unwrap_or(&self.primary_exchange);
        self.submit_order_to(&request, exch_name)
    }

    // -------------------------------------------------------------------
    // Cancel (cross-exchange)
    // -------------------------------------------------------------------

    /// Cancel an order by ID. Looks up which exchange the order belongs to.
    /// Releases the GIL during exchange HTTP calls.
    fn cancel(&self, py: Python<'_>, order_id: &str) -> PyResult<()> {
        // Determine which exchange this order belongs to
        let exchange_name = self
            .orders
            .get(order_id)
            .map(|o| o.exchange.clone())
            .unwrap_or_else(|| self.primary_exchange.clone());

        // Release GIL during exchange HTTP calls
        py.allow_threads(|| -> PyResult<()> {
            if let Some(backend) = self.exchanges.get(&exchange_name) {
                backend.as_exchange().cancel_order(order_id)?;
            } else {
                let mut canceled = false;
                for backend in self.exchanges.values() {
                    if backend.as_exchange().cancel_order(order_id).is_ok() {
                        canceled = true;
                        break;
                    }
                }
                if !canceled {
                    return Err(HorizonError::Order(format!(
                        "order not found on any exchange: {}",
                        order_id
                    ))
                    .into());
                }
            }
            Ok(())
        })?;

        self.orders.mark_canceled(order_id, "user requested");
        if let Some(order) = self.orders.get(order_id) {
            self.with_db("order_canceled", |db| db.insert_order_event(&order));
        }
        debug!(order_id = %order_id, "order canceled");
        Ok(())
    }

    /// Cancel all orders across ALL exchanges.
    /// Only marks orders as canceled locally for exchanges that succeeded.
    fn cancel_all(&self, py: Python<'_>) -> PyResult<u32> {
        // Release GIL during exchange HTTP calls
        let (total, failed_exchanges) = py.allow_threads(|| {
            let mut total = 0u32;
            let mut failed = HashSet::new();
            for (name, backend) in &self.exchanges {
                match backend.as_exchange().cancel_all() {
                    Ok(count) => {
                        total += count as u32;
                        if count > 0 {
                            debug!(exchange = %name, count = count, "canceled orders");
                        }
                    }
                    Err(e) => {
                        warn!(exchange = %name, error = %e, "cancel_all failed on exchange");
                        failed.insert(name.clone());
                    }
                }
            }
            (total, failed)
        });
        for order in self.orders.open_orders() {
            // Only mark as canceled if the exchange cancel succeeded
            if !failed_exchanges.contains(&order.exchange) {
                self.orders.mark_canceled(&order.id, "cancel_all");
                if let Some(updated) = self.orders.get(&order.id) {
                    self.with_db("order_cancel_all", |db| db.insert_order_event(&updated));
                }
            }
        }
        info!(count = total, "canceled all orders across all exchanges");
        Ok(total)
    }

    /// Cancel all orders for a specific market across ALL exchanges.
    /// Only marks orders as canceled locally for exchanges that succeeded.
    fn cancel_market(&self, py: Python<'_>, market_id: &str) -> PyResult<u32> {
        // Release GIL during exchange HTTP calls
        let (total, failed_exchanges) = py.allow_threads(|| {
            let mut total = 0u32;
            let mut failed = HashSet::new();
            for (name, backend) in &self.exchanges {
                match backend.as_exchange().cancel_for_market(market_id) {
                    Ok(count) => {
                        total += count as u32;
                        if count > 0 {
                            debug!(exchange = %name, market = %market_id, count = count, "canceled market orders");
                        }
                    }
                    Err(e) => {
                        debug!(exchange = %name, market = %market_id, error = %e, "cancel_market on exchange");
                        failed.insert(name.clone());
                    }
                }
            }
            (total, failed)
        });
        for order in self.orders.open_orders_for_market(market_id) {
            // Only mark as canceled if the exchange cancel succeeded
            if !failed_exchanges.contains(&order.exchange) {
                self.orders.mark_canceled(&order.id, "cancel_market");
                if let Some(updated) = self.orders.get(&order.id) {
                    self.with_db("order_cancel_market", |db| db.insert_order_event(&updated));
                }
            }
        }
        debug!(market_id = %market_id, count = total, "canceled orders for market");
        Ok(total)
    }

    // -------------------------------------------------------------------
    // Tick / poll (cross-exchange)
    // -------------------------------------------------------------------

    /// Tick the paper exchange with a new price and process fills.
    /// `exchange`: target exchange name (None = primary). Only paper exchanges support tick.
    #[pyo3(signature = (market_id, mid_price, exchange=None))]
    fn tick(&self, market_id: &str, mid_price: f64, exchange: Option<&str>) -> u32 {
        let (exch_name, backend) = match self.resolve_exchange(exchange) {
            Ok(pair) => pair,
            Err(_) => return 0,
        };

        let fill_count = backend.tick(market_id, mid_price);
        let new_fills = backend.as_exchange().drain_fills();
        let processed = self.process_exchange_fills(new_fills);

        if fill_count > 0 {
            debug!(exchange = %exch_name, market = %market_id, fills = fill_count, "tick");
        }

        processed
    }

    /// Poll fills from ALL live exchanges. Returns total number of new fills.
    /// Releases the GIL during exchange HTTP calls.
    fn poll_fills(&self, py: Python<'_>) -> u32 {
        // Collect fills from all live exchanges with GIL released
        let all_fills: Vec<Vec<Fill>> = py.allow_threads(|| {
            self.exchanges
                .iter()
                .filter(|(_, backend)| !backend.is_paper())
                .map(|(_, backend)| backend.as_exchange().drain_fills())
                .collect()
        });
        let mut total = 0u32;
        for new_fills in all_fills {
            if !new_fills.is_empty() {
                total += self.process_exchange_fills(new_fills);
            }
        }
        total
    }

    /// Process a fill manually (e.g., from external source).
    /// Deduplicates and updates order tracking like process_exchange_fills.
    fn process_fill(&self, fill: Fill) {
        // Dedup check with FIFO eviction (matches process_exchange_fills logic)
        match self.seen_fill_ids.lock() {
            Ok(mut seen) => {
                if !seen.insert(fill.fill_id.clone()) {
                    return; // Duplicate fill
                }
                let mut order = self.seen_fill_order.lock().unwrap_or_else(|e| e.into_inner());
                order.push_back(fill.fill_id.clone());
                // FIFO eviction
                if seen.len() > 50_000 {
                    let evict_count = 10_000.min(order.len());
                    for _ in 0..evict_count {
                        if let Some(old_id) = order.pop_front() {
                            seen.remove(&old_id);
                        }
                    }
                }
            }
            Err(poisoned) => {
                let mut seen = poisoned.into_inner();
                if !seen.insert(fill.fill_id.clone()) {
                    return;
                }
                let mut order = self.seen_fill_order.lock().unwrap_or_else(|e| e.into_inner());
                order.push_back(fill.fill_id.clone());
                if seen.len() > 50_000 {
                    let evict_count = 10_000.min(order.len());
                    for _ in 0..evict_count {
                        if let Some(old_id) = order.pop_front() {
                            seen.remove(&old_id);
                        }
                    }
                }
            }
        }
        self.with_db("insert_fill", |db| db.insert_fill(&fill));
        self.positions.apply_fill(&fill);
        self.orders.apply_fill_size(&fill.order_id, fill.size);
        Self::append_fills_capped(&self.fills, vec![fill]);
    }

    // -------------------------------------------------------------------
    // Positions / Orders / Status
    // -------------------------------------------------------------------

    fn update_mark_price(&self, market_id: &str, side: Side, mark_price: f64) {
        self.positions
            .update_mark_price(market_id, side, mark_price);
    }

    fn open_orders(&self) -> Vec<Order> {
        self.orders.open_orders()
    }

    fn open_orders_for_market(&self, market_id: &str) -> Vec<Order> {
        self.orders.open_orders_for_market(market_id)
    }

    fn positions(&self) -> Vec<Position> {
        self.positions.all_positions()
    }

    /// Get positions for a specific market (O(1) — avoids cloning all positions).
    fn positions_for_market(&self, market_id: &str) -> Vec<Position> {
        self.positions.positions_for_market(market_id)
    }

    fn recent_fills(&self) -> Vec<Fill> {
        self.fills
            .lock()
            .map(|fills| fills.clone())
            .unwrap_or_default()
    }

    fn status(&self) -> EngineStatus {
        let total_pnl =
            self.positions.total_realized_pnl() + self.positions.total_unrealized_pnl();
        let baseline = self
            .daily_pnl_baseline
            .lock()
            .map(|v| *v)
            .unwrap_or(0.0);
        EngineStatus {
            running: true,
            kill_switch_active: self.risk.is_kill_switch_active(),
            kill_switch_reason: self.risk.kill_switch_reason(),
            open_orders: self.orders.open_count(),
            active_positions: self.positions.active_count(),
            total_realized_pnl: self.positions.total_realized_pnl(),
            total_unrealized_pnl: self.positions.total_unrealized_pnl(),
            daily_pnl: total_pnl - baseline,
            uptime_secs: self.start_time.elapsed().as_secs(),
        }
    }

    /// Reset daily P&L baseline to current total P&L (call at midnight).
    fn reset_daily_pnl(&self) {
        let total_pnl =
            self.positions.total_realized_pnl() + self.positions.total_unrealized_pnl();
        if let Ok(mut bl) = self.daily_pnl_baseline.lock() {
            *bl = total_pnl;
        }
        info!(baseline = total_pnl, "daily PnL baseline reset");
    }

    // -------------------------------------------------------------------
    // Order Amendment
    // -------------------------------------------------------------------

    /// Amend an order's price and/or size. Returns the (possibly new) order ID.
    /// On paper exchange: same ID returned (in-place). On live: cancel+resubmit (new ID).
    #[pyo3(signature = (order_id, new_price=None, new_size=None))]
    fn amend_order(
        &self,
        order_id: &str,
        new_price: Option<f64>,
        new_size: Option<f64>,
    ) -> PyResult<String> {
        if new_price.is_none() && new_size.is_none() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "must specify new_price, new_size, or both",
            ));
        }

        // Look up the original order
        let original = self.orders.get(order_id).ok_or_else(|| {
            pyo3::exceptions::PyValueError::new_err(format!("order not found: {}", order_id))
        })?;

        let exch_name = original.exchange.clone();
        let backend = self.exchanges.get(&exch_name).ok_or_else(|| {
            pyo3::exceptions::PyValueError::new_err(format!("unknown exchange: {}", exch_name))
        })?;

        // Build an OrderRequest from the original order for risk check + resubmit.
        // Preserve token_id and neg_risk from the original order to avoid
        // submitting to the wrong contract on Polymarket.
        let req = OrderRequest {
            market_id: original.market_id.clone(),
            side: original.side,
            order_side: original.order_side,
            size: new_size.unwrap_or(original.size),
            price: new_price.unwrap_or(original.price),
            order_type: original.order_type,
            time_in_force: original.time_in_force,
            post_only: true,
            token_id: original.token_id.clone(),
            neg_risk: original.neg_risk,
        };

        // Risk check the new values
        let (exposure, notional) = self.positions.snapshot_exposure(&req.market_id);
        let adjusted_notional = self.apply_netting(notional);
        self.risk
            .check(&req, exposure, adjusted_notional)
            .map_err(|e| HorizonError::Risk(e))?;

        // Amend on the exchange
        match backend
            .as_exchange()
            .amend_order(order_id, new_price, new_size, &req)
        {
            Ok(new_id) => {
                self.orders
                    .amend_order(order_id, &new_id, new_price, new_size);
                info!(
                    old_id = %order_id,
                    new_id = %new_id,
                    exchange = %exch_name,
                    "order amended"
                );
                Ok(new_id)
            }
            Err(e) => {
                // If cancel succeeded but resubmit failed, mark old as canceled
                self.orders.mark_canceled(order_id, "amend_failed");
                warn!(
                    order_id = %order_id,
                    exchange = %exch_name,
                    error = %e,
                    "amend failed"
                );
                Err(e.into())
            }
        }
    }

    // -------------------------------------------------------------------
    // Contingent Orders (Stop-Loss / Take-Profit)
    // -------------------------------------------------------------------

    /// Add a stop-loss contingent order.
    #[pyo3(signature = (market_id, side, order_side, size, trigger_price, exchange=None))]
    fn add_stop_loss(
        &self,
        market_id: &str,
        side: Side,
        order_side: OrderSide,
        size: f64,
        trigger_price: f64,
        exchange: Option<&str>,
    ) -> String {
        let exch = exchange.unwrap_or(&self.primary_exchange);
        self.contingent
            .add_stop_loss(market_id, side, order_side, size, trigger_price, exch)
    }

    /// Add a take-profit contingent order.
    #[pyo3(signature = (market_id, side, order_side, size, trigger_price, trigger_pnl=None, exchange=None))]
    fn add_take_profit(
        &self,
        market_id: &str,
        side: Side,
        order_side: OrderSide,
        size: f64,
        trigger_price: f64,
        trigger_pnl: Option<f64>,
        exchange: Option<&str>,
    ) -> String {
        let exch = exchange.unwrap_or(&self.primary_exchange);
        self.contingent
            .add_take_profit(market_id, side, order_side, size, trigger_price, trigger_pnl, exch)
    }

    /// Submit a bracket order: entry + stop-loss + take-profit linked as OCO.
    /// Returns (entry_order_id, stop_loss_id, take_profit_id).
    #[pyo3(signature = (request, stop_trigger, take_profit_trigger, take_profit_pnl=None, exchange=None))]
    fn submit_bracket(
        &self,
        request: &OrderRequest,
        stop_trigger: f64,
        take_profit_trigger: f64,
        take_profit_pnl: Option<f64>,
        exchange: Option<&str>,
    ) -> PyResult<(String, String, String)> {
        let exch_name = exchange.unwrap_or(&self.primary_exchange);

        // Submit entry order
        let entry_id = self.submit_order_to(request, exch_name)?;

        // Determine exit side (opposite of entry)
        let exit_side = match request.order_side {
            OrderSide::Buy => OrderSide::Sell,
            OrderSide::Sell => OrderSide::Buy,
        };

        // Create contingent SL and TP
        let sl_id = self.contingent.add_stop_loss(
            &request.market_id,
            request.side,
            exit_side,
            request.size,
            stop_trigger,
            exch_name,
        );
        let tp_id = self.contingent.add_take_profit(
            &request.market_id,
            request.side,
            exit_side,
            request.size,
            take_profit_trigger,
            take_profit_pnl,
            exch_name,
        );

        // Link SL and TP as OCO
        self.contingent.link_oco(&sl_id, &tp_id);

        info!(
            entry = %entry_id,
            sl = %sl_id,
            tp = %tp_id,
            market = %request.market_id,
            "bracket order submitted"
        );

        Ok((entry_id, sl_id, tp_id))
    }

    /// Check and trigger contingent orders for a market at the current price.
    /// Returns the number of orders triggered.
    fn check_contingent_triggers(&self, market_id: &str, current_price: f64) -> u32 {
        // Get unrealized PnL for this market (O(1) targeted lookup)
        let unrealized_pnl = self.positions.unrealized_pnl_for_market(market_id);

        let to_trigger = self
            .contingent
            .check_triggers(market_id, current_price, unrealized_pnl);

        let mut count = 0u32;
        for (contingent_id, request, exch_name) in to_trigger {
            match self.submit_order_to(&request, &exch_name) {
                Ok(child_id) => {
                    // Record child order ID (already marked triggered in check_triggers)
                    self.contingent.mark_triggered(&contingent_id, &child_id);
                    // Cancel OCO partner
                    if let Some(partner_id) =
                        self.contingent.cancel_oco_partner(&contingent_id)
                    {
                        debug!(
                            contingent = %contingent_id,
                            partner = %partner_id,
                            "OCO partner canceled"
                        );
                    }
                    count += 1;
                    info!(
                        contingent = %contingent_id,
                        child = %child_id,
                        market = %market_id,
                        "contingent order triggered"
                    );
                }
                Err(e) => {
                    // Unmark so it can retry on the next cycle
                    self.contingent.unmark_triggered(&contingent_id);
                    warn!(
                        contingent = %contingent_id,
                        market = %market_id,
                        error = %e,
                        "contingent order trigger failed, will retry"
                    );
                }
            }
        }
        count
    }

    /// Cancel a contingent order by ID.
    fn cancel_contingent(&self, contingent_id: &str) -> bool {
        self.contingent.cancel(contingent_id)
    }

    /// Get all pending (not yet triggered) contingent orders.
    fn pending_contingent_orders(&self) -> Vec<ContingentOrder> {
        self.contingent.pending_orders()
    }

    // -------------------------------------------------------------------
    // Smart Order Routing
    // -------------------------------------------------------------------

    /// Submit an order to the exchange with the best price based on feed data.
    /// For Buy: route to exchange with lowest ask.
    /// For Sell: route to exchange with highest bid.
    #[pyo3(signature = (request, fallback_exchange=None))]
    fn submit_order_smart(
        &self,
        request: &OrderRequest,
        fallback_exchange: Option<&str>,
    ) -> PyResult<String> {
        let fallback = fallback_exchange.unwrap_or(&self.primary_exchange);

        // Collect feed snapshots from all exchanges
        let snapshots = self
            .feed_manager
            .as_ref()
            .map(|fm| fm.all_snapshots())
            .unwrap_or_default();

        // Find best exchange based on feed prices
        let mut best_exchange: Option<&str> = None;
        let mut best_price: Option<f64> = None;

        for (feed_name, snap) in &snapshots {
            // Map feed name to exchange: check if feed name starts with exchange name
            let exch_name = self
                .exchanges
                .keys()
                .find(|k| feed_name.starts_with(k.as_str()))
                .map(|k| k.as_str());

            let exch = match exch_name {
                Some(e) => e,
                None => continue,
            };

            match request.order_side {
                OrderSide::Buy => {
                    if snap.ask > 0.0 && best_price.map_or(true, |bp| snap.ask < bp) {
                        best_price = Some(snap.ask);
                        best_exchange = Some(exch);
                    }
                }
                OrderSide::Sell => {
                    if snap.bid > 0.0 && best_price.map_or(true, |bp| snap.bid > bp) {
                        best_price = Some(snap.bid);
                        best_exchange = Some(exch);
                    }
                }
            }
        }

        let target = best_exchange.unwrap_or(fallback);
        info!(
            target = %target,
            best_price = ?best_price,
            side = ?request.order_side,
            "smart routing"
        );

        match self.submit_order_to(request, target) {
            Ok(id) => Ok(id),
            Err(e) if target != fallback => {
                warn!(
                    target = %target,
                    fallback = %fallback,
                    error = %e,
                    "smart routing failed, falling back"
                );
                self.submit_order_to(request, fallback)
            }
            Err(e) => Err(e),
        }
    }

    // -------------------------------------------------------------------
    // Multi-Outcome Event Management
    // -------------------------------------------------------------------

    /// Register an event with its outcome market IDs.
    fn register_event(&mut self, event_id: String, market_ids: Vec<String>) {
        info!(event = %event_id, markets = ?market_ids, "event registered");
        for mid in &market_ids {
            self.market_to_event.insert(mid.clone(), event_id.clone());
        }
        self.event_markets.insert(event_id, market_ids);
    }

    /// Get total exposure across all outcome markets in an event.
    fn event_exposure(&self, event_id: &str) -> f64 {
        self.event_markets
            .get(event_id)
            .map(|mids| self.positions.event_exposure(mids))
            .unwrap_or(0.0)
    }

    /// Get all positions for markets in an event.
    fn event_positions(&self, event_id: &str) -> Vec<Position> {
        let market_ids = match self.event_markets.get(event_id) {
            Some(ids) => ids,
            None => return Vec::new(),
        };
        let all = self.positions.all_positions();
        all.into_iter()
            .filter(|p| market_ids.contains(&p.market_id))
            .collect()
    }

    /// Check event parity using feed data for outcome prices.
    #[pyo3(signature = (event_id, threshold=0.02))]
    fn event_parity_check(
        &self,
        event_id: &str,
        threshold: f64,
    ) -> Option<crate::parity::ParityResult> {
        let market_ids = self.event_markets.get(event_id)?;
        let snapshots = self
            .feed_manager
            .as_ref()
            .map(|fm| fm.all_snapshots())
            .unwrap_or_default();

        let mut prices = Vec::new();
        for mid in market_ids {
            // Look for a feed snapshot that matches the market_id
            let price = snapshots
                .iter()
                .find(|(name, _)| name.contains(mid.as_str()))
                .map(|(_, snap)| {
                    if snap.bid > 0.0 && snap.ask > 0.0 {
                        (snap.bid + snap.ask) / 2.0
                    } else {
                        snap.price
                    }
                })
                .unwrap_or(0.0);
            prices.push(price);
        }

        // Only check if we have at least some prices
        if prices.iter().all(|p| *p <= 0.0) {
            return None;
        }

        let exchange = self.primary_exchange.clone();
        Some(crate::parity::check_event_parity(
            event_id, &prices, threshold, &exchange,
        ))
    }

    /// Reverse lookup: get event_id for a market_id.
    fn market_event_id(&self, market_id: &str) -> Option<String> {
        self.market_to_event.get(market_id).cloned()
    }

    /// Get all registered events and their market IDs.
    fn registered_events(&self) -> HashMap<String, Vec<String>> {
        self.event_markets.clone()
    }

    // -------------------------------------------------------------------
    // Parity Monitor
    // -------------------------------------------------------------------

    /// Check YES/NO price parity for a market using feed data.
    /// Returns a ParityResult with deviation from 1.0.
    /// `feed_name`: optional — use a specific feed's data instead of scanning all feeds.
    #[pyo3(signature = (market_id, threshold=0.02, feed_name=None))]
    fn parity_check(
        &self,
        market_id: &str,
        threshold: f64,
        feed_name: Option<&str>,
    ) -> Option<crate::parity::ParityResult> {
        let fm = self.feed_manager.as_ref()?;

        // If a specific feed_name is provided, use only that feed
        if let Some(name) = feed_name {
            let snap = fm.snapshot(name)?;
            if snap.bid > 0.0 && snap.ask > 0.0 {
                let yes_price = (snap.bid + snap.ask) / 2.0;
                let no_price = 1.0 - yes_price;
                let exchange = self
                    .exchanges
                    .keys()
                    .find(|k| name.starts_with(k.as_str()))
                    .cloned()
                    .unwrap_or_else(|| self.primary_exchange.clone());
                return Some(crate::parity::check_parity(
                    market_id, yes_price, no_price, threshold, &exchange,
                ));
            }
            return None;
        }

        // Scan all feeds for one with bid/ask data
        let snapshots = fm.all_snapshots();
        for (fname, snap) in &snapshots {
            if snap.bid > 0.0 && snap.ask > 0.0 {
                let yes_price = (snap.bid + snap.ask) / 2.0;
                let no_price = 1.0 - yes_price;
                let exchange = self
                    .exchanges
                    .keys()
                    .find(|k| fname.starts_with(k.as_str()))
                    .cloned()
                    .unwrap_or_else(|| self.primary_exchange.clone());
                return Some(crate::parity::check_parity(
                    market_id, yes_price, no_price, threshold, &exchange,
                ));
            }
        }

        // Fallback: check if we have both YES and NO positions with mark prices
        let positions = self.positions.all_positions();
        let yes_pos = positions.iter().find(|p| p.market_id == market_id && p.side == Side::Yes);
        let no_pos = positions.iter().find(|p| p.market_id == market_id && p.side == Side::No);
        if let (Some(yp), Some(np)) = (yes_pos, no_pos) {
            if yp.avg_entry_price > 0.0 && np.avg_entry_price > 0.0 {
                return Some(crate::parity::check_parity(
                    market_id,
                    yp.avg_entry_price,
                    np.avg_entry_price,
                    threshold,
                    &yp.exchange,
                ));
            }
        }

        None
    }

    /// Scan for arbitrage opportunities across exchanges for a given market.
    /// `market_feed_map`: list of (feed_name, exchange_name, fee_rate) tuples.
    #[pyo3(signature = (market_id, market_feed_map, max_liquidity=100.0))]
    fn scan_arbitrage(
        &self,
        market_id: &str,
        market_feed_map: Vec<(String, String, f64)>,
        max_liquidity: f64,
    ) -> Vec<crate::parity::ArbitrageOpportunity> {
        let snapshots = self
            .feed_manager
            .as_ref()
            .map(|fm| fm.all_snapshots())
            .unwrap_or_default();

        // Collect bid/ask per exchange
        let mut exchange_prices: Vec<(&str, f64, f64, f64)> = Vec::new(); // (exchange, bid, ask, fee)
        for (feed_name, exchange_name, fee_rate) in &market_feed_map {
            if let Some(snap) = snapshots.get(feed_name.as_str()) {
                if snap.bid > 0.0 && snap.ask > 0.0 {
                    exchange_prices.push((exchange_name, snap.bid, snap.ask, *fee_rate));
                }
            }
        }

        let mut opportunities = Vec::new();
        let n = exchange_prices.len();
        for i in 0..n {
            for j in (i + 1)..n {
                let (ex_a, bid_a, ask_a, fee_a) = exchange_prices[i];
                let (ex_b, bid_b, ask_b, fee_b) = exchange_prices[j];
                if let Some(opp) = crate::parity::detect_arbitrage(
                    market_id,
                    ex_a, bid_a, ask_a,
                    ex_b, bid_b, ask_b,
                    fee_a, fee_b,
                    max_liquidity,
                ) {
                    if opp.raw_edge > 0.0 {
                        opportunities.push(opp);
                    }
                }
            }
        }
        opportunities
    }

    // -------------------------------------------------------------------
    // Market Lifecycle Management
    // -------------------------------------------------------------------

    /// Register a market's expiry timestamp for lifecycle management.
    /// `expiry_ts`: UNIX timestamp when the market resolves/expires.
    fn set_market_expiry(&mut self, market_id: String, expiry_ts: f64) {
        info!(market = %market_id, expiry = expiry_ts, "market expiry registered");
        self.market_expiries.insert(market_id, expiry_ts);
    }

    /// Set the lead time (seconds) before expiry to trigger lifecycle actions.
    fn set_lifecycle_lead_secs(&mut self, secs: f64) {
        self.lifecycle_lead_secs = secs.max(0.0);
    }

    /// Check all registered market expiries and auto-cancel orders for markets
    /// approaching settlement. Returns list of market IDs that were acted on.
    /// Releases the GIL during exchange cancel calls so Python threads aren't blocked.
    fn check_lifecycle(&self, py: Python<'_>) -> Vec<String> {
        if self.market_expiries.is_empty() {
            return Vec::new();
        }

        py.allow_threads(|| {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs_f64();

            let mut acted = Vec::new();
            for (market_id, &expiry_ts) in &self.market_expiries {
                let time_to_expiry = expiry_ts - now;
                if time_to_expiry <= self.lifecycle_lead_secs && time_to_expiry > 0.0 {
                    // Cancel all orders for this market
                    let open = self.orders.open_orders_for_market(market_id);
                    if !open.is_empty() {
                        for (_, backend) in &self.exchanges {
                            let _ = backend.as_exchange().cancel_for_market(market_id);
                        }
                        for order in &open {
                            self.orders.mark_canceled(&order.id, "lifecycle_expiry");
                        }
                        info!(
                            market = %market_id,
                            orders_canceled = open.len(),
                            time_to_expiry = time_to_expiry,
                            "lifecycle: auto-canceled orders near expiry"
                        );
                        acted.push(market_id.clone());
                    }
                }
            }
            acted
        })
    }

    /// Get all registered market expiry timestamps.
    fn market_expiries(&self) -> HashMap<String, f64> {
        self.market_expiries.clone()
    }

    /// Check if a market has expired (past its expiry timestamp).
    fn is_market_expired(&self, market_id: &str) -> bool {
        if let Some(&expiry) = self.market_expiries.get(market_id) {
            let now = std::time::SystemTime::now()
                .duration_since(std::time::UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs_f64();
            now >= expiry
        } else {
            false
        }
    }

    // -------------------------------------------------------------------
    // Risk controls
    // -------------------------------------------------------------------

    fn activate_kill_switch(&self, py: Python<'_>, reason: &str) -> PyResult<()> {
        warn!(reason = %reason, "KILL SWITCH ACTIVATED");
        self.risk.activate_kill_switch(reason);
        let details = format!(r#"{{"reason":"{}"}}"#, reason.replace('"', "\\\""));
        self.with_db("risk_kill_switch", |db| {
            db.insert_risk_event("kill_switch_activated", Some(&details))
        });
        self.cancel_all(py)?;
        Ok(())
    }

    fn deactivate_kill_switch(&self) {
        info!("kill switch deactivated");
        self.risk.deactivate_kill_switch();
        self.with_db("risk_kill_switch_off", |db| {
            db.insert_risk_event("kill_switch_deactivated", None)
        });
    }

    fn update_daily_pnl(&self, pnl: f64) {
        self.risk.update_daily_pnl(pnl);
    }

    fn set_daily_baseline(&self, baseline: f64) {
        self.risk.set_daily_baseline(baseline);
        if let Ok(mut bl) = self.daily_pnl_baseline.lock() {
            *bl = baseline;
        }
    }

    // -------------------------------------------------------------------
    // Exchange info
    // -------------------------------------------------------------------

    /// Get the primary exchange name (backward compatible).
    fn exchange_name(&self) -> String {
        self.primary_exchange.clone()
    }

    /// Fetch positions from an exchange and reconcile.
    /// Releases the GIL during exchange HTTP calls.
    #[pyo3(signature = (exchange=None))]
    fn sync_positions(&self, py: Python<'_>, exchange: Option<&str>) -> PyResult<u32> {
        let (exch_name, backend) = self.resolve_exchange(exchange)?;
        let remote_positions = py.allow_threads(|| backend.as_exchange().fetch_positions())?;
        let count = remote_positions.len() as u32;
        if !remote_positions.is_empty() {
            info!(count = count, exchange = %exch_name, "syncing positions from exchange");
            self.positions.reconcile(remote_positions);
        }
        Ok(count)
    }

    /// Evict terminal orders and paper exchange bookkeeping.
    #[pyo3(signature = (max_age_secs=300.0))]
    fn evict_stale_orders(&self, max_age_secs: f64) -> u32 {
        let count = self.orders.evict_terminal(max_age_secs);
        if count > 0 {
            debug!(count = count, "evicted terminal orders");
        }
        // Evict from all paper/book_sim exchanges
        for backend in self.exchanges.values() {
            match backend {
                ExchangeBackend::Paper(ref paper) => paper.evict_terminal(),
                ExchangeBackend::BookSim(ref bsim) => bsim.evict_terminal(),
                _ => {}
            }
        }
        count
    }

    // -------------------------------------------------------------------
    // Persistence: snapshot, recovery, run tracking
    // -------------------------------------------------------------------

    fn snapshot_positions(&self) -> u32 {
        let positions = self.positions.all_positions_for_snapshot();
        let count = positions.len() as u32;
        self.with_db("snapshot_positions", |db| {
            db.snapshot_positions(&positions)
        });
        count
    }

    fn recover_state(&self) -> u32 {
        let (snapshot_ts, snapshot_positions) = self
            .with_db_result("latest_position_snapshot", |db| {
                db.latest_position_snapshot()
            })
            .unwrap_or((0.0, Vec::new()));

        if !snapshot_positions.is_empty() {
            info!(
                count = snapshot_positions.len(),
                snapshot_ts = snapshot_ts,
                "restoring positions from snapshot"
            );
            self.positions.restore_from_snapshot(snapshot_positions);
        }

        let fills = self
            .with_db_result("fills_since", |db| db.fills_since(snapshot_ts))
            .unwrap_or_default();

        if !fills.is_empty() {
            info!(
                count = fills.len(),
                since = snapshot_ts,
                "replaying fills for recovery"
            );
            // Register fill IDs in seen_fill_ids to prevent re-processing
            if let Ok(mut seen) = self.seen_fill_ids.lock() {
                if let Ok(mut order) = self.seen_fill_order.lock() {
                    for fill in &fills {
                        if seen.insert(fill.fill_id.clone()) {
                            order.push_back(fill.fill_id.clone());
                        }
                    }
                } else {
                    for fill in &fills {
                        seen.insert(fill.fill_id.clone());
                    }
                }
            }
            for fill in &fills {
                self.positions.apply_fill(fill);
            }
            Self::append_fills_capped(&self.fills, fills);
        }

        self.positions.active_count()
    }

    fn start_run(&self, strategy_name: &str) {
        // Record all exchange names
        let exchange_names: Vec<&str> = self.exchanges.keys().map(|s| s.as_str()).collect();
        let exchange_str = exchange_names.join(",");
        self.with_db("start_run", |db| {
            db.start_run(strategy_name, &exchange_str, None)
        });
    }

    fn end_run(&self) {
        self.with_db("end_run", |db| db.end_run());
    }

    fn db_run_id(&self) -> Option<String> {
        self.with_db_result("run_id", |db| Ok::<_, std::fmt::Error>(db.run_id().to_string()))
    }

    fn has_persistence(&self) -> bool {
        self.db.is_some()
    }

    fn db_open_order_ids(&self) -> Vec<String> {
        self.with_db_result("latest_open_order_ids", |db| db.latest_open_order_ids())
            .unwrap_or_default()
    }

    // -------------------------------------------------------------------
    // Arbitrage Execution
    // -------------------------------------------------------------------

    /// Execute an arbitrage trade: simultaneous buy on one exchange and sell on another.
    ///
    /// Both legs go through the risk pipeline. Uses FOK time-in-force.
    /// If the sell leg fails after the buy succeeds, the buy order is auto-canceled.
    ///
    /// Returns (buy_order_id, sell_order_id) on success.
    #[pyo3(signature = (market_id, buy_exchange, sell_exchange, buy_price, sell_price, size,
                        side=None, token_id=None, neg_risk=false))]
    fn execute_arbitrage(
        &self,
        market_id: &str,
        buy_exchange: &str,
        sell_exchange: &str,
        buy_price: f64,
        sell_price: f64,
        size: f64,
        side: Option<Side>,
        token_id: Option<String>,
        neg_risk: bool,
    ) -> PyResult<(String, String)> {
        let s = side.unwrap_or(Side::Yes);

        let buy_req = OrderRequest {
            market_id: market_id.to_string(),
            side: s,
            order_side: OrderSide::Buy,
            size,
            price: buy_price,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::FOK,
            post_only: false,
            token_id: token_id.clone(),
            neg_risk,
        };

        let sell_req = OrderRequest {
            market_id: market_id.to_string(),
            side: s,
            order_side: OrderSide::Sell,
            size,
            price: sell_price,
            order_type: OrderType::Limit,
            time_in_force: TimeInForce::FOK,
            post_only: false,
            token_id,
            neg_risk,
        };

        // Submit buy leg first
        let buy_id = self.submit_order_to(&buy_req, buy_exchange)?;

        // Submit sell leg — if it fails, auto-cancel the buy
        match self.submit_order_to(&sell_req, sell_exchange) {
            Ok(sell_id) => {
                info!(
                    market = %market_id,
                    buy_exchange = %buy_exchange,
                    sell_exchange = %sell_exchange,
                    buy_price = buy_price,
                    sell_price = sell_price,
                    size = size,
                    "arbitrage executed"
                );
                Ok((buy_id, sell_id))
            }
            Err(e) => {
                // Auto-cancel the buy leg
                warn!(
                    market = %market_id,
                    buy_id = %buy_id,
                    error = %e,
                    "arb sell leg failed, auto-canceling buy leg"
                );
                if let Some(backend) = self.exchanges.get(buy_exchange) {
                    let _ = backend.as_exchange().cancel_order(&buy_id);
                    self.orders.mark_canceled(&buy_id, "arb_sell_failed");
                }
                Err(e)
            }
        }
    }
}

/// Parse the config_json parameter for new feed types.
fn parse_config_json(
    config_json: &str,
    feed_type: &str,
) -> pyo3::PyResult<serde_json::Value> {
    if config_json.is_empty() {
        return Err(pyo3::exceptions::PyValueError::new_err(format!(
            "{} feed requires config_json parameter",
            feed_type
        )));
    }
    serde_json::from_str::<serde_json::Value>(config_json).map_err(|e| {
        pyo3::exceptions::PyValueError::new_err(format!(
            "invalid config_json for {} feed: {}",
            feed_type, e
        ))
    })
}

/// Drop implementation: snapshot positions, end run, cancel all orders on all exchanges, stop feeds.
impl Drop for Engine {
    fn drop(&mut self) {
        // Snapshot positions before shutdown
        let positions = self.positions.all_positions_for_snapshot();
        if !positions.is_empty() {
            self.with_db("shutdown_snapshot", |db| {
                db.snapshot_positions(&positions)
            });
        }

        self.with_db("shutdown_end_run", |db| db.end_run());

        warn!("engine shutting down, attempting to cancel all orders");
        for (name, backend) in &self.exchanges {
            if let Ok(count) = backend.as_exchange().cancel_all() {
                if count > 0 {
                    info!(exchange = %name, count = count, "canceled orders on engine drop");
                }
            }
        }
        if let Some(ref fm) = self.feed_manager {
            fm.stop_all();
        }
    }
}
