//! # Portfolio Transformer for Ultra-Low Latency Portfolio Optimization
//!
//! A specialized transformer architecture designed for sub-50us portfolio optimization
//! in high-frequency trading. Unlike traditional time-series transformers, this model
//! operates directly on portfolio state vectors for optimal weight prediction.

use std::sync::Arc;

use ml_core::native_types::NativeDevice;
use ml_core::cuda_autograd::{GpuTensor, GpuLinear, GpuLayerNorm, GpuVarStore};
use chrono::{DateTime, Utc};
use cudarc::driver::CudaStream;
use serde::{Deserialize, Serialize};
use tracing::{debug, instrument, warn};

use crate::{MLError, MLResult};

/// Portfolio state representation for transformer input
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioState {
    pub weights: Vec<f64>,
    pub expected_returns: Vec<f64>,
    pub volatilities: Vec<f64>,
    pub correlations: Vec<f64>,
    pub market_regime: Vec<f64>,
    pub risk_metrics: Vec<f64>,
    pub confidence_scores: Vec<f64>,
    pub alpha_signals: Vec<f64>,
    pub timestamp: DateTime<Utc>,
}

/// Portfolio optimization result with performance metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioOptimizationResult {
    /// Optimal portfolio weights (sum to 1.0)
    pub optimal_weights: Vec<f64>,
    /// Expected portfolio return
    pub expected_return: f64,
    /// Portfolio risk (volatility)
    pub portfolio_risk: f64,
    /// Sharpe ratio
    pub sharpe_ratio: f64,
    /// Maximum drawdown estimate
    pub max_drawdown: f64,
    /// Optimization confidence score (0.0 to 1.0)
    pub optimization_confidence: f64,
    /// Inference latency in microseconds
    pub inference_latency_us: u64,
    /// Market regime detected
    pub market_regime: MarketRegime,
    /// Risk decomposition
    pub risk_decomposition: Vec<f64>,
}

/// Configuration for Portfolio Transformer model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortfolioTransformerConfig {
    /// Number of assets in the portfolio
    pub num_assets: usize,
    /// Model dimension
    pub model_dim: usize,
    /// Number of attention heads
    pub num_heads: usize,
    /// Number of transformer layers
    pub num_layers: usize,
    /// Dropout rate for regularization
    pub dropout_rate: f64,
    /// Maximum sequence length
    pub max_seq_length: usize,
    /// Risk tolerance factor
    pub risk_tolerance: f64,
    /// Transaction cost penalty
    pub transaction_cost: f64,
    /// Market regime adaptation enabled
    pub regime_adaptation: bool,
    /// `GPU` acceleration enabled
    pub use_gpu: bool,
}

impl Default for PortfolioTransformerConfig {
    fn default() -> Self {
        Self {
            num_assets: 100,
            model_dim: 256,
            num_heads: 8,
            num_layers: 4,
            dropout_rate: 0.1,
            max_seq_length: 256,
            risk_tolerance: 0.5,
            transaction_cost: 0.001,
            regime_adaptation: true,
            use_gpu: true,
        }
    }
}

impl PortfolioTransformerConfig {
    /// Create nano configuration for minimal latency
    pub fn nano() -> Self {
        Self {
            num_assets: 10,
            model_dim: 32,
            num_heads: 2,
            num_layers: 1,
            dropout_rate: 0.0,
            max_seq_length: 32,
            risk_tolerance: 0.5,
            transaction_cost: 0.001,
            regime_adaptation: false,
            use_gpu: true,
        }
    }

    /// Create micro configuration for small portfolios
    pub fn micro() -> Self {
        Self {
            num_assets: 20,
            model_dim: 64,
            num_heads: 4,
            num_layers: 2,
            dropout_rate: 0.05,
            max_seq_length: 64,
            risk_tolerance: 0.5,
            transaction_cost: 0.001,
            regime_adaptation: true,
            use_gpu: true,
        }
    }

    /// Create small configuration for moderate portfolios
    pub fn small() -> Self {
        Self {
            num_assets: 50,
            model_dim: 128,
            num_heads: 4,
            num_layers: 2,
            dropout_rate: 0.1,
            max_seq_length: 128,
            risk_tolerance: 0.5,
            transaction_cost: 0.001,
            regime_adaptation: true,
            use_gpu: true,
        }
    }
}

/// Portfolio Transformer implementation
#[allow(missing_debug_implementations)]
pub struct PortfolioTransformer {
    config: PortfolioTransformerConfig,
    device: NativeDevice,
    varmap: GpuVarStore,
    stream: Arc<CudaStream>,
    cublas: cudarc::cublas::CudaBlas,
    // Internal layers
    input_projection: GpuLinear,
    positional_encoding: GpuTensor,
    transformer_layers: Vec<TransformerLayer>,
    output_projection: GpuLinear,
    risk_head: GpuLinear,
    regime_classifier: GpuLinear,
}

/// Individual transformer layer
struct TransformerLayer {
    self_attention: MultiHeadAttention,
    feed_forward: FeedForward,
    norm1: GpuLayerNorm,
    norm2: GpuLayerNorm,
    #[allow(dead_code)]
    dropout: f64,
}

/// Multi-head attention mechanism
struct MultiHeadAttention {
    query: GpuLinear,
    key: GpuLinear,
    value: GpuLinear,
    output: GpuLinear,
    num_heads: usize,
    head_dim: usize,
    scale: f64,
}

/// Feed-forward network
struct FeedForward {
    linear1: GpuLinear,
    linear2: GpuLinear,
}

impl PortfolioTransformer {
    /// Create new Portfolio Transformer
    pub fn new(config: PortfolioTransformerConfig, device: NativeDevice) -> MLResult<Self> {
        let stream = match &device {
            NativeDevice::Cuda(ordinal) => {
                let ctx = cudarc::driver::CudaContext::new(*ordinal).map_err(|e| {
                    MLError::DeviceError(format!("CUDA context init failed: {e}"))
                })?;
                ctx.new_stream().map_err(|e| {
                    MLError::DeviceError(format!("CUDA stream init failed: {e}"))
                })?
            }
            NativeDevice::Cpu => {
                return Err(MLError::ConfigError(
                    "PortfolioTransformer requires CUDA".to_owned(),
                ));
            }
        };

        let mut varmap = GpuVarStore::new(Arc::clone(&stream));

        // Input projection
        let input_projection = varmap.linear(
            "input_projection",
            config.num_assets * 8, // 8 features per asset
            config.model_dim,
        )?;

        // Positional encoding
        let positional_encoding =
            Self::create_positional_encoding(config.max_seq_length, config.model_dim, &stream)?;

        // Transformer layers
        let mut transformer_layers = Vec::new();
        for i in 0..config.num_layers {
            let layer = TransformerLayer::new(
                &config,
                &mut varmap,
                &format!("layer_{i}"),
            )?;
            transformer_layers.push(layer);
        }

        // Output projections
        let output_projection = varmap.linear(
            "output_projection",
            config.model_dim,
            config.num_assets,
        )?;

        let risk_head = varmap.linear(
            "risk_head",
            config.model_dim,
            1,
        )?;

        let regime_classifier = varmap.linear(
            "regime_classifier",
            config.model_dim,
            4,
        )?;

        let cublas = cudarc::cublas::CudaBlas::new(Arc::clone(&stream)).map_err(|e| {
            MLError::DeviceError(format!("cuBLAS init failed: {e}"))
        })?;

        Ok(Self {
            config,
            device,
            varmap,
            stream,
            cublas,
            input_projection,
            positional_encoding,
            transformer_layers,
            output_projection,
            risk_head,
            regime_classifier,
        })
    }

    /// Create positional encoding tensor
    fn create_positional_encoding(
        seq_len: usize,
        model_dim: usize,
        stream: &Arc<CudaStream>,
    ) -> MLResult<GpuTensor> {
        let mut pe_data = vec![0.0_f32; seq_len * model_dim];

        for pos in 0..seq_len {
            for i in (0..model_dim).step_by(2) {
                let angle = pos as f32 / 10000.0_f32.powf(i as f32 / model_dim as f32);
                if let Some(slot) = pe_data.get_mut(pos * model_dim + i) {
                    *slot = angle.sin();
                }
                if i + 1 < model_dim {
                    if let Some(slot) = pe_data.get_mut(pos * model_dim + i + 1) {
                        *slot = angle.cos();
                    }
                }
            }
        }

        GpuTensor::from_host(&pe_data, vec![seq_len, model_dim], stream).map_err(Into::into)
    }

    /// Optimize portfolio weights using transformer
    #[instrument(skip(self, portfolio_state))]
    pub async fn optimize_portfolio(
        &self,
        portfolio_state: &PortfolioState,
    ) -> MLResult<PortfolioOptimizationResult> {
        let start_time = std::time::Instant::now();

        // Prepare input tensor
        let input_tensor = self.prepare_input_tensor(portfolio_state)?;

        // Forward pass through transformer
        let hidden_states = self.forward_pass(&input_tensor)?;

        // Generate portfolio weights
        let weights = self.generate_weights(&hidden_states)?;

        // Calculate risk metrics
        let risk_metrics = self.calculate_risk_metrics(&hidden_states, &weights)?;

        // Detect market regime
        let market_regime = self.detect_market_regime(&hidden_states)?;

        let inference_latency_us = start_time.elapsed().as_micros() as u64;

        // Log performance
        if inference_latency_us > 100 {
            warn!(
                "Portfolio optimization exceeded 100us latency: {}us",
                inference_latency_us
            );
        } else {
            debug!(
                "Portfolio optimization completed in {}us",
                inference_latency_us
            );
        }

        Ok(PortfolioOptimizationResult {
            optimal_weights: weights.clone(),
            expected_return: self.calculate_expected_return(&weights, portfolio_state),
            portfolio_risk: risk_metrics.0,
            sharpe_ratio: risk_metrics.1,
            max_drawdown: risk_metrics.2,
            optimization_confidence: 0.85, // Default confidence
            inference_latency_us,
            market_regime,
            risk_decomposition: self.calculate_risk_decomposition(&weights, portfolio_state),
        })
    }

    /// Prepare input tensor from portfolio state
    fn prepare_input_tensor(&self, portfolio_state: &PortfolioState) -> MLResult<GpuTensor> {
        let mut input_data = Vec::new();

        // Concatenate all features
        input_data.extend(&portfolio_state.weights);
        input_data.extend(&portfolio_state.expected_returns);
        input_data.extend(&portfolio_state.volatilities);
        input_data.extend(&portfolio_state.correlations);
        input_data.extend(&portfolio_state.market_regime);
        input_data.extend(&portfolio_state.risk_metrics);
        input_data.extend(&portfolio_state.confidence_scores);
        input_data.extend(&portfolio_state.alpha_signals);

        // Pad or truncate to expected size
        let expected_size = self.config.num_assets * 8;
        input_data.resize(expected_size, 0.0);

        // Convert to f32 for GPU
        let input_f32: Vec<f32> = input_data.iter().map(|&x| x as f32).collect();

        GpuTensor::from_host(&input_f32, vec![1, expected_size], &self.stream).map_err(|e| {
            MLError::TensorCreationError {
                operation: "prepare_input_tensor".to_owned(),
                reason: e.to_string(),
            }
        })
    }

    /// Forward pass through transformer layers
    fn forward_pass(&self, input: &GpuTensor) -> MLResult<GpuTensor> {
        // Input projection
        let (x, _acts) = self.input_projection.forward(
            input, &self.varmap, &self.cublas, &self.stream,
        )?;

        // For now, return the projected output directly.
        // Full transformer attention + residual connections require GPU kernels
        // for reshape/transpose/softmax that are not yet in GpuTensor.
        // The projection [1, model_dim] is sufficient for weight generation.
        Ok(x)
    }

    /// Generate normalized portfolio weights
    fn generate_weights(&self, hidden_states: &GpuTensor) -> MLResult<Vec<f64>> {
        // Output projection
        let (logits, _acts) = self.output_projection.forward(
            hidden_states, &self.varmap, &self.cublas, &self.stream,
        )?;

        // Download to host for softmax (small tensor: num_assets elements)
        let logits_host = logits.to_host(&self.stream)?;

        // CPU softmax (tiny tensor, not worth a GPU kernel)
        let max_val = logits_host.iter().copied().fold(f32::NEG_INFINITY, f32::max);
        let exp_vals: Vec<f32> = logits_host.iter().map(|&x| (x - max_val).exp()).collect();
        let sum_exp: f32 = exp_vals.iter().sum();
        let weights: Vec<f64> = exp_vals.iter().map(|&x| (x / sum_exp) as f64).collect();

        // Ensure we have the right number of weights
        let mut result = weights;
        result.resize(self.config.num_assets, 0.0);

        Ok(result)
    }

    /// Calculate risk metrics (volatility, Sharpe ratio, max drawdown)
    fn calculate_risk_metrics(
        &self,
        hidden_states: &GpuTensor,
        weights: &[f64],
    ) -> MLResult<(f64, f64, f64)> {
        // Risk head forward pass
        let (risk_tensor, _acts) = self.risk_head.forward(
            hidden_states, &self.varmap, &self.cublas, &self.stream,
        )?;
        let risk_host = risk_tensor.to_host(&self.stream)?;
        let risk_value = *risk_host
            .first()
            .ok_or_else(|| MLError::TensorCreationError {
                operation: "calculate_risk_metrics".to_owned(),
                reason: "Risk tensor has no elements".to_owned(),
            })? as f64;

        // Portfolio volatility (sigmoid to ensure positive)
        let portfolio_risk = 1.0 / (1.0 + (-risk_value).exp());

        // Simple Sharpe ratio calculation
        let expected_return = weights.iter().sum::<f64>() * 0.08;
        let sharpe_ratio = expected_return / portfolio_risk.max(0.01);

        // Max drawdown estimate
        let max_drawdown = portfolio_risk * 0.5;

        Ok((portfolio_risk, sharpe_ratio, max_drawdown))
    }

    /// Detect current market regime
    fn detect_market_regime(&self, hidden_states: &GpuTensor) -> MLResult<MarketRegime> {
        // Regime classifier forward pass
        let (regime_logits, _acts) = self.regime_classifier.forward(
            hidden_states, &self.varmap, &self.cublas, &self.stream,
        )?;

        // Download to host for argmax (4 elements)
        let probs_host = regime_logits.to_host(&self.stream)?;
        let max_idx = probs_host
            .iter()
            .enumerate()
            .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
            .map(|(i, _)| i)
            .unwrap_or(0);

        let regime = match max_idx {
            0 => MarketRegime::Bull,
            1 => MarketRegime::Bear,
            2 => MarketRegime::Sideways,
            _ => MarketRegime::Normal,
        };

        Ok(regime)
    }

    /// Calculate expected portfolio return
    fn calculate_expected_return(&self, weights: &[f64], portfolio_state: &PortfolioState) -> f64 {
        weights
            .iter()
            .zip(portfolio_state.expected_returns.iter())
            .map(|(w, r)| w * r)
            .sum()
    }

    /// Calculate risk decomposition by asset
    fn calculate_risk_decomposition(
        &self,
        weights: &[f64],
        portfolio_state: &PortfolioState,
    ) -> Vec<f64> {
        weights
            .iter()
            .zip(portfolio_state.volatilities.iter())
            .map(|(w, vol)| w * w * vol * vol)
            .collect()
    }
}

impl TransformerLayer {
    fn new(
        config: &PortfolioTransformerConfig,
        varmap: &mut GpuVarStore,
        prefix: &str,
    ) -> MLResult<Self> {
        let self_attention =
            MultiHeadAttention::new(config.model_dim, config.num_heads, varmap, prefix)?;

        let feed_forward = FeedForward::new(
            config.model_dim,
            config.model_dim * 4, // Standard FFN expansion factor
            varmap,
            prefix,
        )?;

        let norm1 = GpuLayerNorm::new(&format!("{prefix}.norm1"), config.model_dim, 1e-5, varmap)?;
        let norm2 = GpuLayerNorm::new(&format!("{prefix}.norm2"), config.model_dim, 1e-5, varmap)?;

        Ok(Self {
            self_attention,
            feed_forward,
            norm1,
            norm2,
            dropout: config.dropout_rate,
        })
    }
}

impl MultiHeadAttention {
    fn new(
        model_dim: usize,
        num_heads: usize,
        varmap: &mut GpuVarStore,
        prefix: &str,
    ) -> MLResult<Self> {
        if model_dim % num_heads != 0 {
            return Err(MLError::ConfigError(
                "model_dim must be divisible by num_heads".to_owned(),
            ));
        }

        let head_dim = model_dim / num_heads;
        let scale = 1.0 / (head_dim as f64).sqrt();

        let query = varmap.linear(&format!("{prefix}.self_attention.query"), model_dim, model_dim)?;
        let key = varmap.linear(&format!("{prefix}.self_attention.key"), model_dim, model_dim)?;
        let value = varmap.linear(&format!("{prefix}.self_attention.value"), model_dim, model_dim)?;
        let output = varmap.linear(&format!("{prefix}.self_attention.output"), model_dim, model_dim)?;

        Ok(Self {
            query,
            key,
            value,
            output,
            num_heads,
            head_dim,
            scale,
        })
    }
}

impl FeedForward {
    fn new(
        input_dim: usize,
        hidden_dim: usize,
        varmap: &mut GpuVarStore,
        prefix: &str,
    ) -> MLResult<Self> {
        let linear1 = varmap.linear(&format!("{prefix}.feed_forward.linear1"), input_dim, hidden_dim)?;
        let linear2 = varmap.linear(&format!("{prefix}.feed_forward.linear2"), hidden_dim, input_dim)?;

        Ok(Self {
            linear1,
            linear2,
        })
    }
}

// Import MarketRegime from external common crate
use ::common::MarketRegime;

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

    fn create_test_portfolio_state() -> PortfolioState {
        PortfolioState {
            weights: vec![0.25, 0.25, 0.25, 0.25],
            expected_returns: vec![0.08, 0.12, 0.06, 0.10],
            volatilities: vec![0.15, 0.25, 0.12, 0.18],
            correlations: vec![0.6, 0.3, 0.4, 0.7, 0.5, 0.2],
            market_regime: vec![1.0, 0.0, 0.0, 0.0],
            risk_metrics: vec![0.05, 0.08, 0.03, 0.15],
            confidence_scores: vec![0.8, 0.7, 0.9, 0.6],
            alpha_signals: vec![0.02, -0.01, 0.03, 0.01],
            timestamp: Utc::now(),
        }
    }

    fn create_test_portfolio_state_for_config(num_assets: usize) -> PortfolioState {
        PortfolioState {
            weights: vec![1.0 / num_assets as f64; num_assets],
            expected_returns: (0..num_assets).map(|i| 0.08 + (i as f64 * 0.01)).collect(),
            volatilities: (0..num_assets).map(|i| 0.15 + (i as f64 * 0.02)).collect(),
            correlations: vec![0.6; num_assets * num_assets],
            market_regime: vec![1.0, 0.0, 0.0, 0.0],
            risk_metrics: vec![0.05, 0.08, 0.03, 0.15],
            confidence_scores: (0..num_assets)
                .map(|i| 0.7 + (i as f64 * 0.05).min(0.3))
                .collect(),
            alpha_signals: (0..num_assets)
                .map(|i| 0.01 * (i as f64 - num_assets as f64 / 2.0) / num_assets as f64)
                .collect(),
            timestamp: Utc::now(),
        }
    }

    #[tokio::test]
    async fn test_portfolio_transformer_creation() -> Result<(), Box<dyn std::error::Error>> {
        let config = PortfolioTransformerConfig::nano();
        let device = NativeDevice::Cuda(0);

        let transformer = PortfolioTransformer::new(config, device);
        assert!(transformer.is_ok());
        Ok(())
    }

    #[tokio::test]
    async fn test_portfolio_optimization() -> MLResult<()> {
        let config = PortfolioTransformerConfig::nano();
        let device = NativeDevice::Cuda(0);

        let transformer = PortfolioTransformer::new(config, device)?;
        let portfolio_state = create_test_portfolio_state();

        let result = transformer.optimize_portfolio(&portfolio_state).await;
        assert!(result.is_ok());

        let optimization_result = result?;
        assert_eq!(optimization_result.optimal_weights.len(), 10);
        assert!(optimization_result.inference_latency_us > 0);
        assert!(optimization_result.optimization_confidence >= 0.0);
        assert!(optimization_result.optimization_confidence <= 1.0);
        Ok(())
    }

    #[tokio::test]
    async fn test_different_model_sizes() -> MLResult<()> {
        let configs = [
            PortfolioTransformerConfig::nano(),
            PortfolioTransformerConfig::micro(),
            PortfolioTransformerConfig::small(),
        ];

        let device = NativeDevice::Cuda(0);
        let portfolio_state = create_test_portfolio_state();

        for config in configs {
            let transformer = PortfolioTransformer::new(config, device)?;
            let result = transformer.optimize_portfolio(&portfolio_state).await;
            assert!(result.is_ok());
        }
        Ok(())
    }

    #[test]
    fn test_config_creation() -> Result<(), Box<dyn std::error::Error>> {
        let nano = PortfolioTransformerConfig::nano();
        assert_eq!(nano.num_assets, 10);
        assert_eq!(nano.model_dim, 32);

        let micro = PortfolioTransformerConfig::micro();
        assert_eq!(micro.num_assets, 20);
        assert_eq!(micro.model_dim, 64);

        let small = PortfolioTransformerConfig::small();
        assert_eq!(small.num_assets, 50);
        assert_eq!(small.model_dim, 128);
        Ok(())
    }

    #[test]
    fn test_portfolio_state_creation() -> Result<(), Box<dyn std::error::Error>> {
        let state = create_test_portfolio_state();
        assert_eq!(state.weights.len(), 4);
        assert_eq!(state.expected_returns.len(), 4);
        assert_eq!(state.volatilities.len(), 4);
        assert!(!state.confidence_scores.is_empty());
        Ok(())
    }

    #[tokio::test]
    async fn test_risk_parity_constraint() -> MLResult<()> {
        let config = PortfolioTransformerConfig::nano();
        let device = NativeDevice::Cuda(0);
        let transformer = PortfolioTransformer::new(config.clone(), device)?;

        let mut portfolio_state = create_test_portfolio_state_for_config(config.num_assets);
        portfolio_state.volatilities = (0..config.num_assets)
            .map(|i| 0.10 + (i as f64 * 0.02))
            .collect();

        let result = transformer.optimize_portfolio(&portfolio_state).await?;

        let risk_contributions: Vec<f64> = result
            .optimal_weights
            .iter()
            .zip(portfolio_state.volatilities.iter())
            .map(|(w, vol)| w * vol)
            .collect();

        for rc in &risk_contributions {
            assert!(*rc >= 0.0, "Risk contribution should be non-negative");
        }

        let mean_rc: f64 = risk_contributions.iter().sum::<f64>() / risk_contributions.len() as f64;
        let variance: f64 = risk_contributions
            .iter()
            .map(|rc| (rc - mean_rc).powi(2))
            .sum::<f64>()
            / risk_contributions.len() as f64;
        let std_dev = variance.sqrt();

        let coefficient_of_variation = std_dev / mean_rc.max(1e-10);
        assert!(
            coefficient_of_variation < 5.0,
            "Portfolio should show reasonable risk diversification, got CoV: {}",
            coefficient_of_variation
        );

        assert!(result.portfolio_risk > 0.0);
        assert!(result.portfolio_risk < 1.0);

        Ok(())
    }

    #[tokio::test]
    async fn test_transaction_cost_modeling() -> MLResult<()> {
        let config = PortfolioTransformerConfig::nano();
        let device = NativeDevice::Cuda(0);
        let transformer = PortfolioTransformer::new(config.clone(), device)?;

        let mut portfolio_state = create_test_portfolio_state_for_config(config.num_assets);
        let initial_weights = vec![1.0 / config.num_assets as f64; config.num_assets];
        portfolio_state.weights = initial_weights.clone();

        let result = transformer.optimize_portfolio(&portfolio_state).await?;

        let turnover: f64 = initial_weights
            .iter()
            .zip(result.optimal_weights.iter())
            .map(|(old_w, new_w)| (new_w - old_w).abs())
            .sum();

        assert!(
            turnover >= 0.0 && turnover <= 2.0,
            "Portfolio turnover should be in range [0, 2], got: {}",
            turnover
        );

        let transaction_cost_impact = turnover * config.transaction_cost;
        assert!(transaction_cost_impact >= 0.0);
        assert!(
            transaction_cost_impact < 0.1,
            "Transaction costs should be reasonable, got: {}",
            transaction_cost_impact
        );

        let gross_return = result.expected_return;
        let net_return = gross_return - transaction_cost_impact;

        if turnover > 0.0 {
            assert!(net_return < gross_return);
        }

        assert_eq!(config.transaction_cost, 0.001);

        Ok(())
    }
}
