Powered by AppSignal & Oban Pro

Backtest examples

examples/backtest_examples.livemd

Backtest examples

Untitled notebook

Comprehensive Backtesting with Quant Explorer

Mix.install([
  {:quant, path: "/Users/guillaume/perso/quant"},
  {:explorer, "~> 0.11"},
  {:kino, "~> 0.12"},
  {:decimal, "~> 2.0"},
  {:kino_vega_lite, "~> 0.1.13"},
  {:vega_lite, "~> 0.1.11"}
])

Introduction

This LiveBook provides a comprehensive guide to backtesting trading strategies using the Quant Explorer framework. We’ll explore:

  • Real Market Data: Fetch actual financial data using multiple providers
  • Strategy Development: Create and test various trading strategies
  • Advanced Backtesting: Run realistic simulations with costs and position sizing
  • Performance Analysis: Calculate risk-adjusted returns and metrics
  • Visualization: Interactive charts to analyze strategy performance
  • Portfolio Management: Multi-asset strategies and diversification

Module Availability Check

# Check if required modules are available
IO.puts("=== Module Availability Check ===")

modules_to_check = [
  Quant.Explorer,
  Quant.Strategy,
  Quant.Strategy.Backtest,
  Explorer.DataFrame,
  Explorer.Series
]

Enum.each(modules_to_check, fn module ->
  available = Code.ensure_loaded?(module)
  status = if available, do: "✓ Available", else: "✗ Not Available"
  IO.puts("#{module}: #{status}")
end)

# Try to call a simple function to verify it works
try do
  # Test if we can create a simple strategy
  test_strategy = %{type: :test, fast_period: 10, slow_period: 20}
  IO.puts("\n✓ Strategy creation works")
  IO.puts("Test strategy: #{inspect(test_strategy)}")
  
  # Test if Quant.Strategy functions are available and callable
  if Code.ensure_loaded?(Quant.Strategy) do
    IO.puts("\n=== Testing Quant.Strategy Functions ===")
    
    try do
      sma_strategy = Quant.Strategy.sma_crossover(fast_period: 5, slow_period: 10)
      IO.puts("✓ sma_crossover function works: #{inspect(sma_strategy)}")
    rescue
      error ->
        IO.puts("✗ sma_crossover function failed: #{inspect(error)}")
    end
    
    # Test if we can access the backtest function
    try do
      # Create a minimal test dataframe
      test_data = Explorer.DataFrame.new(%{
        "timestamp" => [~D[2024-01-01], ~D[2024-01-02], ~D[2024-01-03]],
        "close" => [100.0, 101.0, 102.0],
        "open" => [99.0, 100.0, 101.0],
        "high" => [101.0, 102.0, 103.0],
        "low" => [98.0, 99.0, 100.0],
        "volume" => [1000, 1100, 1200]
      })
      
      simple_strategy = %{type: :sma_crossover, fast_period: 2, slow_period: 3}
      
      # Try to generate signals
      case Quant.Strategy.generate_signals(test_data, simple_strategy) do
        {:ok, _signals} ->
          IO.puts("✓ generate_signals function works")
        {:error, reason} ->
          IO.puts("✗ generate_signals failed: #{inspect(reason)}")
      end
      
      # Try to run backtest
      case Quant.Strategy.backtest(test_data, simple_strategy, initial_capital: 1000.0) do
        {:ok, _results} ->
          IO.puts("✓ backtest function works")
        {:error, reason} ->
          IO.puts("✗ backtest failed: #{inspect(reason)}")
      end
      
    rescue
      error ->
        IO.puts("✗ Strategy testing failed: #{inspect(error)}")
    end
  end
  
rescue
  error ->
    IO.puts("\n✗ Strategy creation failed: #{inspect(error)}")
end

IO.puts("\nIf any modules show as 'Not Available', the project may not be properly compiled.")
IO.puts("Try running: mix deps.get && mix compile")

Setup and Configuration

# Configure the data providers and API keys (if available)
# For demonstration, we'll use Yahoo Finance (no API key required)

# Display available providers and their capabilities
providers_info = Quant.Explorer.providers()
IO.puts("=== Available Data Providers ===")
Enum.each(providers_info, fn {provider, info} ->
  IO.puts("#{provider}:")
  IO.puts("  Rate Limit: #{info.rate_limit} requests/minute")
  IO.puts("  API Key Required: #{info.api_key_configured}")
  IO.puts("  Standardized: #{info.standardized}")
  IO.puts("  Timezone: #{info.timezone}")
  IO.puts("")
end)

# Configuration for our backtests
config = %{
  default_provider: :yahoo_finance,
  initial_capital: 100_000.0,
  commission: 0.001,  # 0.1%
  slippage: 0.0005,   # 0.05%
  test_period: "2y"   # 2 years of data
}

IO.puts("Backtest Configuration:")
IO.inspect(config, pretty: true)

Data Fetching and Preparation

# Fetch real market data for our backtests
# We'll use multiple symbols to demonstrate portfolio strategies

symbols = ["AAPL", "MSFT", "GOOGL", "TSLA", "SPY"]

IO.puts("Fetching historical data for symbols: #{inspect(symbols)}")
IO.puts("This may take a moment due to rate limiting...")

# Fetch data for each symbol
market_data = Enum.reduce(symbols, %{}, fn symbol, acc ->
  case Quant.Explorer.history(symbol, 
    provider: config.default_provider,
    period: config.test_period,
    interval: "1d"
  ) do
    {:ok, df} ->
      IO.puts("✓ Fetched #{Explorer.DataFrame.n_rows(df)} days of data for #{symbol}")
      Map.put(acc, symbol, df)
    
    {:error, reason} ->
      IO.puts("✗ Failed to fetch data for #{symbol}: #{inspect(reason)}")
      acc
  end
end)

# Display summary of fetched data
IO.puts("\n=== Data Summary ===")
total_symbols = map_size(market_data)
IO.puts("Successfully fetched data for #{total_symbols} symbols")

if total_symbols > 0 do
  # Show sample data structure
  {first_symbol, first_df} = Enum.at(market_data, 0)
  IO.puts("\nSample data structure (#{first_symbol}):")
  IO.puts("Columns: #{inspect(Explorer.DataFrame.names(first_df))}")
  IO.puts("Date range: #{Explorer.DataFrame.n_rows(first_df)} days")
  
  # Display first few rows
  Explorer.DataFrame.head(first_df, 5)
else
  IO.puts("No data available for backtesting. Please check your internet connection.")
end

Simple Strategy Function Test

# Test basic strategy function before running full backtests
IO.puts("=== Simple Strategy Function Test ===")

if Map.has_key?(market_data, "AAPL") do
  test_data = market_data["AAPL"]
  IO.puts("Using AAPL data with #{Explorer.DataFrame.n_rows(test_data)} rows")
  
  # Test 1: Can we create a strategy?
  try do
    test_strategy = Quant.Strategy.sma_crossover(fast_period: 5, slow_period: 10)
    IO.puts("✓ Strategy creation successful")
    IO.puts("Strategy: #{inspect(test_strategy)}")
    
    # Test 2: Can we generate signals?
    case Quant.Strategy.generate_signals(test_data, test_strategy) do
      {:ok, signals_df} ->
        IO.puts("✓ Signal generation successful")
        IO.puts("Signals DataFrame has #{Explorer.DataFrame.n_rows(signals_df)} rows")
        IO.puts("Columns: #{inspect(Explorer.DataFrame.names(signals_df))}")
        
        # Test 3: Can we run a basic backtest?
        case Quant.Strategy.backtest(test_data, test_strategy, initial_capital: 10000.0) do
          {:ok, backtest_df} ->
            IO.puts("✓ Backtest successful")
            IO.puts("Backtest DataFrame has #{Explorer.DataFrame.n_rows(backtest_df)} rows")
            IO.puts("Columns: #{inspect(Explorer.DataFrame.names(backtest_df))}")
          {:error, reason} ->
            IO.puts("✗ Backtest failed: #{inspect(reason)}")
        end
        
      {:error, reason} ->
        IO.puts("✗ Signal generation failed: #{inspect(reason)}")
    end
    
  rescue
    error ->
      IO.puts("✗ Strategy function test failed: #{inspect(error)}")
      IO.puts("Stack trace: #{Exception.format_stacktrace(__STACKTRACE__)}")
  end
else
  IO.puts("No AAPL data available for testing")
end

Strategy Implementation and Testing

Let’s implement and test various trading strategies on our real market data.

1. Single-Asset Strategy: AAPL with SMA Crossover

# Test SMA Crossover strategy on Apple stock
{aapl_performance, aapl_backtest_results} = if Map.has_key?(market_data, "AAPL") do
  aapl_data = market_data["AAPL"]
  
  IO.puts("=== AAPL SMA Crossover Strategy Backtest ===")
  IO.puts("Strategy: 20-day SMA crossing above/below 50-day SMA")
  IO.puts("AAPL data shape: #{Explorer.DataFrame.n_rows(aapl_data)} rows, #{Explorer.DataFrame.n_columns(aapl_data)} columns")
  
  # Check if required modules are available
  strategy_module_available = Code.ensure_loaded?(Quant.Strategy)
  IO.puts("Quant.Strategy module available: #{strategy_module_available}")
  
  if strategy_module_available do
    # Create SMA crossover strategy
    try do
      sma_strategy = Quant.Strategy.sma_crossover(fast_period: 20, slow_period: 50)
      IO.puts("Strategy created successfully: #{inspect(sma_strategy)}")
      
      # Generate trading signals
      case Quant.Strategy.generate_signals(aapl_data, sma_strategy) do
        {:ok, signals_df} ->
          # Count signals
          signals = Explorer.DataFrame.pull(signals_df, "signal") |> Explorer.Series.to_list()
          signal_counts = Enum.frequencies(signals)
          
          IO.puts("Signal Analysis:")
          IO.puts("  Buy signals: #{Map.get(signal_counts, 1, 0)}")
          IO.puts("  Sell signals: #{Map.get(signal_counts, -1, 0)}")
          IO.puts("  Hold periods: #{Map.get(signal_counts, 0, 0)}")
          
          # Run backtest
          case Quant.Strategy.backtest(aapl_data, sma_strategy,
            initial_capital: config.initial_capital,
            commission: config.commission,
            slippage: config.slippage,
            position_size: :percent_capital
          ) do
            {:ok, backtest_results} ->
              # Extract performance metrics
              portfolio_values = Explorer.DataFrame.pull(backtest_results, "portfolio_value") 
                               |> Explorer.Series.to_list()
              
              initial_value = List.first(portfolio_values)
              final_value = List.last(portfolio_values)
              total_return = (final_value - initial_value) / initial_value
              
              # Get AAPL buy-and-hold comparison
              aapl_prices = Explorer.DataFrame.pull(aapl_data, "close") |> Explorer.Series.to_list()
              aapl_initial = List.first(aapl_prices)
              aapl_final = List.last(aapl_prices)
              aapl_return = (aapl_final - aapl_initial) / aapl_initial
              
              IO.puts("\nPerformance Results:")
              IO.puts("  Strategy Return: #{Float.round(total_return * 100, 2)}%")
              IO.puts("  Buy & Hold Return: #{Float.round(aapl_return * 100, 2)}%")
              IO.puts("  Outperformance: #{Float.round((total_return - aapl_return) * 100, 2)}%")
              IO.puts("  Final Portfolio Value: $#{Float.round(final_value, 2)}")
              
              # Calculate additional metrics
              days_traded = length(portfolio_values)
              annualized_return = (final_value / initial_value) ** (365.0 / days_traded) - 1
              
              IO.puts("  Annualized Return: #{Float.round(annualized_return * 100, 2)}%")
              IO.puts("  Trading Period: #{days_traded} days")
              
              # Store results for later comparison
              performance = %{
                symbol: "AAPL",
                strategy: "SMA Crossover (20/50)",
                total_return: total_return,
                annualized_return: annualized_return,
                final_value: final_value,
                outperformance: total_return - aapl_return
              }
              
              # Display sample of results
              display_columns = ["timestamp", "close", "signal", "portfolio_value", "position"]
              available_columns = Explorer.DataFrame.names(backtest_results)
              columns_to_show = Enum.filter(display_columns, &(&1 in available_columns))
              
              result_display = Explorer.DataFrame.head(backtest_results, 10) |>
                Explorer.DataFrame.select(columns_to_show)
              
              IO.inspect(result_display)
              
              {performance, backtest_results}
              
            {:error, reason} ->
              IO.puts("Backtest failed: #{inspect(reason)}")
              {nil, nil}
          end
          
        {:error, reason} ->
          IO.puts("Signal generation failed: #{inspect(reason)}")
          {nil, nil}
      end
      
    rescue
      error ->
        IO.puts("Strategy execution failed with error: #{inspect(error)}")
        {nil, nil}
    end
  else
    IO.puts("Quant.Strategy module not available - check if the project is properly compiled")
    {nil, nil}
  end
else
  IO.puts("AAPL data not available for backtesting")
  {nil, nil}
end

# Display results
{aapl_performance, aapl_backtest_results}
2. Multi-Strategy Comparison on MSFT
# Compare different strategies on Microsoft stock
msft_best_strategy = if Map.has_key?(market_data, "MSFT") do
  msft_data = market_data["MSFT"]
  
  IO.puts("=== MSFT Multi-Strategy Comparison ===")
  IO.puts("MSFT data shape: #{Explorer.DataFrame.n_rows(msft_data)} rows, #{Explorer.DataFrame.n_columns(msft_data)} columns")
  
  # Check if required modules are available
  if Code.ensure_loaded?(Quant.Strategy) do
    try do
      # Define strategies to test
      strategies_to_test = [
        {"SMA Crossover (10/30)", Quant.Strategy.sma_crossover(fast_period: 10, slow_period: 30)},
        {"SMA Crossover (20/50)", Quant.Strategy.sma_crossover(fast_period: 20, slow_period: 50)},
    {"EMA Crossover (12/26)", Quant.Strategy.ema_crossover(fast_period: 12, slow_period: 26)},
    {"RSI Mean Reversion", Quant.Strategy.rsi_threshold(period: 14, oversold: 30, overbought: 70)},
    {"MACD Momentum", Quant.Strategy.macd_crossover(fast_period: 12, slow_period: 26, signal_period: 9)}
  ]
  
  # Helper function to run backtest and extract key metrics
  extract_performance = fn {name, strategy} ->
    case Quant.Strategy.backtest(msft_data, strategy,
      initial_capital: config.initial_capital,
      commission: config.commission,
      slippage: config.slippage
    ) do
      {:ok, results} ->
        portfolio_values = Explorer.DataFrame.pull(results, "portfolio_value") |> Explorer.Series.to_list()
        initial_value = List.first(portfolio_values)
        final_value = List.last(portfolio_values)
        total_return = (final_value - initial_value) / initial_value
        
        # Calculate max drawdown
        running_max = Enum.scan(portfolio_values, initial_value, &max/2)
        drawdowns = Enum.zip_with(portfolio_values, running_max, fn val, peak -> 
          if peak > 0, do: (val - peak) / peak, else: 0.0
        end)
        max_drawdown = Enum.min(drawdowns, fn -> 0.0 end) |> abs()
        
        # Count trades from position changes
        positions = Explorer.DataFrame.pull(results, "position") |> Explorer.Series.to_list()
        trade_count = Enum.chunk_every(positions, 2, 1, :discard)
        |> Enum.count(fn [current, next] -> current != next end)
        
        {name, %{
          return: total_return,
          max_drawdown: max_drawdown,
          final_value: final_value,
          trade_count: trade_count,
          success: true
        }}
        
      {:error, reason} ->
        IO.puts("Strategy #{name} failed: #{inspect(reason)}")
        {name, %{return: 0.0, max_drawdown: 0.0, final_value: config.initial_capital, trade_count: 0, success: false}}
    end
  end
  
  # Run all strategies
  strategy_results = Enum.map(strategies_to_test, extract_performance)
  
  # Calculate buy-and-hold benchmark
  msft_prices = Explorer.DataFrame.pull(msft_data, "close") |> Explorer.Series.to_list()
  msft_return = (List.last(msft_prices) - List.first(msft_prices)) / List.first(msft_prices)
  
  IO.puts("\nStrategy Performance Comparison:")
  IO.puts("#{String.pad_trailing("Strategy", 25)} | #{String.pad_trailing("Return", 10)} | #{String.pad_trailing("Max DD", 10)} | #{String.pad_trailing("Trades", 8)} | Final Value")
  IO.puts(String.duplicate("-", 80))
  
  Enum.each(strategy_results, fn {name, metrics} ->
    if metrics.success do
      return_str = "#{Float.round(metrics.return * 100, 1)}%"
      dd_str = "#{Float.round(metrics.max_drawdown * 100, 1)}%"
      trades_str = "#{metrics.trade_count}"
      final_str = "$#{Float.round(metrics.final_value, 0)}"
      
      IO.puts("#{String.pad_trailing(name, 25)} | #{String.pad_trailing(return_str, 10)} | #{String.pad_trailing(dd_str, 10)} | #{String.pad_trailing(trades_str, 8)} | #{final_str}")
    end
  end)
  
  IO.puts("#{String.pad_trailing("Buy & Hold MSFT", 25)} | #{String.pad_trailing("#{Float.round(msft_return * 100, 1)}%", 10)} | #{String.pad_trailing("N/A", 10)} | #{String.pad_trailing("0", 8)} | $#{Float.round(config.initial_capital * (1 + msft_return), 0)}")
  
  # Find best performing strategy
  successful_results = Enum.filter(strategy_results, fn {_name, metrics} -> metrics.success end)
  if length(successful_results) > 0 do
    {best_name, best_metrics} = Enum.max_by(successful_results, fn {_name, metrics} -> metrics.return end)
    
    IO.puts("\n🏆 Best Strategy: #{best_name}")
    IO.puts("   Return: #{Float.round(best_metrics.return * 100, 2)}%")
    IO.puts("   Outperformance vs Buy & Hold: #{Float.round((best_metrics.return - msft_return) * 100, 2)}%")
    IO.puts("   Max Drawdown: #{Float.round(best_metrics.max_drawdown * 100, 2)}%")
    IO.puts("   Number of Trades: #{best_metrics.trade_count}")
    
    {best_name, best_metrics}
  else
    nil
  end
  
  rescue
    error ->
      IO.puts("MSFT strategy execution failed with error: #{inspect(error)}")
      nil
  end
  else
    IO.puts("Quant.Strategy module not available for MSFT strategies")
    nil
  end
else
  IO.puts("MSFT data not available for backtesting")
  nil
end

# Display result
msft_best_strategy

Multi-Strategy Performance Visualization

# Create a multi-strategy comparison chart
if Map.has_key?(market_data, "MSFT") && msft_best_strategy do
  # Get strategy results from the MSFT analysis
  msft_data = market_data["MSFT"]
  
  # Re-run the strategies to get full results for visualization
  strategies_to_test = [
    {"SMA Crossover (10/30)", Quant.Strategy.sma_crossover(fast_period: 10, slow_period: 30)},
    {"SMA Crossover (20/50)", Quant.Strategy.sma_crossover(fast_period: 20, slow_period: 50)},
    {"EMA Crossover (12/26)", Quant.Strategy.ema_crossover(fast_period: 12, slow_period: 26)},
    {"RSI Mean Reversion", Quant.Strategy.rsi_threshold(period: 14, oversold: 30, overbought: 70)},
    {"MACD Momentum", Quant.Strategy.macd_crossover(fast_period: 12, slow_period: 26, signal_period: 9)}
  ]
  
  # Configuration for backtesting
  config = %{
    initial_capital: 100_000,
    commission: 0.001,
    slippage: 0.0005
  }
  
  # Create time series data for all strategies
  time_series_data = 
    strategies_to_test
    |> Enum.flat_map(fn {name, strategy} ->
      case Quant.Strategy.backtest(msft_data, strategy,
        initial_capital: config.initial_capital,
        commission: config.commission,
        slippage: config.slippage
      ) do
        {:ok, results_df} ->
          # Extract time series for this strategy
          timestamps = Explorer.DataFrame.pull(results_df, "timestamp") |> Explorer.Series.to_list()
          portfolio_values = Explorer.DataFrame.pull(results_df, "portfolio_value") |> Explorer.Series.to_list()
          signals = Explorer.DataFrame.pull(results_df, "signal") |> Explorer.Series.to_list()
          
          # Create data points for this strategy
          Enum.zip([timestamps, portfolio_values, signals])
          |> Enum.with_index()
          |> Enum.map(fn {{timestamp, portfolio_val, signal}, index} ->
            %{
              "date" => timestamp,
              "value" => portfolio_val,
              "strategy" => name,
              "signal" => signal,
              "index" => index
            }
          end)
        {:error, _reason} ->
          []
      end
    end)
  
  # Add buy & hold data
  msft_prices = Explorer.DataFrame.pull(msft_data, "close") |> Explorer.Series.to_list()
  msft_timestamps = Explorer.DataFrame.pull(msft_data, "timestamp") |> Explorer.Series.to_list()
  initial_price = List.first(msft_prices)
  
  buy_hold_data = 
    Enum.zip([msft_timestamps, msft_prices])
    |> Enum.with_index()
    |> Enum.map(fn {{timestamp, price}, index} ->
      buy_hold_value = config.initial_capital * (price / initial_price)
      %{
        "date" => timestamp,
        "value" => buy_hold_value,
        "strategy" => "Buy & Hold",
        "signal" => 0,
        "index" => index
      }
    end)
  
  # Combine all strategy data
  all_time_series_data = time_series_data ++ buy_hold_data
  
  # Create signal data for markers (filter for actual buy/sell signals)
  signal_data = 
    all_time_series_data
    |> Enum.filter(fn row -> row["signal"] != 0 end)
    |> Enum.map(fn row ->
      signal_type = if row["signal"] == 1, do: "Buy", else: "Sell"
      Map.put(row, "signal_type", signal_type)
    end)
  
  # Create the main time series chart
  performance_lines =
    VegaLite.new()
    |> VegaLite.data_from_values(all_time_series_data)
    |> VegaLite.mark(:line, tooltip: true, point: false)
    |> VegaLite.encode_field(:x, "date", type: :temporal, title: "Date")
    |> VegaLite.encode_field(:y, "value", type: :quantitative, title: "Portfolio Value ($)")
    |> VegaLite.encode_field(:color, "strategy", type: :nominal,
        title: "Strategy",
        scale: [range: ["#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", "#8c564b"]])
    |> VegaLite.encode(:stroke_dash, 
        condition: [
          test: "datum.strategy == 'Buy & Hold'",
          value: [5, 5]
        ],
        value: [])
  
  # Create signal markers
  signal_markers =
    VegaLite.new()
    |> VegaLite.data_from_values(signal_data)
    |> VegaLite.mark(:point, size: 80, filled: true, tooltip: true)
    |> VegaLite.encode_field(:x, "date", type: :temporal)
    |> VegaLite.encode_field(:y, "value", type: :quantitative)
    |> VegaLite.encode(:color,
        condition: [
          test: "datum.signal == 1",
          value: "green"
        ],
        value: "red")
    |> VegaLite.encode(:shape,
        condition: [
          test: "datum.signal == 1", 
          value: "triangle-up"
        ],
        value: "triangle-down")
    |> VegaLite.encode(:opacity, value: 0.8)
  
  # Combine charts using layers
  multi_strategy_chart =
    VegaLite.new(width: 900, height: 500, title: "Multi-Strategy Performance with Trading Signals (MSFT)")
    |> VegaLite.layers([performance_lines, signal_markers])
  
  Kino.VegaLite.new(multi_strategy_chart)
end

Performance Summary Metrics:

if Map.has_key?(market_data, "MSFT") && msft_best_strategy do
  # Re-run strategies for metrics (since we need the results in this scope)
  msft_data = market_data["MSFT"]
  
  # Configuration for backtesting
  config = %{
    initial_capital: 100_000,
    commission: 0.001,
    slippage: 0.0005
  }
  
  strategies_to_test = [
    {"SMA Crossover (10/30)", Quant.Strategy.sma_crossover(fast_period: 10, slow_period: 30)},
    {"SMA Crossover (20/50)", Quant.Strategy.sma_crossover(fast_period: 20, slow_period: 50)},
    {"EMA Crossover (12/26)", Quant.Strategy.ema_crossover(fast_period: 12, slow_period: 26)},
    {"RSI Mean Reversion", Quant.Strategy.rsi_threshold(period: 14, oversold: 30, overbought: 70)},
    {"MACD Momentum", Quant.Strategy.macd_crossover(fast_period: 12, slow_period: 26, signal_period: 9)}
  ]
  
  # Helper function to run backtest and extract key metrics
  extract_performance = fn {name, strategy} ->
    case Quant.Strategy.backtest(msft_data, strategy,
      initial_capital: config.initial_capital,
      commission: config.commission,
      slippage: config.slippage
    ) do
      {:ok, results_df} ->
        # Extract metrics from the DataFrame
        portfolio_values = Explorer.DataFrame.pull(results_df, "portfolio_value") |> Explorer.Series.to_list()
        total_returns = Explorer.DataFrame.pull(results_df, "total_return") |> Explorer.Series.to_list()
        max_drawdowns = Explorer.DataFrame.pull(results_df, "max_drawdown") |> Explorer.Series.to_list()
        trade_counts = Explorer.DataFrame.pull(results_df, "trade_count") |> Explorer.Series.to_list()
        
        final_value = List.last(portfolio_values) || config.initial_capital
        total_return = List.last(total_returns) || 0.0
        max_drawdown = List.last(max_drawdowns) || 0.0
        trade_count = List.last(trade_counts) || 0
        
        {name, %{
          success: true,
          return: total_return,
          max_drawdown: max_drawdown,
          final_value: final_value,
          trade_count: trade_count
        }}
      {:error, _reason} ->
        {name, %{success: false}}
    end
  end
  
  # Prepare data for multi-strategy comparison
  chart_strategy_results = Enum.map(strategies_to_test, extract_performance)
  multi_strategy_data = 
    chart_strategy_results
    |> Enum.filter(fn {_name, metrics} -> metrics.success end)
    |> Enum.flat_map(fn {name, metrics} ->
      # Create data points for each strategy
      [
        %{
          "strategy" => name,
          "metric" => "Return (%)",
          "value" => metrics.return * 100
        },
        %{
          "strategy" => name,
          "metric" => "Max Drawdown (%)",
          "value" => abs(metrics.max_drawdown * 100)  # Make positive for visualization
        },
        %{
          "strategy" => name,
          "metric" => "Trade Count",
          "value" => metrics.trade_count
        }
      ]
    end)
  
  # Add buy & hold benchmark
  msft_prices = Explorer.DataFrame.pull(msft_data, "close") |> Explorer.Series.to_list()
  buy_hold_return = (List.last(msft_prices) - List.first(msft_prices)) / List.first(msft_prices) * 100
  
  benchmark_data = [
    %{
      "strategy" => "Buy & Hold",
      "metric" => "Return (%)",
      "value" => buy_hold_return
    },
    %{
      "strategy" => "Buy & Hold", 
      "metric" => "Max Drawdown (%)",
      "value" => 0  # Simplified for comparison
    },
    %{
      "strategy" => "Buy & Hold",
      "metric" => "Trade Count", 
      "value" => 1
    }
  ]
  
  all_strategies_data = multi_strategy_data ++ benchmark_data
  
  # Create strategy comparison chart
  strategy_comparison_chart =
    VegaLite.new(width: 800, height: 400, title: "Multi-Strategy Performance Comparison (MSFT)")
    |> VegaLite.data_from_values(all_strategies_data)
    |> VegaLite.mark(:bar, tooltip: true)
    |> VegaLite.encode_field(:x, "strategy", type: :nominal, title: "Strategy", 
        axis: [label_angle: -45])
    |> VegaLite.encode_field(:y, "value", type: :quantitative, title: "Value")
    |> VegaLite.encode_field(:color, "metric", type: :nominal,
        title: "Metric",
        scale: [range: ["#2ca02c", "#d62728", "#1f77b4"]])  # Green, Red, Blue
    |> VegaLite.encode_field(:column, "metric", type: :nominal, title: "Performance Metrics")
    |> VegaLite.resolve(:scale, y: :independent)
  
  Kino.VegaLite.new(strategy_comparison_chart)
else
  Kino.Markdown.new("*Multi-strategy performance metrics not available*")
end
else
  Kino.Markdown.new("*Multi-strategy comparison not available - MSFT data required*")
end
3. Portfolio Strategy: Multi-Asset Momentum
# Implement a simple portfolio strategy across multiple assets
portfolio_performance = if map_size(market_data) >= 3 do
  IO.puts("=== Multi-Asset Portfolio Strategy ===")
  
  # Select assets with available data
  available_symbols = Map.keys(market_data) |> Enum.take(3)
  IO.puts("Testing portfolio strategy on: #{inspect(available_symbols)}")
  
  # Simple momentum strategy: buy assets with strong recent performance
  portfolio_results = Enum.map(available_symbols, fn symbol ->
    asset_data = market_data[symbol]
    
    # Use a simple momentum strategy (SMA crossover)
    momentum_strategy = Quant.Strategy.sma_crossover(fast_period: 10, slow_period: 20)
    
    case Quant.Strategy.backtest(asset_data, momentum_strategy,
      initial_capital: config.initial_capital / length(available_symbols),  # Equal allocation
      commission: config.commission,
      slippage: config.slippage
    ) do
      {:ok, results} ->
        portfolio_values = Explorer.DataFrame.pull(results, "portfolio_value") |> Explorer.Series.to_list()
        final_value = List.last(portfolio_values)
        initial_value = List.first(portfolio_values)
        
        # Also calculate buy-and-hold for this asset for comparison
        asset_prices = Explorer.DataFrame.pull(asset_data, "close") |> Explorer.Series.to_list()
        asset_initial_price = List.first(asset_prices)
        asset_final_price = List.last(asset_prices)
        buy_hold_return = (asset_final_price - asset_initial_price) / asset_initial_price
        strategy_return = (final_value - initial_value) / initial_value
        
        {symbol, %{
          final_value: final_value,
          initial_value: initial_value,
          return: strategy_return,
          buy_hold_return: buy_hold_return,
          outperformance: strategy_return - buy_hold_return,
          success: true
        }}
        
      {:error, reason} ->
        IO.puts("Portfolio component #{symbol} failed: #{inspect(reason)}")
        initial_allocation = config.initial_capital / length(available_symbols)
        {symbol, %{
          final_value: initial_allocation,
          initial_value: initial_allocation,
          return: 0.0,
          buy_hold_return: 0.0,
          outperformance: 0.0,
          success: false
        }}
    end
  end)
  
  # Calculate total portfolio performance
  portfolio_final = Enum.reduce(portfolio_results, 0.0, fn {_symbol, metrics}, acc ->
    acc + metrics.final_value
  end)
  
  portfolio_initial = config.initial_capital
  portfolio_return = (portfolio_final - portfolio_initial) / portfolio_initial
  
  IO.puts("\nPortfolio Performance:")
  IO.puts("Individual Asset Performance:")
  
  Enum.each(portfolio_results, fn {symbol, metrics} ->
    if metrics.success do
      IO.puts("  #{symbol}:")
      IO.puts("    Strategy Return: #{Float.round(metrics.return * 100, 2)}% ($#{Float.round(metrics.final_value, 0)})")
      IO.puts("    Buy & Hold Return: #{Float.round(metrics.buy_hold_return * 100, 2)}%")
      IO.puts("    Outperformance: #{Float.round(metrics.outperformance * 100, 2)}%")
    else
      IO.puts("  #{symbol}: Failed")
    end
  end)
  
  IO.puts("\nPortfolio Summary:")
  IO.puts("  Total Return: #{Float.round(portfolio_return * 100, 2)}%")
  IO.puts("  Final Value: $#{Float.round(portfolio_final, 0)}")
  IO.puts("  Number of Assets: #{length(available_symbols)}")
  
  # Calculate equal-weight buy-and-hold benchmark from individual returns
  benchmark_returns = Enum.map(portfolio_results, fn {_symbol, metrics} ->
    if metrics.success, do: metrics.buy_hold_return, else: 0.0
  end)
  
  avg_benchmark_return = Enum.sum(benchmark_returns) / length(benchmark_returns)
  
  IO.puts("  Equal-Weight Buy & Hold: #{Float.round(avg_benchmark_return * 100, 2)}%")
  IO.puts("  Portfolio Outperformance: #{Float.round((portfolio_return - avg_benchmark_return) * 100, 2)}%")
  
  # Calculate why the strategy might be underperforming
  underperforming_assets = Enum.filter(portfolio_results, fn {_symbol, metrics} ->
    metrics.success and metrics.outperformance < 0
  end)
  
  if length(underperforming_assets) > 0 do
    IO.puts("\n⚠️  Assets where strategy underperformed buy-and-hold:")
    Enum.each(underperforming_assets, fn {symbol, metrics} ->
      IO.puts("    #{symbol}: #{Float.round(metrics.outperformance * 100, 2)}% underperformance")
    end)
  end
  
  %{
    return: portfolio_return,
    final_value: portfolio_final,
    benchmark_return: avg_benchmark_return,
    outperformance: portfolio_return - avg_benchmark_return,
    num_assets: length(available_symbols)
  }
else
  IO.puts("Insufficient data for portfolio strategy (need at least 3 assets)")
  nil
end

# Display result
portfolio_performance

Advanced Risk Analysis

# Perform detailed risk analysis on our best strategies
if aapl_performance &amp;&amp; msft_best_strategy do
  IO.puts("=== Advanced Risk Analysis ===")
  
  # Risk metrics helper function
  calculate_risk_metrics = fn portfolio_values ->
    # Calculate daily returns
    returns = Enum.zip(portfolio_values, tl(portfolio_values))
    |> Enum.map(fn {prev, curr} -> (curr - prev) / prev end)
    
    if length(returns) > 1 do
      # Basic statistics
      mean_return = Enum.sum(returns) / length(returns)
      
      # Variance and standard deviation
      variance = Enum.map(returns, &amp;((&amp;1 - mean_return) ** 2))
      |> Enum.sum()
      |> then(&amp;(&amp;1 / (length(returns) - 1)))
      std_dev = :math.sqrt(variance)
      
      # Sharpe ratio (assuming 0% risk-free rate)
      sharpe_ratio = if std_dev > 0, do: mean_return / std_dev, else: 0.0
      
      # Downside deviation (for Sortino ratio)
      downside_returns = Enum.filter(returns, &amp;(&amp;1 < 0))
      downside_variance = if length(downside_returns) > 1 do
        (Enum.map(downside_returns, &amp;(&amp;1 ** 2)) |> Enum.sum()) / (length(downside_returns) - 1)
      else
        0.0
      end
      downside_deviation = :math.sqrt(downside_variance)
      
      # Sortino ratio
      sortino_ratio = if downside_deviation > 0, do: mean_return / downside_deviation, else: 0.0
      
      # Value at Risk (95% confidence)
      sorted_returns = Enum.sort(returns)
      var_95_index = round(length(sorted_returns) * 0.05)
      var_95 = if var_95_index > 0, do: Enum.at(sorted_returns, var_95_index - 1), else: 0.0
      
      # Maximum drawdown
      running_max = Enum.scan(portfolio_values, 0, &amp;max/2)
      drawdowns = Enum.zip_with(portfolio_values, running_max, fn val, peak -> 
        if peak > 0, do: (val - peak) / peak, else: 0.0
      end)
      max_drawdown = Enum.min(drawdowns, fn -> 0.0 end) |> abs()
      
      # Win rate
      winning_days = Enum.count(returns, &amp;(&amp;1 > 0))
      win_rate = winning_days / length(returns)
      
      %{
        mean_daily_return: mean_return,
        daily_volatility: std_dev,
        annualized_volatility: std_dev * :math.sqrt(252),
        sharpe_ratio: sharpe_ratio,
        sortino_ratio: sortino_ratio,
        max_drawdown: max_drawdown,
        var_95: var_95,
        win_rate: win_rate,
        best_day: Enum.max(returns, fn -> 0.0 end),
        worst_day: Enum.min(returns, fn -> 0.0 end)
      }
    else
      %{error: "Insufficient data for risk analysis"}
    end
  end
  
  # Get portfolio values for analysis (using stored backtest results if available)
  aapl_risk_metrics = if aapl_backtest_results do
    aapl_portfolio_values = Explorer.DataFrame.pull(aapl_backtest_results, "portfolio_value") |> Explorer.Series.to_list()
    risk_metrics = calculate_risk_metrics.(aapl_portfolio_values)
    
    IO.puts("AAPL SMA Strategy Risk Metrics:")
    IO.puts("  Daily Volatility: #{Float.round(risk_metrics.daily_volatility * 100, 2)}%")
    IO.puts("  Annualized Volatility: #{Float.round(risk_metrics.annualized_volatility * 100, 2)}%")
    IO.puts("  Sharpe Ratio: #{Float.round(risk_metrics.sharpe_ratio, 3)}")
    IO.puts("  Sortino Ratio: #{Float.round(risk_metrics.sortino_ratio, 3)}")
    IO.puts("  Maximum Drawdown: #{Float.round(risk_metrics.max_drawdown * 100, 2)}%")
    IO.puts("  Value at Risk (95%): #{Float.round(risk_metrics.var_95 * 100, 2)}%")
    IO.puts("  Win Rate: #{Float.round(risk_metrics.win_rate * 100, 2)}%")
    IO.puts("  Best Single Day: #{Float.round(risk_metrics.best_day * 100, 2)}%")
    IO.puts("  Worst Single Day: #{Float.round(risk_metrics.worst_day * 100, 2)}%")
    
    risk_metrics
  else
    nil
  end
  
  # Risk-adjusted performance comparison
  IO.puts("\n=== Risk-Adjusted Performance Summary ===")
  
  strategies_summary = []
  
  strategies_summary = if aapl_performance do
    [
      %{
        name: "AAPL SMA Crossover",
        return: aapl_performance.total_return,
        volatility: if(aapl_risk_metrics, do: aapl_risk_metrics.annualized_volatility, else: 0.0),
        sharpe: if(aapl_risk_metrics, do: aapl_risk_metrics.sharpe_ratio, else: 0.0),
        max_dd: if(aapl_risk_metrics, do: aapl_risk_metrics.max_drawdown, else: 0.0)
      } | strategies_summary
    ]
  else
    strategies_summary
  end
  
  strategies_summary = if msft_best_strategy do
    {msft_name, msft_metrics} = msft_best_strategy
    [
      %{
        name: "MSFT #{msft_name}",
        return: msft_metrics.return,
        volatility: 0.0,  # Would need to recalculate
        sharpe: 0.0,      # Would need to recalculate
        max_dd: msft_metrics.max_drawdown
      } | strategies_summary
    ]
  else
    strategies_summary
  end
  
  strategies_summary = if portfolio_performance do
    [
      %{
        name: "Multi-Asset Portfolio",
        return: portfolio_performance.return,
        volatility: 0.0,  # Would need to recalculate
        sharpe: 0.0,      # Would need to recalculate
        max_dd: 0.0       # Would need to recalculate
      } | strategies_summary
    ]
  else
    strategies_summary
  end
  
  if length(strategies_summary) > 0 do
    IO.puts("#{String.pad_trailing("Strategy", 30)} | #{String.pad_trailing("Return", 10)} | #{String.pad_trailing("Volatility", 12)} | #{String.pad_trailing("Sharpe", 8)} | Max DD")
    IO.puts(String.duplicate("-", 75))
    
    Enum.each(strategies_summary, fn strategy ->
      return_str = "#{Float.round(strategy.return * 100, 1)}%"
      vol_str = if strategy.volatility > 0, do: "#{Float.round(strategy.volatility * 100, 1)}%", else: "N/A"
      sharpe_str = if strategy.sharpe != 0.0, do: "#{Float.round(strategy.sharpe, 2)}", else: "N/A"
      dd_str = if strategy.max_dd > 0, do: "#{Float.round(strategy.max_dd * 100, 1)}%", else: "N/A"
      
      IO.puts("#{String.pad_trailing(strategy.name, 30)} | #{String.pad_trailing(return_str, 10)} | #{String.pad_trailing(vol_str, 12)} | #{String.pad_trailing(sharpe_str, 8)} | #{dd_str}")
    end)
  else
    IO.puts("No strategy results available for risk analysis.")
    IO.puts("This may happen if:")
    IO.puts("  - Market data fetching failed")
    IO.puts("  - Strategy backtests encountered errors")
    IO.puts("  - No AAPL, MSFT, or portfolio data is available")
    IO.puts("\nCheck the Variable Status Check section above for more details.")
  end
else
  IO.puts("Insufficient strategy results for risk analysis")
end

Variable Status Check

# Debug: Check what variables are available
IO.puts("=== Variable Status Check ===")
IO.puts("aapl_performance: #{if aapl_performance, do: "✓ Available", else: "✗ Not available"}")
IO.puts("aapl_backtest_results: #{if aapl_backtest_results, do: "✓ Available", else: "✗ Not available"}")
IO.puts("msft_best_strategy: #{if msft_best_strategy, do: "✓ Available", else: "✗ Not available"}")
IO.puts("portfolio_performance: #{if portfolio_performance, do: "✓ Available", else: "✗ Not available"}")
IO.puts("market_data symbols: #{Map.keys(market_data) |> Enum.join(", ")}")

# If AAPL backtest results are available, show some basic info
if aapl_backtest_results do
  IO.puts("\nAAPL Backtest Results Info:")
  IO.puts("  Columns: #{Explorer.DataFrame.names(aapl_backtest_results) |> Enum.join(", ")}")
  IO.puts("  Rows: #{Explorer.DataFrame.n_rows(aapl_backtest_results)}")
else
  IO.puts("\nNo AAPL backtest results available for visualization")
end

Performance Visualization

# Create visualizations for our backtest results
alias VegaLite, as: Vl

# Check if we have the necessary data for visualization
# (aapl_backtest_results should be available from previous cells)
visualization_data_available = aapl_backtest_results != nil

if visualization_data_available do
  # Prepare data for visualization
  timestamps = Explorer.DataFrame.pull(aapl_backtest_results, "timestamp") |> Explorer.Series.to_list()
  portfolio_values = Explorer.DataFrame.pull(aapl_backtest_results, "portfolio_value") |> Explorer.Series.to_list()
  prices = Explorer.DataFrame.pull(aapl_backtest_results, "close") |> Explorer.Series.to_list()
  signals = Explorer.DataFrame.pull(aapl_backtest_results, "signal") |> Explorer.Series.to_list()

  initial_price = List.first(prices)
  initial_portfolio = List.first(portfolio_values)

  normalized_prices = Enum.map(prices, fn price ->
    initial_portfolio * (price / initial_price)
  end)

  chart_data = Enum.zip([timestamps, portfolio_values, normalized_prices, signals])
    |> Enum.with_index()
    |> Enum.map(fn {{timestamp, portfolio_val, buy_hold_val, signal}, index} ->
      %{
        "date" => timestamp,
        "strategy" => portfolio_val,
        "buy_hold" => buy_hold_val,
        "signal" => signal,
        "index" => index
      }
    end)

  # Prepare data for VegaLite with separate series for legend
  strategy_data = 
    chart_data
    |> Enum.map(fn row ->
      %{
        "date" => row["date"],
        "value" => row["strategy"],
        "series" => "Strategy",
        "signal" => row["signal"],
        "index" => row["index"]
      }
    end)
  
  buy_hold_data = 
    chart_data
    |> Enum.map(fn row ->
      %{
        "date" => row["date"],
        "value" => row["buy_hold"],
        "series" => "Buy & Hold",
        "signal" => row["signal"],
        "index" => row["index"]
      }
    end)
  
  # Combined data for the main chart
  combined_chart_data = strategy_data ++ buy_hold_data

  # Main performance chart with legend
  performance_chart =
    Vl.new(width: 800, height: 400, title: "AAPL Strategy vs Buy & Hold Performance")
    |> Vl.data_from_values(combined_chart_data)
    |> Vl.mark(:line, tooltip: true)
    |> Vl.encode_field(:x, "date", type: :temporal, title: "Date")
    |> Vl.encode_field(:y, "value", type: :quantitative, title: "Portfolio Value ($)")
    |> Vl.encode_field(:color, "series", type: :nominal, 
        title: "Performance",
        scale: [range: ["#1f77b4", "#ff7f0e"]], # Blue for Strategy, Orange for Buy & Hold
        legend: [title: "Performance Type"])

  buy_signals = Enum.filter(strategy_data, fn row -> row["signal"] == 1 end)
  sell_signals = Enum.filter(strategy_data, fn row -> row["signal"] == -1 end)

  buy_markers =
    Vl.new()
    |> Vl.data_from_values(buy_signals)
    |> Vl.mark(:point, size: 100, filled: true)
    |> Vl.encode_field(:x, "date", type: :temporal)
    |> Vl.encode_field(:y, "value", type: :quantitative)
    |> Vl.encode(:color, value: "green")
    |> Vl.encode(:shape, value: "triangle-up")

  sell_markers =
    Vl.new()
    |> Vl.data_from_values(sell_signals)
    |> Vl.mark(:point, size: 100, filled: true)
    |> Vl.encode_field(:x, "date", type: :temporal)
    |> Vl.encode_field(:y, "value", type: :quantitative)
    |> Vl.encode(:color, value: "red")
    |> Vl.encode(:shape, value: "triangle-down")

  combined_chart = 
    VegaLite.new()
    |> VegaLite.layers([performance_chart, buy_markers, sell_markers])

  Kino.VegaLite.new(combined_chart)
else
  Kino.Markdown.new("*Portfolio visualization not available - no AAPL backtest results*")
end

Advanced Position Sizing Analysis

# Test different position sizing methods to optimize risk-adjusted returns

if Map.has_key?(market_data, "SPY") do
  spy_data = market_data["SPY"]

  IO.puts("=== Position Sizing Impact Analysis (SPY) ===")

  base_strategy = Quant.Strategy.sma_crossover(fast_period: 20, slow_period: 50)

  position_sizing_methods = [
    {:percent_capital, "95% of Capital"},
    {{:fixed, 50000.0}, "Fixed $50,000"},
    {{:fixed, 25000.0}, "Fixed $25,000"},
    {30000.0, "Fixed $30,000 (numeric)"}
  ]

  position_results = Enum.map(position_sizing_methods, fn {method, description} ->
    case Quant.Strategy.backtest(spy_data, base_strategy,
      initial_capital: config.initial_capital,
      commission: config.commission,
      slippage: config.slippage,
      position_size: method
    ) do
      {:ok, results} ->
        portfolio_values = Explorer.DataFrame.pull(results, "portfolio_value") |> Explorer.Series.to_list()
        initial_value = List.first(portfolio_values)
        final_value = List.last(portfolio_values)
        total_return = (final_value - initial_value) / initial_value

        # Calculate max drawdown
        running_max = Enum.scan(portfolio_values, initial_value, &amp;max/2)
        drawdowns = Enum.zip_with(portfolio_values, running_max, fn val, peak -> 
          if peak > 0, do: (val - peak) / peak, else: 0.0
        end)
        max_drawdown = Enum.min(drawdowns, fn -> 0.0 end) |> abs()

        # Calculate return/risk ratio
        risk_adjusted_return = if max_drawdown > 0, do: total_return / max_drawdown, else: total_return

        {description, %{
          return: total_return,
          max_drawdown: max_drawdown,
          final_value: final_value,
          risk_adjusted: risk_adjusted_return,
          success: true
        }}
        
      {:error, reason} ->
        IO.puts("Position sizing test failed for #{description}: #{inspect(reason)}")
        {description, %{return: 0.0, max_drawdown: 0.0, final_value: config.initial_capital, risk_adjusted: 0.0, success: false}}
    end
  end)

  IO.puts("\nPosition Sizing Comparison:")
  IO.puts("#{String.pad_trailing("Method", 25)} | #{String.pad_trailing("Return", 10)} | #{String.pad_trailing("Max DD", 10)} | #{String.pad_trailing("Risk Adj", 10)} | Final Value")
  IO.puts(String.duplicate("-", 85))

  Enum.each(position_results, fn {description, metrics} ->
    if metrics.success do
      return_str = "#{Float.round(metrics.return * 100, 1)}%"
      dd_str = "#{Float.round(metrics.max_drawdown * 100, 1)}%"
      risk_str = "#{Float.round(metrics.risk_adjusted, 2)}"
      final_str = "$#{Float.round(metrics.final_value, 0)}"

      IO.puts("#{String.pad_trailing(description, 25)} | #{String.pad_trailing(return_str, 10)} | #{String.pad_trailing(dd_str, 10)} | #{String.pad_trailing(risk_str, 10)} | #{final_str}")
    end
  end)

  successful_position_tests = Enum.filter(position_results, fn {_desc, metrics} -> metrics.success end)
  if length(successful_position_tests) > 0 do
    {best_method, best_metrics} = Enum.max_by(successful_position_tests, fn {_desc, metrics} -> metrics.risk_adjusted end)

    IO.puts("\n🎯 Best Risk-Adjusted Method: #{best_method}")
    IO.puts("   Return: #{Float.round(best_metrics.return * 100, 2)}%")
    IO.puts("   Max Drawdown: #{Float.round(best_metrics.max_drawdown * 100, 2)}%")
    IO.puts("   Risk-Adjusted Score: #{Float.round(best_metrics.risk_adjusted, 3)}")
  end
else
  IO.puts("SPY data not available for position sizing analysis")
end

Monte Carlo Simulation

# Perform Monte Carlo simulation to assess strategy robustness

if aapl_performance &amp;&amp; aapl_backtest_results do
  IO.puts("=== Monte Carlo Simulation Analysis ===")

  portfolio_values = Explorer.DataFrame.pull(aapl_backtest_results, "portfolio_value") |> Explorer.Series.to_list()
  daily_returns = Enum.zip(portfolio_values, tl(portfolio_values))
  |> Enum.map(fn {prev, curr} -> (curr - prev) / prev end)

  if length(daily_returns) > 30 do
    # Calculate statistics for simulation
    mean_return = Enum.sum(daily_returns) / length(daily_returns)
    variance = Enum.map(daily_returns, &amp;((&amp;1 - mean_return) ** 2))
    |> Enum.sum()
    |> then(&amp;(&amp;1 / (length(daily_returns) - 1)))
    std_dev = :math.sqrt(variance)

    IO.puts("Historical Strategy Statistics:")
    IO.puts("  Mean Daily Return: #{Float.round(mean_return * 100, 3)}%")
    IO.puts("  Daily Volatility: #{Float.round(std_dev * 100, 2)}%")
    IO.puts("  Sample Size: #{length(daily_returns)} days")

    # Simple Monte Carlo simulation (bootstrap resampling)
    num_simulations = 1000
    simulation_days = 252  # 1 year
    initial_capital = config.initial_capital

    simulation_results = Enum.map(1..num_simulations, fn _sim ->
      # Resample daily returns
      simulated_returns = Enum.map(1..simulation_days, fn _day ->
        Enum.random(daily_returns)
      end)

      final_value = Enum.reduce(simulated_returns, initial_capital, fn return, portfolio ->
        portfolio * (1 + return)
      end)

      total_return = (final_value - initial_capital) / initial_capital
      total_return
    end)

## Analyze simulation results

sorted_results = Enum.sort(simulation_results)

## Percentiles

p5_index = round(length(sorted_results) * 0.05)
p25_index = round(length(sorted_results) * 0.25)
p50_index = round(length(sorted_results) * 0.50)
p75_index = round(length(sorted_results) * 0.75)
p95_index = round(length(sorted_results) * 0.95)

p5_return = Enum.at(sorted_results, max(0, p5_index - 1))
p25_return = Enum.at(sorted_results, max(0, p25_index - 1))
median_return = Enum.at(sorted_results, max(0, p50_index - 1))
p75_return = Enum.at(sorted_results, max(0, p75_index - 1))
p95_return = Enum.at(sorted_results, max(0, p95_index - 1))

mean_simulated = Enum.sum(simulation_results) / length(simulation_results)

## Probability of profit

profitable_sims = Enum.count(simulation_results, &amp;(&amp;1 > 0))
prob_profit = profitable_sims / length(simulation_results)

IO.puts("\nMonte Carlo Simulation Results (#{num_simulations} simulations, #{simulation_days} days):")
IO.puts("  Mean Return: #{Float.round(mean_simulated * 100, 2)}%")
IO.puts("  Median Return: #{Float.round(median_return * 100, 2)}%")
IO.puts("  5th Percentile: #{Float.round(p5_return * 100, 2)}%")
IO.puts("  25th Percentile: #{Float.round(p25_return * 100, 2)}%")
IO.puts("  75th Percentile: #{Float.round(p75_return * 100, 2)}%")
IO.puts("  95th Percentile: #{Float.round(p95_return * 100, 2)}%")
IO.puts("  Probability of Profit: #{Float.round(prob_profit * 100, 1)}%")

## Risk assessment

worst_case = Enum.min(simulation_results)
best_case = Enum.max(simulation_results)

IO.puts("\nRisk Assessment:")
IO.puts("  Best Case Scenario: #{Float.round(best_case * 100, 2)}%")
IO.puts("  Worst Case Scenario: #{Float.round(worst_case * 100, 2)}%")
IO.puts("  Expected Range (25th-75th): #{Float.round(p25_return * 100, 2)}% to #{Float.round(p75_return * 100, 2)}%")

## Value at Risk

var_5 = abs(p5_return) * 100
IO.puts("  Value at Risk (5%): #{Float.round(var_5, 2)}%")

    if prob_profit >= 0.6 do
      IO.puts("\n✅ Strategy shows robust positive performance across simulations")
    else
      IO.puts("\n⚠️  Strategy shows high uncertainty - consider risk management")
    end
  else
    IO.puts("Insufficient data for Monte Carlo simulation (need at least 30 daily returns)")
  end
else
  IO.puts("Monte Carlo simulation requires completed AAPL backtest results")
end

Summary and Recommendations

# Generate comprehensive summary and recommendations

IO.puts("=== Backtesting Summary and Recommendations ===")

summary_data = %{
  data_sources: map_size(market_data),
  total_backtests: (if aapl_performance, do: 1, else: 0) +
                   (if msft_best_strategy, do: 1, else: 0) + 
                   (if portfolio_performance, do: 1, else: 0),
  best_single_asset: if(aapl_performance &amp;&amp; aapl_performance.total_return > 0, do: "AAPL SMA", else: "None"),
  best_portfolio: if(portfolio_performance &amp;&amp; portfolio_performance.return > 0, do: "Multi-Asset", else: "None")
}

IO.puts("Testing Overview:")
IO.puts("  Data Sources: #{summary_data.data_sources} symbols")
IO.puts("  Total Strategies Tested: #{summary_data.total_backtests}")
IO.puts("  Test Period: #{config.test_period}")
IO.puts("  Initial Capital: $#{Float.round(config.initial_capital, 0)}")

IO.puts("\nKey Findings:")

## Best performing strategies

best_strategies = []

best_strategies = if aapl_performance do
  best_strategies ++ [
    %{name: "AAPL SMA Crossover", return: aapl_performance.total_return, type: "Single Asset"} | best_strategies
  ]
else
  best_strategies
end

best_strategies = if msft_best_strategy do
  {msft_name, msft_metrics} = msft_best_strategy
  best_strategies ++  [
    %{name: "MSFT #{msft_name}", return: msft_metrics.return, type: "Single Asset"} | best_strategies
  ]
else
  best_strategies
end

best_strategies = if portfolio_performance do
  best_strategies ++ [
    %{name: "Multi-Asset Portfolio", return: portfolio_performance.return, type: "Portfolio"} | best_strategies
  ]
else
  best_strategies
end

if length(best_strategies) > 0 do
  best_overall = Enum.max_by(best_strategies, &amp; &amp;1.return)
  IO.puts("  🏆 Best Overall Strategy: #{best_overall.name}")
  IO.puts("     Return: #{Float.round(best_overall.return * 100, 2)}%")
  IO.puts("     Type: #{best_overall.type}")

  profitable_strategies = Enum.filter(best_strategies, &amp;(&amp;1.return > 0))
  IO.puts("  📈 Profitable Strategies: #{length(profitable_strategies)}/#{length(best_strategies)}")
else
  IO.puts("  ⚠️  No completed strategy tests for analysis")
end

IO.puts("\nRecommendations:")

if length(best_strategies) > 0 do
  avg_return = Enum.sum(Enum.map(best_strategies, &amp; &amp;1.return)) / length(best_strategies)

  cond do
    avg_return > 0.15 ->
      IO.puts("  ✅ Strong performance detected - consider live implementation")
      IO.puts("  🎯 Focus on risk management and position sizing optimization")
      IO.puts("  📊 Implement real-time monitoring and adjustment mechanisms")

    avg_return > 0.05 ->
      IO.puts("  ⚖️ Moderate performance - suitable for conservative allocation")
      IO.puts("  🔍 Consider strategy refinement and additional indicators")
      IO.puts("  💰 Test with transaction cost sensitivity analysis")

    avg_return > 0 ->
      IO.puts("  ⚠️ Minimal outperformance - review strategy assumptions")
      IO.puts("  🔧 Consider parameter optimization and regime analysis")
      IO.puts("  📉 Compare against low-cost index fund alternatives")

    true ->
      IO.puts("  ❌ Poor performance - strategies need significant revision")
      IO.puts("  🔄 Consider contrarian approaches or different time horizons")
      IO.puts("  📚 Back to research phase - analyze market regime changes")
  end
else
  IO.puts("  🔧 Technical issues prevented comprehensive analysis")
  IO.puts("  🌐 Check data connectivity and provider availability")
  IO.puts("  🔍 Review error logs for systematic issues")
end

IO.puts("\nNext Steps:")
IO.puts("  1. 📋 Document successful strategies and parameters")
IO.puts("  2. 🧪 Test strategies on out-of-sample data")
IO.puts("  3. 🎚️ Implement dynamic parameter adjustment")
IO.puts("  4. 📱 Set up real-time monitoring and alerts")
IO.puts("  5. 💼 Consider portfolio allocation and diversification")
IO.puts("  6. 🔄 Schedule regular strategy review and rebalancing")

IO.puts("\n" <> String.duplicate("=", 60))
IO.puts("Backtesting Analysis Complete")
IO.puts("Framework: Quant Explorer v#{Mix.Project.config()[:version]}")
IO.puts("Timestamp: #{DateTime.utc_now() |> DateTime.to_string()}")
IO.puts(String.duplicate("=", 60))

Framework Features Demonstrated

This comprehensive backtesting LiveBook showcases the full capabilities of the Quant Explorer framework:

Real Market Data Integration

  • Multi-provider data fetching (Yahoo Finance, Alpha Vantage, etc.)
  • Automatic rate limiting and error handling
  • Standardized data schemas across all providers

Advanced Strategy Testing

  • Single-asset strategies (SMA, EMA, RSI, MACD)
  • Multi-asset portfolio strategies
  • Composite strategy combinations
  • Performance comparison frameworks

Realistic Backtesting Engine

  • Transaction cost modeling (commission + slippage)
  • Multiple position sizing methods
  • Risk management integration
  • Trade execution simulation

Comprehensive Risk Analysis

  • Volatility and drawdown analysis
  • Sharpe and Sortino ratio calculations
  • Value at Risk (VaR) measurements
  • Monte Carlo simulation support

Professional Visualizations

  • Interactive performance charts
  • Signal overlay visualization
  • Portfolio comparison plots
  • Risk metric dashboards

Production-Ready Features

  • Robust error handling and recovery
  • Performance optimization for large datasets
  • Configurable parameters and thresholds
  • Detailed logging and monitoring

This framework provides a complete solution for quantitative strategy development, from data acquisition through backtesting to production deployment.