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 && 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, &((&1 - mean_return) ** 2))
|> Enum.sum()
|> then(&(&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, &(&1 < 0))
downside_variance = if length(downside_returns) > 1 do
(Enum.map(downside_returns, &(&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, &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, &(&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, &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 && 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, &((&1 - mean_return) ** 2))
|> Enum.sum()
|> then(&(&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, &(&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 && aapl_performance.total_return > 0, do: "AAPL SMA", else: "None"),
best_portfolio: if(portfolio_performance && 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, & &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, &(&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, & &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.