//! PyO3 function bindings for RaptorBT.

use numpy::{PyArray1, PyReadonlyArray1};
use pyo3::prelude::*;

use std::collections::HashMap;

use crate::core::types::{
    BacktestConfig, CompiledSignals, Direction, InstrumentConfig, OhlcvData, StopConfig,
    TargetConfig,
};
use crate::indicators;
use crate::signals::synchronizer::SyncMode;
use crate::strategies::basket::{BasketBacktest, BasketConfig};
use crate::strategies::multi::{CombineMode, MultiStrategyBacktest, MultiStrategyConfig};
use crate::strategies::options::{
    OptionType, OptionsBacktest, OptionsConfig, SizeType, StrikeSelection,
};
use crate::strategies::pairs::{PairsBacktest, PairsConfig};
use crate::strategies::single::SingleBacktest;
use crate::strategies::spreads::{
    LegConfig, OptionType as SpreadOptionType, SpreadBacktest, SpreadConfig, SpreadType,
};
use crate::strategies::tick::{TickBacktest, TickBacktestConfig};

use super::numpy_bridge::*;

// ============================================================================
// Configuration Classes
// ============================================================================

/// Python-exposed backtest configuration.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyBacktestConfig {
    #[pyo3(get, set)]
    pub initial_capital: f64,
    #[pyo3(get, set)]
    pub fees: f64,
    #[pyo3(get, set)]
    pub slippage: f64,
    #[pyo3(get, set)]
    pub upon_bar_close: bool,
    stop_config: StopConfig,
    target_config: TargetConfig,
}

#[pymethods]
impl PyBacktestConfig {
    #[new]
    #[pyo3(signature = (initial_capital=100000.0, fees=0.001, slippage=0.0, upon_bar_close=true))]
    fn new(initial_capital: f64, fees: f64, slippage: f64, upon_bar_close: bool) -> Self {
        Self {
            initial_capital,
            fees,
            slippage,
            upon_bar_close,
            stop_config: StopConfig::None,
            target_config: TargetConfig::None,
        }
    }

    /// Set fixed percentage stop-loss.
    fn set_fixed_stop(&mut self, percent: f64) {
        self.stop_config = StopConfig::Fixed { percent };
    }

    /// Set ATR-based stop-loss.
    fn set_atr_stop(&mut self, multiplier: f64, period: usize) {
        self.stop_config = StopConfig::Atr { multiplier, period };
    }

    /// Set trailing stop-loss.
    fn set_trailing_stop(&mut self, percent: f64) {
        self.stop_config = StopConfig::Trailing { percent };
    }

    /// Set fixed percentage take-profit.
    fn set_fixed_target(&mut self, percent: f64) {
        self.target_config = TargetConfig::Fixed { percent };
    }

    /// Set ATR-based take-profit.
    fn set_atr_target(&mut self, multiplier: f64, period: usize) {
        self.target_config = TargetConfig::Atr { multiplier, period };
    }

    /// Set risk-reward based take-profit.
    fn set_risk_reward_target(&mut self, ratio: f64) {
        self.target_config = TargetConfig::RiskReward { ratio };
    }
}

impl From<&PyBacktestConfig> for BacktestConfig {
    fn from(py_config: &PyBacktestConfig) -> Self {
        BacktestConfig {
            initial_capital: py_config.initial_capital,
            fees: py_config.fees,
            slippage: py_config.slippage,
            stop: py_config.stop_config,
            target: py_config.target_config,
            upon_bar_close: py_config.upon_bar_close,
        }
    }
}

/// Python-exposed per-instrument configuration.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyInstrumentConfig {
    #[pyo3(get, set)]
    pub lot_size: Option<f64>,
    #[pyo3(get, set)]
    pub alloted_capital: Option<f64>,
    #[pyo3(get, set)]
    pub existing_qty: Option<f64>,
    #[pyo3(get, set)]
    pub avg_price: Option<f64>,
    stop_config: Option<StopConfig>,
    target_config: Option<TargetConfig>,
}

#[pymethods]
impl PyInstrumentConfig {
    #[new]
    #[pyo3(signature = (lot_size=None, alloted_capital=None, existing_qty=None, avg_price=None))]
    fn new(
        lot_size: Option<f64>,
        alloted_capital: Option<f64>,
        existing_qty: Option<f64>,
        avg_price: Option<f64>,
    ) -> Self {
        Self {
            lot_size,
            alloted_capital,
            existing_qty,
            avg_price,
            stop_config: None,
            target_config: None,
        }
    }

    /// Set fixed percentage stop-loss override.
    fn set_fixed_stop(&mut self, percent: f64) {
        self.stop_config = Some(StopConfig::Fixed { percent });
    }

    /// Set ATR-based stop-loss override.
    fn set_atr_stop(&mut self, multiplier: f64, period: usize) {
        self.stop_config = Some(StopConfig::Atr { multiplier, period });
    }

    /// Set trailing stop-loss override.
    fn set_trailing_stop(&mut self, percent: f64) {
        self.stop_config = Some(StopConfig::Trailing { percent });
    }

    /// Set fixed percentage take-profit override.
    fn set_fixed_target(&mut self, percent: f64) {
        self.target_config = Some(TargetConfig::Fixed { percent });
    }

    /// Set ATR-based take-profit override.
    fn set_atr_target(&mut self, multiplier: f64, period: usize) {
        self.target_config = Some(TargetConfig::Atr { multiplier, period });
    }

    /// Set risk-reward based take-profit override.
    fn set_risk_reward_target(&mut self, ratio: f64) {
        self.target_config = Some(TargetConfig::RiskReward { ratio });
    }

    fn __repr__(&self) -> String {
        format!(
            "InstrumentConfig(lot_size={:?}, alloted_capital={:?})",
            self.lot_size, self.alloted_capital
        )
    }
}

impl From<&PyInstrumentConfig> for InstrumentConfig {
    fn from(py_config: &PyInstrumentConfig) -> Self {
        InstrumentConfig {
            lot_size: py_config.lot_size,
            alloted_capital: py_config.alloted_capital,
            stop: py_config.stop_config,
            target: py_config.target_config,
            existing_qty: py_config.existing_qty,
            avg_price: py_config.avg_price,
        }
    }
}

/// Python-exposed stop configuration.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyStopConfig {
    #[pyo3(get, set)]
    pub stop_type: String,
    #[pyo3(get, set)]
    pub percent: Option<f64>,
    #[pyo3(get, set)]
    pub multiplier: Option<f64>,
    #[pyo3(get, set)]
    pub period: Option<usize>,
}

#[pymethods]
impl PyStopConfig {
    #[new]
    fn new() -> Self {
        Self { stop_type: "none".to_string(), percent: None, multiplier: None, period: None }
    }

    #[staticmethod]
    fn fixed(percent: f64) -> Self {
        Self {
            stop_type: "fixed".to_string(),
            percent: Some(percent),
            multiplier: None,
            period: None,
        }
    }

    #[staticmethod]
    fn atr(multiplier: f64, period: usize) -> Self {
        Self {
            stop_type: "atr".to_string(),
            percent: None,
            multiplier: Some(multiplier),
            period: Some(period),
        }
    }

    #[staticmethod]
    fn trailing(percent: f64) -> Self {
        Self {
            stop_type: "trailing".to_string(),
            percent: Some(percent),
            multiplier: None,
            period: None,
        }
    }
}

/// Python-exposed target configuration.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyTargetConfig {
    #[pyo3(get, set)]
    pub target_type: String,
    #[pyo3(get, set)]
    pub percent: Option<f64>,
    #[pyo3(get, set)]
    pub multiplier: Option<f64>,
    #[pyo3(get, set)]
    pub period: Option<usize>,
    #[pyo3(get, set)]
    pub ratio: Option<f64>,
}

#[pymethods]
impl PyTargetConfig {
    #[new]
    fn new() -> Self {
        Self {
            target_type: "none".to_string(),
            percent: None,
            multiplier: None,
            period: None,
            ratio: None,
        }
    }

    #[staticmethod]
    fn fixed(percent: f64) -> Self {
        Self {
            target_type: "fixed".to_string(),
            percent: Some(percent),
            multiplier: None,
            period: None,
            ratio: None,
        }
    }

    #[staticmethod]
    fn atr(multiplier: f64, period: usize) -> Self {
        Self {
            target_type: "atr".to_string(),
            percent: None,
            multiplier: Some(multiplier),
            period: Some(period),
            ratio: None,
        }
    }

    #[staticmethod]
    fn risk_reward(ratio: f64) -> Self {
        Self {
            target_type: "risk_reward".to_string(),
            percent: None,
            multiplier: None,
            period: None,
            ratio: Some(ratio),
        }
    }
}

// ============================================================================
// Result Classes
// ============================================================================

/// Python-exposed trade.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyTrade {
    #[pyo3(get)]
    pub id: u64,
    #[pyo3(get)]
    pub symbol: String,
    #[pyo3(get)]
    pub entry_idx: usize,
    #[pyo3(get)]
    pub exit_idx: usize,
    #[pyo3(get)]
    pub entry_price: f64,
    #[pyo3(get)]
    pub exit_price: f64,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub direction: i32,
    #[pyo3(get)]
    pub pnl: f64,
    #[pyo3(get)]
    pub return_pct: f64,
    #[pyo3(get)]
    pub entry_time: i64,
    #[pyo3(get)]
    pub exit_time: i64,
    #[pyo3(get)]
    pub fees: f64,
    #[pyo3(get)]
    pub exit_reason: String,
}

#[pymethods]
impl PyTrade {
    fn __repr__(&self) -> String {
        format!(
            "Trade(symbol={}, entry={:.2}, exit={:.2}, pnl={:.2}, return={:.2}%)",
            self.symbol, self.entry_price, self.exit_price, self.pnl, self.return_pct
        )
    }
}

/// Python-exposed backtest metrics.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyBacktestMetrics {
    #[pyo3(get)]
    pub total_return_pct: f64,
    #[pyo3(get)]
    pub sharpe_ratio: f64,
    #[pyo3(get)]
    pub sortino_ratio: f64,
    #[pyo3(get)]
    pub calmar_ratio: f64,
    #[pyo3(get)]
    pub omega_ratio: f64,
    #[pyo3(get)]
    pub max_drawdown_pct: f64,
    #[pyo3(get)]
    pub max_drawdown_duration: usize,
    #[pyo3(get)]
    pub win_rate_pct: f64,
    #[pyo3(get)]
    pub profit_factor: f64,
    #[pyo3(get)]
    pub expectancy: f64,
    #[pyo3(get)]
    pub sqn: f64,
    #[pyo3(get)]
    pub total_trades: usize,
    #[pyo3(get)]
    pub total_closed_trades: usize,
    #[pyo3(get)]
    pub total_open_trades: usize,
    #[pyo3(get)]
    pub open_trade_pnl: f64,
    #[pyo3(get)]
    pub winning_trades: usize,
    #[pyo3(get)]
    pub losing_trades: usize,
    #[pyo3(get)]
    pub start_value: f64,
    #[pyo3(get)]
    pub end_value: f64,
    #[pyo3(get)]
    pub total_fees_paid: f64,
    #[pyo3(get)]
    pub best_trade_pct: f64,
    #[pyo3(get)]
    pub worst_trade_pct: f64,
    #[pyo3(get)]
    pub avg_trade_return_pct: f64,
    #[pyo3(get)]
    pub avg_win_pct: f64,
    #[pyo3(get)]
    pub avg_loss_pct: f64,
    #[pyo3(get)]
    pub avg_winning_duration: f64,
    #[pyo3(get)]
    pub avg_losing_duration: f64,
    #[pyo3(get)]
    pub max_consecutive_wins: usize,
    #[pyo3(get)]
    pub max_consecutive_losses: usize,
    #[pyo3(get)]
    pub avg_holding_period: f64,
    #[pyo3(get)]
    pub exposure_pct: f64,
    #[pyo3(get)]
    pub payoff_ratio: f64,
    #[pyo3(get)]
    pub recovery_factor: f64,
}

#[pymethods]
impl PyBacktestMetrics {
    fn __repr__(&self) -> String {
        format!(
            "BacktestMetrics(return={:.2}%, sharpe={:.2}, max_dd={:.2}%, trades={})",
            self.total_return_pct, self.sharpe_ratio, self.max_drawdown_pct, self.total_trades
        )
    }

    /// Convert to dictionary of all metrics.
    fn to_dict(&self, py: Python) -> PyResult<PyObject> {
        let dict = pyo3::types::PyDict::new(py);
        dict.set_item("Start Value", self.start_value)?;
        dict.set_item("End Value", self.end_value)?;
        dict.set_item("Total Return [%]", self.total_return_pct)?;
        dict.set_item("Total Fees Paid", self.total_fees_paid)?;
        dict.set_item("Max Drawdown [%]", self.max_drawdown_pct)?;
        dict.set_item("Max Drawdown Duration", self.max_drawdown_duration)?;
        dict.set_item("Total Trades", self.total_trades)?;
        dict.set_item("Total Closed Trades", self.total_closed_trades)?;
        dict.set_item("Total Open Trades", self.total_open_trades)?;
        dict.set_item("Open Trade PnL", self.open_trade_pnl)?;
        dict.set_item("Win Rate [%]", self.win_rate_pct)?;
        dict.set_item("Best Trade [%]", self.best_trade_pct)?;
        dict.set_item("Worst Trade [%]", self.worst_trade_pct)?;
        dict.set_item("Avg Winning Trade [%]", self.avg_win_pct)?;
        dict.set_item("Avg Losing Trade [%]", self.avg_loss_pct)?;
        dict.set_item("Avg Winning Trade Duration", self.avg_winning_duration)?;
        dict.set_item("Avg Losing Trade Duration", self.avg_losing_duration)?;
        dict.set_item("Profit Factor", self.profit_factor)?;
        dict.set_item("Expectancy", self.expectancy)?;
        dict.set_item("SQN", self.sqn)?;
        dict.set_item("Sharpe Ratio", self.sharpe_ratio)?;
        dict.set_item("Sortino Ratio", self.sortino_ratio)?;
        dict.set_item("Calmar Ratio", self.calmar_ratio)?;
        dict.set_item("Omega Ratio", self.omega_ratio)?;
        Ok(dict.into())
    }
}

/// Python-exposed backtest result.
#[pyclass]
#[derive(Debug, Clone)]
pub struct PyBacktestResult {
    #[pyo3(get)]
    pub metrics: PyBacktestMetrics,
    equity_curve: Vec<f64>,
    drawdown_curve: Vec<f64>,
    trades: Vec<PyTrade>,
    returns: Vec<f64>,
}

#[pymethods]
impl PyBacktestResult {
    /// Get equity curve as numpy array.
    fn equity_curve<'py>(&self, py: Python<'py>) -> &'py PyArray1<f64> {
        vec_to_numpy_f64(py, self.equity_curve.clone())
    }

    /// Get drawdown curve as numpy array.
    fn drawdown_curve<'py>(&self, py: Python<'py>) -> &'py PyArray1<f64> {
        vec_to_numpy_f64(py, self.drawdown_curve.clone())
    }

    /// Get returns as numpy array.
    fn returns<'py>(&self, py: Python<'py>) -> &'py PyArray1<f64> {
        vec_to_numpy_f64(py, self.returns.clone())
    }

    /// Get list of trades.
    fn trades(&self) -> Vec<PyTrade> {
        self.trades.clone()
    }

    fn __repr__(&self) -> String {
        format!(
            "BacktestResult(return={:.2}%, trades={}, max_dd={:.2}%)",
            self.metrics.total_return_pct, self.metrics.total_trades, self.metrics.max_drawdown_pct
        )
    }
}

// ============================================================================
// Backtest Functions
// ============================================================================

/// Run single instrument backtest.
#[pyfunction]
#[pyo3(signature = (timestamps, open, high, low, close, volume, entries, exits, direction=1, weight=1.0, symbol="UNKNOWN", config=None, position_sizes=None, instrument_config=None))]
pub fn run_single_backtest<'py>(
    _py: Python<'py>,
    timestamps: PyReadonlyArray1<i64>,
    open: PyReadonlyArray1<f64>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    entries: PyReadonlyArray1<bool>,
    exits: PyReadonlyArray1<bool>,
    direction: i32,
    weight: f64,
    symbol: &str,
    config: Option<&PyBacktestConfig>,
    position_sizes: Option<PyReadonlyArray1<f64>>,
    instrument_config: Option<&PyInstrumentConfig>,
) -> PyResult<PyBacktestResult> {
    let ohlcv = OhlcvData {
        timestamps: numpy_to_vec_i64(timestamps),
        open: numpy_to_vec_f64(open),
        high: numpy_to_vec_f64(high),
        low: numpy_to_vec_f64(low),
        close: numpy_to_vec_f64(close),
        volume: numpy_to_vec_f64(volume),
    };

    let dir = Direction::from_int(direction).unwrap_or(Direction::Long);

    let signals = CompiledSignals {
        symbol: symbol.to_string(),
        entries: numpy_to_vec_bool(entries),
        exits: numpy_to_vec_bool(exits),
        position_sizes: position_sizes.map(numpy_to_vec_f64),
        direction: dir,
        weight,
    };

    let rust_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default();
    let inst_config = instrument_config.map(InstrumentConfig::from);

    let backtest = SingleBacktest::new(rust_config);
    let result = backtest.run_with_instrument_config(&ohlcv, &signals, inst_config.as_ref());

    Ok(convert_result(result))
}

/// Run basket/collective backtest.
#[pyfunction]
#[pyo3(signature = (instruments, config=None, sync_mode="all", instrument_configs=None))]
pub fn run_basket_backtest<'py>(
    _py: Python<'py>,
    instruments: Vec<(
        PyReadonlyArray1<i64>,
        PyReadonlyArray1<f64>,
        PyReadonlyArray1<f64>,
        PyReadonlyArray1<f64>,
        PyReadonlyArray1<f64>,
        PyReadonlyArray1<f64>,
        PyReadonlyArray1<bool>,
        PyReadonlyArray1<bool>,
        i32,
        f64,
        String,
    )>,
    config: Option<&PyBacktestConfig>,
    sync_mode: &str,
    instrument_configs: Option<HashMap<String, PyInstrumentConfig>>,
) -> PyResult<PyBacktestResult> {
    let rust_instruments: Vec<(OhlcvData, CompiledSignals)> = instruments
        .into_iter()
        .map(|(ts, o, h, l, c, v, entries, exits, dir, weight, sym)| {
            let ohlcv = OhlcvData {
                timestamps: numpy_to_vec_i64(ts),
                open: numpy_to_vec_f64(o),
                high: numpy_to_vec_f64(h),
                low: numpy_to_vec_f64(l),
                close: numpy_to_vec_f64(c),
                volume: numpy_to_vec_f64(v),
            };
            let signals = CompiledSignals {
                symbol: sym,
                entries: numpy_to_vec_bool(entries),
                exits: numpy_to_vec_bool(exits),
                position_sizes: None,
                direction: Direction::from_int(dir).unwrap_or(Direction::Long),
                weight,
            };
            (ohlcv, signals)
        })
        .collect();

    let mode = match sync_mode {
        "any" => SyncMode::Any,
        "majority" => SyncMode::Majority,
        "master" => SyncMode::Master,
        _ => SyncMode::All,
    };

    let basket_config = BasketConfig {
        base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(),
        sync_mode: mode,
        ..Default::default()
    };

    // Convert PyInstrumentConfig map to InstrumentConfig map
    let rust_inst_configs: Option<HashMap<String, InstrumentConfig>> =
        instrument_configs.map(|configs| {
            configs.iter().map(|(k, v)| (k.clone(), InstrumentConfig::from(v))).collect()
        });

    let backtest = BasketBacktest::new(basket_config);
    let result =
        backtest.run_with_instrument_configs(&rust_instruments, rust_inst_configs.as_ref());

    Ok(convert_result(result))
}

/// Run options backtest.
#[pyfunction]
#[pyo3(signature = (timestamps, open, high, low, close, volume, option_prices, entries, exits, direction=1, symbol="OPTION", config=None, option_type="call", strike_selection="atm", size_type="percent", size_value=1.0, lot_size=1, strike_interval=50.0))]
pub fn run_options_backtest<'py>(
    _py: Python<'py>,
    timestamps: PyReadonlyArray1<i64>,
    open: PyReadonlyArray1<f64>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    option_prices: PyReadonlyArray1<f64>,
    entries: PyReadonlyArray1<bool>,
    exits: PyReadonlyArray1<bool>,
    direction: i32,
    symbol: &str,
    config: Option<&PyBacktestConfig>,
    option_type: &str,
    strike_selection: &str,
    size_type: &str,
    size_value: f64,
    lot_size: usize,
    strike_interval: f64,
) -> PyResult<PyBacktestResult> {
    let ohlcv = OhlcvData {
        timestamps: numpy_to_vec_i64(timestamps),
        open: numpy_to_vec_f64(open),
        high: numpy_to_vec_f64(high),
        low: numpy_to_vec_f64(low),
        close: numpy_to_vec_f64(close),
        volume: numpy_to_vec_f64(volume),
    };

    let opt_prices = numpy_to_vec_f64(option_prices);

    let dir = Direction::from_int(direction).unwrap_or(Direction::Long);

    let signals = CompiledSignals {
        symbol: symbol.to_string(),
        entries: numpy_to_vec_bool(entries),
        exits: numpy_to_vec_bool(exits),
        position_sizes: None,
        direction: dir,
        weight: 1.0,
    };

    let opt_type = match option_type {
        "put" => OptionType::Put,
        _ => OptionType::Call,
    };

    let strike_sel = match strike_selection {
        "otm1" => StrikeSelection::Otm(1),
        "otm2" => StrikeSelection::Otm(2),
        "itm1" => StrikeSelection::Itm(1),
        "itm2" => StrikeSelection::Itm(2),
        _ => StrikeSelection::Atm,
    };

    let size = match size_type {
        "contracts" => SizeType::Contracts(size_value as usize),
        "notional" => SizeType::Notional(size_value),
        "risk" => SizeType::RiskPercent(size_value),
        _ => SizeType::Percent(size_value),
    };

    let options_config = OptionsConfig {
        base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(),
        option_type: opt_type,
        strike_selection: strike_sel,
        size_type: size,
        lot_size,
        strike_interval,
        target_dte: None,
    };

    let backtest = OptionsBacktest::new(options_config);
    let result = backtest.run(&ohlcv, &opt_prices, &signals);

    Ok(convert_result(result))
}

/// Run pairs trading backtest.
#[pyfunction]
#[pyo3(signature = (leg1_timestamps, leg1_open, leg1_high, leg1_low, leg1_close, leg1_volume, leg2_timestamps, leg2_open, leg2_high, leg2_low, leg2_close, leg2_volume, entries, exits, direction=1, symbol="PAIR", config=None, hedge_ratio=1.0, dynamic_hedge=false))]
pub fn run_pairs_backtest<'py>(
    _py: Python<'py>,
    leg1_timestamps: PyReadonlyArray1<i64>,
    leg1_open: PyReadonlyArray1<f64>,
    leg1_high: PyReadonlyArray1<f64>,
    leg1_low: PyReadonlyArray1<f64>,
    leg1_close: PyReadonlyArray1<f64>,
    leg1_volume: PyReadonlyArray1<f64>,
    leg2_timestamps: PyReadonlyArray1<i64>,
    leg2_open: PyReadonlyArray1<f64>,
    leg2_high: PyReadonlyArray1<f64>,
    leg2_low: PyReadonlyArray1<f64>,
    leg2_close: PyReadonlyArray1<f64>,
    leg2_volume: PyReadonlyArray1<f64>,
    entries: PyReadonlyArray1<bool>,
    exits: PyReadonlyArray1<bool>,
    direction: i32,
    symbol: &str,
    config: Option<&PyBacktestConfig>,
    hedge_ratio: f64,
    dynamic_hedge: bool,
) -> PyResult<PyBacktestResult> {
    let leg1_ohlcv = OhlcvData {
        timestamps: numpy_to_vec_i64(leg1_timestamps),
        open: numpy_to_vec_f64(leg1_open),
        high: numpy_to_vec_f64(leg1_high),
        low: numpy_to_vec_f64(leg1_low),
        close: numpy_to_vec_f64(leg1_close),
        volume: numpy_to_vec_f64(leg1_volume),
    };

    let leg2_ohlcv = OhlcvData {
        timestamps: numpy_to_vec_i64(leg2_timestamps),
        open: numpy_to_vec_f64(leg2_open),
        high: numpy_to_vec_f64(leg2_high),
        low: numpy_to_vec_f64(leg2_low),
        close: numpy_to_vec_f64(leg2_close),
        volume: numpy_to_vec_f64(leg2_volume),
    };

    let dir = Direction::from_int(direction).unwrap_or(Direction::Long);

    let signals = CompiledSignals {
        symbol: symbol.to_string(),
        entries: numpy_to_vec_bool(entries),
        exits: numpy_to_vec_bool(exits),
        position_sizes: None,
        direction: dir,
        weight: 1.0,
    };

    let pairs_config = PairsConfig {
        base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(),
        hedge_ratio,
        dynamic_hedge,
        ..Default::default()
    };

    let backtest = PairsBacktest::new(pairs_config);
    let result = backtest.run(&leg1_ohlcv, &leg2_ohlcv, &signals);

    Ok(convert_result(result))
}

/// Run spread backtest (multi-leg options).
#[pyfunction]
#[pyo3(signature = (timestamps, underlying_close, legs_premiums, leg_configs, entries, exits, config=None, spread_type="custom", max_loss=None, target_profit=None, leg_expiry_timestamps=None))]
pub fn run_spread_backtest<'py>(
    _py: Python<'py>,
    timestamps: PyReadonlyArray1<i64>,
    underlying_close: PyReadonlyArray1<f64>,
    legs_premiums: Vec<PyReadonlyArray1<f64>>,
    leg_configs: Vec<(String, f64, i32, usize)>, // (option_type, strike, quantity, lot_size)
    entries: PyReadonlyArray1<bool>,
    exits: PyReadonlyArray1<bool>,
    config: Option<&PyBacktestConfig>,
    spread_type: &str,
    max_loss: Option<f64>,
    target_profit: Option<f64>,
    leg_expiry_timestamps: Option<Vec<i64>>,
) -> PyResult<PyBacktestResult> {
    let ts = numpy_to_vec_i64(timestamps);
    let underlying = numpy_to_vec_f64(underlying_close);
    let premiums: Vec<Vec<f64>> = legs_premiums.into_iter().map(numpy_to_vec_f64).collect();
    let entry_signals = numpy_to_vec_bool(entries);
    let exit_signals = numpy_to_vec_bool(exits);

    // Convert leg configs
    let rust_leg_configs: Vec<LegConfig> = leg_configs
        .into_iter()
        .map(|(opt_type, strike, quantity, lot_size)| {
            let option_type =
                SpreadOptionType::from_str(&opt_type).unwrap_or(SpreadOptionType::Call);
            LegConfig::new(option_type, strike, quantity, lot_size)
        })
        .collect();

    // Parse spread type
    let spread_type_enum = match spread_type.to_lowercase().as_str() {
        "straddle" => SpreadType::Straddle,
        "strangle" => SpreadType::Strangle,
        "vertical_call" | "verticalcall" => SpreadType::VerticalCall,
        "vertical_put" | "verticalput" => SpreadType::VerticalPut,
        "iron_condor" | "ironcondor" => SpreadType::IronCondor,
        "iron_butterfly" | "ironbutterfly" => SpreadType::IronButterfly,
        "butterfly_call" | "butterflycall" => SpreadType::ButterflyCall,
        "butterfly_put" | "butterflyput" => SpreadType::ButterflyPut,
        "calendar" => SpreadType::Calendar,
        "diagonal" => SpreadType::Diagonal,
        "long_call" | "longcall" => SpreadType::LongCall,
        "long_put" | "longput" => SpreadType::LongPut,
        "naked_call" | "nakedcall" => SpreadType::NakedCall,
        "naked_put" | "nakedput" => SpreadType::NakedPut,
        _ => SpreadType::Custom,
    };

    let spread_config = SpreadConfig {
        base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(),
        spread_type: spread_type_enum,
        leg_configs: rust_leg_configs,
        max_loss,
        target_profit,
        close_at_eod: false,
        leg_expiry_timestamps,
    };

    let backtest = SpreadBacktest::new(spread_config);
    let result = backtest.run(&ts, &underlying, &premiums, &entry_signals, &exit_signals);

    Ok(convert_result(result))
}

/// A single spread backtest item for batch execution.
#[pyclass]
#[derive(Clone)]
pub struct PyBatchSpreadItem {
    #[pyo3(get, set)]
    pub strategy_id: String,
    pub legs_premiums: Vec<Vec<f64>>,
    pub leg_configs: Vec<(String, f64, i32, usize)>,
    pub entries: Vec<bool>,
    pub exits: Vec<bool>,
    #[pyo3(get, set)]
    pub spread_type: String,
    #[pyo3(get, set)]
    pub max_loss: Option<f64>,
    #[pyo3(get, set)]
    pub target_profit: Option<f64>,
}

#[pymethods]
impl PyBatchSpreadItem {
    #[new]
    #[pyo3(signature = (strategy_id, legs_premiums, leg_configs, entries, exits,
        spread_type="custom", max_loss=None, target_profit=None))]
    fn new(
        strategy_id: String,
        legs_premiums: Vec<PyReadonlyArray1<f64>>,
        leg_configs: Vec<(String, f64, i32, usize)>,
        entries: PyReadonlyArray1<bool>,
        exits: PyReadonlyArray1<bool>,
        spread_type: &str,
        max_loss: Option<f64>,
        target_profit: Option<f64>,
    ) -> Self {
        Self {
            strategy_id,
            legs_premiums: legs_premiums.into_iter().map(numpy_to_vec_f64).collect(),
            leg_configs,
            entries: numpy_to_vec_bool(entries),
            exits: numpy_to_vec_bool(exits),
            spread_type: spread_type.to_string(),
            max_loss,
            target_profit,
        }
    }
}

/// Run multiple spread backtests in parallel via Rayon.
///
/// Shared data (timestamps, underlying_close) is converted once, then each
/// item is backtested on its own Rayon thread with the GIL released.
///
/// Returns a Vec of (strategy_id, PyBacktestResult) tuples.
#[pyfunction]
#[pyo3(signature = (timestamps, underlying_close, items, config=None))]
pub fn batch_spread_backtest(
    py: Python<'_>,
    timestamps: PyReadonlyArray1<i64>,
    underlying_close: PyReadonlyArray1<f64>,
    items: Vec<PyBatchSpreadItem>,
    config: Option<&PyBacktestConfig>,
) -> PyResult<Vec<(String, PyBacktestResult)>> {
    use rayon::prelude::*;

    // Convert shared data while holding GIL
    let ts = numpy_to_vec_i64(timestamps);
    let underlying = numpy_to_vec_f64(underlying_close);
    let base_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default();

    // Prepare each item into a self-contained struct for parallel execution
    struct PreparedItem {
        strategy_id: String,
        premiums: Vec<Vec<f64>>,
        entries: Vec<bool>,
        exits: Vec<bool>,
        spread_config: SpreadConfig,
    }

    let prepared: Vec<PreparedItem> = items
        .into_iter()
        .map(|item| {
            let rust_leg_configs: Vec<LegConfig> = item
                .leg_configs
                .into_iter()
                .map(|(opt_type, strike, quantity, lot_size)| {
                    let option_type =
                        SpreadOptionType::from_str(&opt_type).unwrap_or(SpreadOptionType::Call);
                    LegConfig::new(option_type, strike, quantity, lot_size)
                })
                .collect();

            let spread_type_enum = match item.spread_type.to_lowercase().as_str() {
                "straddle" => SpreadType::Straddle,
                "strangle" => SpreadType::Strangle,
                "vertical_call" | "verticalcall" => SpreadType::VerticalCall,
                "vertical_put" | "verticalput" => SpreadType::VerticalPut,
                "iron_condor" | "ironcondor" => SpreadType::IronCondor,
                "iron_butterfly" | "ironbutterfly" => SpreadType::IronButterfly,
                "butterfly_call" | "butterflycall" => SpreadType::ButterflyCall,
                "butterfly_put" | "butterflyput" => SpreadType::ButterflyPut,
                "calendar" => SpreadType::Calendar,
                "diagonal" => SpreadType::Diagonal,
                "long_call" | "longcall" => SpreadType::LongCall,
                "long_put" | "longput" => SpreadType::LongPut,
                "naked_call" | "nakedcall" => SpreadType::NakedCall,
                "naked_put" | "nakedput" => SpreadType::NakedPut,
                _ => SpreadType::Custom,
            };

            let spread_config = SpreadConfig {
                base: base_config.clone(),
                spread_type: spread_type_enum,
                leg_configs: rust_leg_configs.clone(),
                max_loss: item.max_loss,
                target_profit: item.target_profit,
                close_at_eod: false,
                leg_expiry_timestamps: None,
            };

            PreparedItem {
                strategy_id: item.strategy_id,
                premiums: item.legs_premiums,
                entries: item.entries,
                exits: item.exits,
                spread_config,
            }
        })
        .collect();

    // Release GIL and run all backtests in parallel via Rayon
    let results: Vec<(String, crate::core::types::BacktestResult)> = py.allow_threads(|| {
        prepared
            .into_par_iter()
            .map(|item| {
                let backtest = SpreadBacktest::new(item.spread_config);
                let result =
                    backtest.run(&ts, &underlying, &item.premiums, &item.entries, &item.exits);
                (item.strategy_id, result)
            })
            .collect()
    });

    // Re-acquire GIL and convert results to Python objects
    Ok(results.into_iter().map(|(id, result)| (id, convert_result(result))).collect())
}

/// Run multi-strategy backtest.
#[pyfunction]
#[pyo3(signature = (timestamps, open, high, low, close, volume, strategies, config=None, combine_mode="any"))]
pub fn run_multi_backtest<'py>(
    _py: Python<'py>,
    timestamps: PyReadonlyArray1<i64>,
    open: PyReadonlyArray1<f64>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    strategies: Vec<(PyReadonlyArray1<bool>, PyReadonlyArray1<bool>, i32, f64, String)>,
    config: Option<&PyBacktestConfig>,
    combine_mode: &str,
) -> PyResult<PyBacktestResult> {
    let ohlcv = OhlcvData {
        timestamps: numpy_to_vec_i64(timestamps),
        open: numpy_to_vec_f64(open),
        high: numpy_to_vec_f64(high),
        low: numpy_to_vec_f64(low),
        close: numpy_to_vec_f64(close),
        volume: numpy_to_vec_f64(volume),
    };

    let rust_strategies: Vec<CompiledSignals> = strategies
        .into_iter()
        .map(|(entries, exits, dir, weight, symbol)| CompiledSignals {
            symbol,
            entries: numpy_to_vec_bool(entries),
            exits: numpy_to_vec_bool(exits),
            position_sizes: None,
            direction: Direction::from_int(dir).unwrap_or(Direction::Long),
            weight,
        })
        .collect();

    let mode = match combine_mode {
        "all" => CombineMode::All,
        "majority" => CombineMode::Majority,
        "independent" => CombineMode::Independent,
        "weighted" => CombineMode::Weighted,
        _ => CombineMode::Any,
    };

    let multi_config = MultiStrategyConfig {
        base: config.map(|c| BacktestConfig::from(c)).unwrap_or_default(),
        combine_mode: mode,
        ..Default::default()
    };

    let backtest = MultiStrategyBacktest::new(multi_config);
    let result = backtest.run(&ohlcv, &rust_strategies);

    Ok(convert_result(result))
}

/// Run tick-level backtest on a single instrument.
///
/// All arrays must be the same length N (one element per tick).
/// `buy_qty_delta` and `sell_qty_delta` must already be per-tick deltas —
/// pass the difference from the previous tick, not Zerodha's cumulative totals.
/// `entries` / `exits` are caller-computed boolean signal arrays.
///
/// Returns a `PyBacktestResult` with the same fields as `run_single_backtest`.
#[pyfunction]
#[pyo3(signature = (
    timestamps,
    ltp,
    bid,
    ask,
    buy_qty_delta,
    sell_qty_delta,
    oi,
    entries,
    exits,
    symbol = "TICK",
    initial_capital = 100_000.0,
    fees = 0.001,
    slippage = 0.0,
    stop_loss_pct = 5.0,
    take_profit_pct = 10.0,
    max_hold_seconds = 1800_u64,
    entry_cooldown_ticks = 10_usize,
    max_trades = 50_usize,
))]
pub fn run_tick_backtest<'py>(
    _py: Python<'py>,
    timestamps: PyReadonlyArray1<i64>,
    ltp: PyReadonlyArray1<f64>,
    bid: PyReadonlyArray1<f64>,
    ask: PyReadonlyArray1<f64>,
    buy_qty_delta: PyReadonlyArray1<f64>,
    sell_qty_delta: PyReadonlyArray1<f64>,
    oi: PyReadonlyArray1<f64>,
    entries: PyReadonlyArray1<bool>,
    exits: PyReadonlyArray1<bool>,
    symbol: &str,
    initial_capital: f64,
    fees: f64,
    slippage: f64,
    stop_loss_pct: f64,
    take_profit_pct: f64,
    max_hold_seconds: u64,
    entry_cooldown_ticks: usize,
    max_trades: usize,
) -> PyResult<PyBacktestResult> {
    let tick_data = crate::core::types::TickData {
        timestamps: numpy_to_vec_i64(timestamps),
        ltp: numpy_to_vec_f64(ltp),
        bid: numpy_to_vec_f64(bid),
        ask: numpy_to_vec_f64(ask),
        buy_qty_delta: numpy_to_vec_f64(buy_qty_delta),
        sell_qty_delta: numpy_to_vec_f64(sell_qty_delta),
        oi: numpy_to_vec_f64(oi),
    };

    let entry_signals = numpy_to_vec_bool(entries);
    let exit_signals = numpy_to_vec_bool(exits);

    let config = TickBacktestConfig {
        base: crate::core::types::BacktestConfig {
            initial_capital,
            fees,
            slippage,
            stop: crate::core::types::StopConfig::None,
            target: crate::core::types::TargetConfig::None,
            upon_bar_close: false,
        },
        stop_loss_pct,
        take_profit_pct,
        max_hold_seconds,
        entry_cooldown_ticks,
        max_trades,
    };

    let backtest = TickBacktest::new(config);
    let result = backtest.run(&tick_data, &entry_signals, &exit_signals, symbol);

    Ok(convert_result(result))
}

// ============================================================================
// Tick Signal Functions
// ============================================================================

/// Compute tick momentum entry signals from per-tick feature arrays.
///
/// All input arrays must have the same length N. Returns a bool array of length N
/// where True indicates a valid entry tick (all gates passed, not in cooldown).
///
/// Gates (each can be disabled by setting threshold to 0.0):
///   - spread_pct[i] <= spread_pct_max
///   - bsi_delta[i] >= bsi_min  (0.0 = disabled)
///   - |return_1m[i]| >= return_1m_min_abs  (0.0 = disabled; NaN always fails)
///   - cooldown_ticks between consecutive entries
///
/// return_direction: +1 for long (needs positive return_1m), -1 for short.
#[pyfunction]
#[pyo3(signature = (
    spread_pct,
    bsi_delta,
    return_1m,
    spread_pct_max = 5.0,
    bsi_min = 0.0,
    return_1m_min_abs = 0.0,
    return_direction = 1_i8,
    cooldown_ticks = 10_usize,
))]
pub fn compute_tick_entry_signals<'py>(
    py: Python<'py>,
    spread_pct: PyReadonlyArray1<f64>,
    bsi_delta: PyReadonlyArray1<f64>,
    return_1m: PyReadonlyArray1<f64>,
    spread_pct_max: f64,
    bsi_min: f64,
    return_1m_min_abs: f64,
    return_direction: i8,
    cooldown_ticks: usize,
) -> PyResult<&'py PyArray1<bool>> {
    let result = crate::signals::tick_signals::tick_momentum_entry(
        &numpy_to_vec_f64(spread_pct),
        &numpy_to_vec_f64(bsi_delta),
        &numpy_to_vec_f64(return_1m),
        spread_pct_max,
        bsi_min,
        return_1m_min_abs,
        return_direction,
        cooldown_ticks,
    );
    Ok(vec_to_numpy_bool(py, result))
}

/// Compute time-based exit signals (EOD / session-end).
///
/// Sets exit[i] = True for every tick with timestamp >= eod_exit_time_ns.
/// Set eod_exit_time_ns = 0 to disable (returns all False).
///
/// timestamps_ns: nanoseconds-since-epoch for each tick (int64 array).
#[pyfunction]
#[pyo3(signature = (timestamps_ns, eod_exit_time_ns = 0_i64))]
pub fn compute_tick_exit_signals<'py>(
    py: Python<'py>,
    timestamps_ns: PyReadonlyArray1<i64>,
    eod_exit_time_ns: i64,
) -> PyResult<&'py PyArray1<bool>> {
    let result = crate::signals::tick_signals::tick_momentum_exit(
        &numpy_to_vec_i64(timestamps_ns),
        eod_exit_time_ns,
    );
    Ok(vec_to_numpy_bool(py, result))
}

// ============================================================================
// Tick Feature Functions
// ============================================================================

/// Per-tick bid/ask spread as percentage of mid price.
/// Returns 0.0 where both bid and ask are zero.
#[pyfunction]
pub fn tick_spread_pct<'py>(
    py: Python<'py>,
    bid: PyReadonlyArray1<f64>,
    ask: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::spread_pct(&numpy_to_vec_f64(bid), &numpy_to_vec_f64(ask)),
    ))
}

/// Per-tick delta BSI from Zerodha cumulative session totals.
///
/// buy_qty_cumulative / sell_qty_cumulative must be the raw cumulative running sums
/// from Zerodha (NOT already-converted deltas). Returns [0, 1] per tick; 0.5 = neutral.
#[pyfunction]
pub fn buy_sell_imbalance_delta<'py>(
    py: Python<'py>,
    buy_qty_cumulative: PyReadonlyArray1<f64>,
    sell_qty_cumulative: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::buy_sell_imbalance_delta(
            &numpy_to_vec_f64(buy_qty_cumulative),
            &numpy_to_vec_f64(sell_qty_cumulative),
        ),
    ))
}

/// Per-tick lookback return over a time window.
///
/// timestamps_ns: nanoseconds-since-epoch for each tick.
/// Returns NaN for ticks without sufficient history.
#[pyfunction]
#[pyo3(signature = (timestamps_ns, ltp, window_seconds = 60.0))]
pub fn return_window<'py>(
    py: Python<'py>,
    timestamps_ns: PyReadonlyArray1<i64>,
    ltp: PyReadonlyArray1<f64>,
    window_seconds: f64,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::return_window(
            &numpy_to_vec_i64(timestamps_ns),
            &numpy_to_vec_f64(ltp),
            window_seconds,
        ),
    ))
}

/// Rolling realized volatility proxy: stddev of log-returns over a time window (as %).
/// Returns NaN for ticks without at least 2 data points in the window.
#[pyfunction]
#[pyo3(signature = (timestamps_ns, ltp, window_seconds = 300.0))]
pub fn realized_vol_rolling<'py>(
    py: Python<'py>,
    timestamps_ns: PyReadonlyArray1<i64>,
    ltp: PyReadonlyArray1<f64>,
    window_seconds: f64,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::realized_vol_rolling(
            &numpy_to_vec_i64(timestamps_ns),
            &numpy_to_vec_f64(ltp),
            window_seconds,
        ),
    ))
}

/// Per-tick OI position within the day's high/low range: [0, 100].
/// Returns NaN where oi_day_high <= oi_day_low.
#[pyfunction]
pub fn oi_position_pct<'py>(
    py: Python<'py>,
    oi: PyReadonlyArray1<f64>,
    oi_day_high: f64,
    oi_day_low: f64,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::oi_position_pct(
            &numpy_to_vec_f64(oi),
            oi_day_high,
            oi_day_low,
        ),
    ))
}

/// Rolling tick velocity: ticks per minute over the preceding window_seconds.
#[pyfunction]
#[pyo3(signature = (timestamps_ns, window_seconds = 60.0))]
pub fn tick_velocity<'py>(
    py: Python<'py>,
    timestamps_ns: PyReadonlyArray1<i64>,
    window_seconds: f64,
) -> PyResult<&'py PyArray1<f64>> {
    Ok(vec_to_numpy_f64(
        py,
        crate::indicators::tick_features::tick_velocity(
            &numpy_to_vec_i64(timestamps_ns),
            window_seconds,
        ),
    ))
}

// ============================================================================
// Indicator Functions
// ============================================================================

/// Simple Moving Average.
#[pyfunction]
pub fn sma<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::trend::sma(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Exponential Moving Average.
#[pyfunction]
pub fn ema<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::trend::ema(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Relative Strength Index.
#[pyfunction]
pub fn rsi<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::momentum::rsi(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// MACD indicator.
#[pyfunction]
#[pyo3(signature = (data, fast_period=12, slow_period=26, signal_period=9))]
pub fn macd<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    fast_period: usize,
    slow_period: usize,
    signal_period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::momentum::macd(&vec, fast_period, slow_period, signal_period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, result.macd_line),
        vec_to_numpy_f64(py, result.signal_line),
        vec_to_numpy_f64(py, result.histogram),
    ))
}

/// Stochastic oscillator.
#[pyfunction]
#[pyo3(signature = (high, low, close, k_period=14, d_period=3))]
pub fn stochastic<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    k_period: usize,
    d_period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::momentum::stochastic(&h, &l, &c, k_period, d_period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, result.k), vec_to_numpy_f64(py, result.d)))
}

/// Average True Range.
#[pyfunction]
pub fn atr<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::volatility::atr(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Bollinger Bands.
#[pyfunction]
#[pyo3(signature = (data, period=20, std_dev=2.0))]
pub fn bollinger_bands<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
    std_dev: f64,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::volatility::bollinger_bands(&vec, period, std_dev)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, result.upper),
        vec_to_numpy_f64(py, result.middle),
        vec_to_numpy_f64(py, result.lower),
    ))
}

/// Average Directional Index.
#[pyfunction]
pub fn adx<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::strength::adx(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Volume Weighted Average Price.
#[pyfunction]
pub fn vwap<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::volume::vwap(&h, &l, &c, &v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Supertrend indicator.
#[pyfunction]
#[pyo3(signature = (high, low, close, period=10, multiplier=3.0))]
pub fn supertrend<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
    multiplier: f64,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<i8>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::trend::supertrend(&h, &l, &c, period, multiplier)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;

    let direction_array = PyArray1::from_vec(py, result.direction);
    Ok((vec_to_numpy_f64(py, result.supertrend), direction_array))
}

/// Rolling minimum (Lowest Low Value).
#[pyfunction]
pub fn rolling_min<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::rolling::rolling_min(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Rolling maximum (Highest High Value).
#[pyfunction]
pub fn rolling_max<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::rolling::rolling_max(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

// ============================================================================
// Extended Indicator Functions (via ferro_ta_core)
// ============================================================================

/// Commodity Channel Index.
#[pyfunction]
pub fn cci<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::cci(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Williams %R.
#[pyfunction]
pub fn willr<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::willr(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Parabolic SAR.
#[pyfunction]
#[pyo3(signature = (high, low, acceleration=0.02, maximum=0.2))]
pub fn sar<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    acceleration: f64,
    maximum: f64,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::sar(&h, &l, acceleration, maximum)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Plus Directional Indicator (+DI).
#[pyfunction]
pub fn plus_di<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::plus_di(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Minus Directional Indicator (-DI).
#[pyfunction]
pub fn minus_di<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::minus_di(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// ADX with +DI and -DI.
#[pyfunction]
pub fn adx_all<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::adx_all(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, result.adx),
        vec_to_numpy_f64(py, result.plus_di),
        vec_to_numpy_f64(py, result.minus_di),
    ))
}

/// Average Directional Movement Rating.
#[pyfunction]
pub fn adxr<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::adxr(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Rate of Change.
#[pyfunction]
pub fn roc<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::roc(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Money Flow Index.
#[pyfunction]
pub fn mfi<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::ferro_bridge::mfi(&h, &l, &c, &v, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Weighted Moving Average.
#[pyfunction]
pub fn wma<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::wma(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Double Exponential Moving Average.
#[pyfunction]
pub fn dema<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::dema(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Triple Exponential Moving Average.
#[pyfunction]
pub fn tema<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::tema(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Kaufman Adaptive Moving Average.
#[pyfunction]
pub fn kama<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::kama(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Stochastic RSI.
#[pyfunction]
#[pyo3(signature = (data, timeperiod=14, fastk_period=5, fastd_period=3))]
pub fn stochrsi<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    timeperiod: usize,
    fastk_period: usize,
    fastd_period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let vec = numpy_to_vec_f64(data);
    let (fastk, fastd) = indicators::ferro_bridge::stochrsi(&vec, timeperiod, fastk_period, fastd_period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, fastk), vec_to_numpy_f64(py, fastd)))
}

/// Aroon indicator (up, down).
#[pyfunction]
pub fn aroon<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::aroon(&h, &l, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, result.up), vec_to_numpy_f64(py, result.down)))
}

/// TRIX indicator.
#[pyfunction]
pub fn trix<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::trix(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Normalized Average True Range.
#[pyfunction]
pub fn natr<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::natr(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// True Range.
#[pyfunction]
pub fn trange<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::trange(&h, &l, &c)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Standard Deviation.
#[pyfunction]
#[pyo3(signature = (data, period, nbdev=1.0))]
pub fn stddev<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
    nbdev: f64,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::stddev(&vec, period, nbdev)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Variance.
#[pyfunction]
#[pyo3(signature = (data, period, nbdev=1.0))]
pub fn var<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
    nbdev: f64,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::var(&vec, period, nbdev)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Linear Regression.
#[pyfunction]
pub fn linearreg<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::linearreg(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Linear Regression Slope.
#[pyfunction]
pub fn linearreg_slope<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::linearreg_slope(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Linear Regression Intercept.
#[pyfunction]
pub fn linearreg_intercept<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::linearreg_intercept(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Linear Regression Angle.
#[pyfunction]
pub fn linearreg_angle<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::linearreg_angle(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Time Series Forecast.
#[pyfunction]
pub fn tsf<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::tsf(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Beta.
#[pyfunction]
pub fn beta<'py>(
    py: Python<'py>,
    data0: PyReadonlyArray1<f64>,
    data1: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let v0 = numpy_to_vec_f64(data0);
    let v1 = numpy_to_vec_f64(data1);
    let result = indicators::ferro_bridge::beta(&v0, &v1, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Correlation.
#[pyfunction]
pub fn correl<'py>(
    py: Python<'py>,
    data0: PyReadonlyArray1<f64>,
    data1: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let v0 = numpy_to_vec_f64(data0);
    let v1 = numpy_to_vec_f64(data1);
    let result = indicators::ferro_bridge::correl(&v0, &v1, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Chaikin A/D Line.
#[pyfunction]
pub fn ad<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::ferro_bridge::ad(&h, &l, &c, &v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Chaikin A/D Oscillator.
#[pyfunction]
#[pyo3(signature = (high, low, close, volume, fastperiod=3, slowperiod=10))]
pub fn adosc<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    fastperiod: usize,
    slowperiod: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::ferro_bridge::adosc(&h, &l, &c, &v, fastperiod, slowperiod)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// On Balance Volume (ferro_ta_core version).
#[pyfunction]
pub fn obv<'py>(
    py: Python<'py>,
    close: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let c = numpy_to_vec_f64(close);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::ferro_bridge::obv(&c, &v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Momentum.
#[pyfunction]
pub fn mom<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::mom(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Percentage Price Oscillator.
#[pyfunction]
#[pyo3(signature = (data, fastperiod=12, slowperiod=26, signalperiod=9))]
pub fn ppo<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    fastperiod: usize,
    slowperiod: usize,
    signalperiod: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::ppo(&vec, fastperiod, slowperiod, signalperiod)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, result.ppo_line),
        vec_to_numpy_f64(py, result.signal_line),
        vec_to_numpy_f64(py, result.histogram),
    ))
}

/// Chande Momentum Oscillator.
#[pyfunction]
pub fn cmo<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::cmo(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Aroon Oscillator.
#[pyfunction]
pub fn aroonosc<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::aroonosc(&h, &l, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Balance of Power.
#[pyfunction]
pub fn bop<'py>(
    py: Python<'py>,
    open: PyReadonlyArray1<f64>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let o = numpy_to_vec_f64(open);
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::bop(&o, &h, &l, &c)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Ultimate Oscillator.
#[pyfunction]
#[pyo3(signature = (high, low, close, period1=7, period2=14, period3=28))]
pub fn ultosc<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period1: usize,
    period2: usize,
    period3: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::ultosc(&h, &l, &c, period1, period2, period3)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Typical Price.
#[pyfunction]
pub fn typprice<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::typprice(&h, &l, &c)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Median Price.
#[pyfunction]
pub fn medprice<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::medprice(&h, &l)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Average Price.
#[pyfunction]
pub fn avgprice<'py>(
    py: Python<'py>,
    open: PyReadonlyArray1<f64>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let o = numpy_to_vec_f64(open);
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::avgprice(&o, &h, &l, &c)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Weighted Close Price.
#[pyfunction]
pub fn wclprice<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::wclprice(&h, &l, &c)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Midpoint over period.
#[pyfunction]
pub fn midpoint<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::midpoint(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Midprice over period.
#[pyfunction]
pub fn midprice<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::midprice(&h, &l, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Triple Exponential Moving Average (T3).
#[pyfunction]
#[pyo3(signature = (data, period=5, vfactor=0.7))]
pub fn t3<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
    vfactor: f64,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::t3(&vec, period, vfactor)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Triangular Moving Average.
#[pyfunction]
pub fn trima<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::trima(&vec, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Absolute Price Oscillator.
#[pyfunction]
#[pyo3(signature = (data, fastperiod=12, slowperiod=26))]
pub fn apo<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    fastperiod: usize,
    slowperiod: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let vec = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::apo(&vec, fastperiod, slowperiod)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

// ============================================================================
// Extended indicators (P0 batch)
// ============================================================================

/// Volume-Weighted Moving Average.
#[pyfunction]
#[pyo3(signature = (data, volume, period=20))]
pub fn vwma<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    volume: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let c = numpy_to_vec_f64(data);
    let v = numpy_to_vec_f64(volume);
    let result = indicators::ferro_bridge::vwma(&c, &v, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Donchian Channels — returns (upper, middle, lower).
#[pyfunction]
pub fn donchian<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let result = indicators::ferro_bridge::donchian(&h, &l, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, result.upper),
        vec_to_numpy_f64(py, result.middle),
        vec_to_numpy_f64(py, result.lower),
    ))
}

/// Choppiness Index.
#[pyfunction]
#[pyo3(signature = (high, low, close, period=14))]
pub fn choppiness_index<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::choppiness_index(&h, &l, &c, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Hull Moving Average.
#[pyfunction]
pub fn hull_ma<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    period: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let v = numpy_to_vec_f64(data);
    let result = indicators::ferro_bridge::hull_ma(&v, period)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, result))
}

/// Chandelier Exit — returns (long_exit, short_exit).
#[pyfunction]
#[pyo3(signature = (high, low, close, period=22, multiplier=3.0))]
pub fn chandelier_exit<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    period: usize,
    multiplier: f64,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let result = indicators::ferro_bridge::chandelier_exit(&h, &l, &c, period, multiplier)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, result.long_exit), vec_to_numpy_f64(py, result.short_exit)))
}

/// Ichimoku Cloud — returns (tenkan, kijun, senkou_a, senkou_b, chikou).
#[pyfunction]
#[pyo3(signature = (high, low, close, tenkan_period=9, kijun_period=26, senkou_b_period=52, displacement=26))]
#[allow(clippy::too_many_arguments)]
pub fn ichimoku<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    tenkan_period: usize,
    kijun_period: usize,
    senkou_b_period: usize,
    displacement: usize,
) -> PyResult<(
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let r = indicators::ferro_bridge::ichimoku(
        &h, &l, &c, tenkan_period, kijun_period, senkou_b_period, displacement,
    )
    .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, r.tenkan),
        vec_to_numpy_f64(py, r.kijun),
        vec_to_numpy_f64(py, r.senkou_a),
        vec_to_numpy_f64(py, r.senkou_b),
        vec_to_numpy_f64(py, r.chikou),
    ))
}

/// Pivot Points — method: "classic" | "fibonacci" | "camarilla".
///
/// Returns (pivot, r1, s1, r2, s2).
#[pyfunction]
pub fn pivot_points<'py>(
    py: Python<'py>,
    high: PyReadonlyArray1<f64>,
    low: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    method: &str,
) -> PyResult<(
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
    &'py PyArray1<f64>,
)> {
    let h = numpy_to_vec_f64(high);
    let l = numpy_to_vec_f64(low);
    let c = numpy_to_vec_f64(close);
    let r = indicators::ferro_bridge::pivot_points(&h, &l, &c, method)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((
        vec_to_numpy_f64(py, r.pivot),
        vec_to_numpy_f64(py, r.r1),
        vec_to_numpy_f64(py, r.s1),
        vec_to_numpy_f64(py, r.r2),
        vec_to_numpy_f64(py, r.s2),
    ))
}

// ============================================================================
// Hilbert Transform (cycle) indicators
// ============================================================================

/// Hilbert Transform — Instantaneous Trendline.
#[pyfunction]
pub fn ht_trendline<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_trendline(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Hilbert Transform — Dominant Cycle Period.
#[pyfunction]
pub fn ht_dcperiod<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_dcperiod(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Hilbert Transform — Dominant Cycle Phase (degrees).
#[pyfunction]
pub fn ht_dcphase<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_dcphase(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Hilbert Transform — Phasor Components (in_phase, quadrature).
#[pyfunction]
pub fn ht_phasor<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_phasor(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, r.in_phase), vec_to_numpy_f64(py, r.quadrature)))
}

/// Hilbert Transform — Sine Wave (sine, lead_sine).
#[pyfunction]
pub fn ht_sine<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<(&'py PyArray1<f64>, &'py PyArray1<f64>)> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_sine(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, r.sine), vec_to_numpy_f64(py, r.lead_sine)))
}

/// Hilbert Transform — Trend vs Cycle Mode.
///
/// Returns i32 numpy array: 1 = trend, 0 = cycle.
#[pyfunction]
pub fn ht_trendmode<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<i32>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::ht_trendmode(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(PyArray1::from_vec(py, r))
}

// ============================================================================
// Market regime detection
// ============================================================================

/// Trend/range regime from ADX. Returns i8: 1=trend, 0=range, -1=warmup.
#[pyfunction]
pub fn regime_adx<'py>(
    py: Python<'py>,
    adx: PyReadonlyArray1<f64>,
    threshold: f64,
) -> PyResult<&'py PyArray1<i8>> {
    let v = numpy_to_vec_f64(adx);
    let r = indicators::ferro_bridge::regime_adx(&v, threshold)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(PyArray1::from_vec(py, r))
}

/// Combined ADX + ATR-ratio regime. Returns i8: 1=trend, 0=range, -1=NaN.
#[pyfunction]
pub fn regime_combined<'py>(
    py: Python<'py>,
    adx: PyReadonlyArray1<f64>,
    atr: PyReadonlyArray1<f64>,
    close: PyReadonlyArray1<f64>,
    adx_threshold: f64,
    atr_pct_threshold: f64,
) -> PyResult<&'py PyArray1<i8>> {
    let a = numpy_to_vec_f64(adx);
    let t = numpy_to_vec_f64(atr);
    let c = numpy_to_vec_f64(close);
    let r = indicators::ferro_bridge::regime_combined(&a, &t, &c, adx_threshold, atr_pct_threshold)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(PyArray1::from_vec(py, r))
}

/// CUSUM-based structural break detection. Returns i8: 1 at break bars.
#[pyfunction]
pub fn detect_breaks_cusum<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    window: usize,
    threshold: f64,
    slack: f64,
) -> PyResult<&'py PyArray1<i8>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::detect_breaks_cusum(&v, window, threshold, slack)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(PyArray1::from_vec(py, r))
}

/// Rolling variance break. Returns i8: 1 at break bars.
#[pyfunction]
pub fn rolling_variance_break<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    short_window: usize,
    long_window: usize,
    threshold: f64,
) -> PyResult<&'py PyArray1<i8>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::rolling_variance_break(&v, short_window, long_window, threshold)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(PyArray1::from_vec(py, r))
}

// ============================================================================
// Portfolio / cross-series tools
// ============================================================================

/// Rolling beta of asset vs benchmark.
#[pyfunction]
pub fn rolling_beta<'py>(
    py: Python<'py>,
    asset: PyReadonlyArray1<f64>,
    benchmark: PyReadonlyArray1<f64>,
    window: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let a = numpy_to_vec_f64(asset);
    let b = numpy_to_vec_f64(benchmark);
    let r = indicators::ferro_bridge::rolling_beta(&a, &b, window)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Drawdown series from an equity curve. Returns (per_bar_dd, max_dd).
#[pyfunction]
pub fn drawdown_series<'py>(
    py: Python<'py>,
    equity: PyReadonlyArray1<f64>,
) -> PyResult<(&'py PyArray1<f64>, f64)> {
    let v = numpy_to_vec_f64(equity);
    let r = indicators::ferro_bridge::drawdown_series(&v)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok((vec_to_numpy_f64(py, r.series), r.max_drawdown))
}

/// Rolling z-score.
#[pyfunction]
pub fn zscore_series<'py>(
    py: Python<'py>,
    data: PyReadonlyArray1<f64>,
    window: usize,
) -> PyResult<&'py PyArray1<f64>> {
    let v = numpy_to_vec_f64(data);
    let r = indicators::ferro_bridge::zscore_series(&v, window)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Relative strength = asset - beta*benchmark (excess-return style).
#[pyfunction]
pub fn relative_strength<'py>(
    py: Python<'py>,
    asset_returns: PyReadonlyArray1<f64>,
    benchmark_returns: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let a = numpy_to_vec_f64(asset_returns);
    let b = numpy_to_vec_f64(benchmark_returns);
    let r = indicators::ferro_bridge::relative_strength(&a, &b)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Spread = a - hedge*b.
#[pyfunction]
pub fn spread<'py>(
    py: Python<'py>,
    a: PyReadonlyArray1<f64>,
    b: PyReadonlyArray1<f64>,
    hedge: f64,
) -> PyResult<&'py PyArray1<f64>> {
    let av = numpy_to_vec_f64(a);
    let bv = numpy_to_vec_f64(b);
    let r = indicators::ferro_bridge::spread(&av, &bv, hedge)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

/// Ratio = a/b element-wise.
#[pyfunction]
pub fn ratio<'py>(
    py: Python<'py>,
    a: PyReadonlyArray1<f64>,
    b: PyReadonlyArray1<f64>,
) -> PyResult<&'py PyArray1<f64>> {
    let av = numpy_to_vec_f64(a);
    let bv = numpy_to_vec_f64(b);
    let r = indicators::ferro_bridge::ratio(&av, &bv)
        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
    Ok(vec_to_numpy_f64(py, r))
}

// ============================================================================
// Helper Functions
// ============================================================================

// ============================================================================
// Monte Carlo Forward Simulation
// ============================================================================

/// Run Monte Carlo forward simulation for a portfolio.
///
/// Uses Geometric Brownian Motion with Cholesky-decomposed correlated random
/// draws, parallelized via Rayon.
///
/// # Arguments
/// * `returns` - List of per-strategy return arrays (N strategies)
/// * `weights` - Portfolio weight vector (length N, sums to 1)
/// * `correlation_matrix` - N x N correlation matrix (flattened row-major as 2D list)
/// * `initial_value` - Starting portfolio value
/// * `n_simulations` - Number of simulation paths (default: 10000)
/// * `horizon_days` - Forward simulation horizon in trading days (default: 252)
/// * `seed` - Random seed for reproducibility (default: 42)
#[pyfunction]
#[pyo3(signature = (returns, weights, correlation_matrix, initial_value, n_simulations=10000, horizon_days=252, seed=42))]
pub fn simulate_portfolio_mc(
    py: Python<'_>,
    returns: Vec<PyReadonlyArray1<'_, f64>>,
    weights: PyReadonlyArray1<'_, f64>,
    correlation_matrix: Vec<PyReadonlyArray1<'_, f64>>,
    initial_value: f64,
    n_simulations: usize,
    horizon_days: usize,
    seed: u64,
) -> PyResult<PyObject> {
    use crate::portfolio::monte_carlo::{simulate_portfolio_forward, MonteCarloConfig};

    // Convert numpy arrays to Rust vecs
    let rust_returns: Vec<Vec<f64>> =
        returns.iter().map(|arr| arr.as_slice().unwrap().to_vec()).collect();

    let rust_weights: Vec<f64> = weights.as_slice().unwrap().to_vec();

    let rust_corr: Vec<Vec<f64>> =
        correlation_matrix.iter().map(|arr| arr.as_slice().unwrap().to_vec()).collect();

    let config = MonteCarloConfig { n_simulations, horizon_days, seed };

    // Run simulation (releases GIL for Rayon parallelism)
    let result = py.allow_threads(|| {
        simulate_portfolio_forward(&rust_returns, &rust_weights, &rust_corr, initial_value, &config)
    });

    // Build Python dict result
    let dict = pyo3::types::PyDict::new(py);

    // percentile_paths: list of (percentile, list[float])
    let paths_list = pyo3::types::PyList::empty(py);
    for (pct, path) in &result.percentile_paths {
        let path_list = pyo3::types::PyList::new(py, path);
        let tuple = pyo3::types::PyTuple::new(py, &[pct.to_object(py), path_list.to_object(py)]);
        paths_list.append(tuple)?;
    }
    dict.set_item("percentile_paths", paths_list)?;

    // final_values as numpy array for efficiency
    let final_arr = PyArray1::from_vec(py, result.final_values);
    dict.set_item("final_values", final_arr)?;

    dict.set_item("expected_return", result.expected_return)?;
    dict.set_item("probability_of_loss", result.probability_of_loss)?;
    dict.set_item("var_95", result.var_95)?;
    dict.set_item("cvar_95", result.cvar_95)?;

    Ok(dict.into())
}

/// Convert Rust BacktestResult to Python PyBacktestResult.
fn convert_result(result: crate::core::types::BacktestResult) -> PyBacktestResult {
    let metrics = PyBacktestMetrics {
        total_return_pct: result.metrics.total_return_pct,
        sharpe_ratio: result.metrics.sharpe_ratio,
        sortino_ratio: result.metrics.sortino_ratio,
        calmar_ratio: result.metrics.calmar_ratio,
        omega_ratio: result.metrics.omega_ratio,
        max_drawdown_pct: result.metrics.max_drawdown_pct,
        max_drawdown_duration: result.metrics.max_drawdown_duration,
        win_rate_pct: result.metrics.win_rate_pct,
        profit_factor: result.metrics.profit_factor,
        expectancy: result.metrics.expectancy,
        sqn: result.metrics.sqn,
        total_trades: result.metrics.total_trades,
        total_closed_trades: result.metrics.total_closed_trades,
        total_open_trades: result.metrics.total_open_trades,
        open_trade_pnl: result.metrics.open_trade_pnl,
        winning_trades: result.metrics.winning_trades,
        losing_trades: result.metrics.losing_trades,
        start_value: result.metrics.start_value,
        end_value: result.metrics.end_value,
        total_fees_paid: result.metrics.total_fees_paid,
        best_trade_pct: result.metrics.best_trade_pct,
        worst_trade_pct: result.metrics.worst_trade_pct,
        avg_trade_return_pct: result.metrics.avg_trade_return_pct,
        avg_win_pct: result.metrics.avg_win_pct,
        avg_loss_pct: result.metrics.avg_loss_pct,
        avg_winning_duration: result.metrics.avg_winning_duration,
        avg_losing_duration: result.metrics.avg_losing_duration,
        max_consecutive_wins: result.metrics.max_consecutive_wins,
        max_consecutive_losses: result.metrics.max_consecutive_losses,
        avg_holding_period: result.metrics.avg_holding_period,
        exposure_pct: result.metrics.exposure_pct,
        payoff_ratio: result.metrics.payoff_ratio,
        recovery_factor: result.metrics.recovery_factor,
    };

    let trades: Vec<PyTrade> = result
        .trades
        .into_iter()
        .map(|t| PyTrade {
            id: t.id,
            symbol: t.symbol,
            entry_idx: t.entry_idx,
            exit_idx: t.exit_idx,
            entry_price: t.entry_price,
            exit_price: t.exit_price,
            size: t.size,
            direction: t.direction as i32,
            pnl: t.pnl,
            return_pct: t.return_pct,
            entry_time: t.entry_time,
            exit_time: t.exit_time,
            fees: t.fees,
            exit_reason: format!("{:?}", t.exit_reason),
        })
        .collect();

    PyBacktestResult {
        metrics,
        equity_curve: result.equity_curve,
        drawdown_curve: result.drawdown_curve,
        trades,
        returns: result.returns,
    }
}