# LiveBook Integration Guide
## Overview
[LiveBook](https://livebook.dev/) is Elixir's interactive and collaborative code notebook for data science, machine learning, and exploratory programming. This guide demonstrates how to integrate ExPostFacto with LiveBook to create interactive trading strategy backtesting and analysis workflows.
## Key Benefits
- **Interactive Development**: Test and refine trading strategies in real-time
- **Data Visualization**: Rich charts and graphs using VegaLite and Kino
- **Collaborative Analysis**: Share notebooks with team members
- **Rapid Prototyping**: Quick iteration on strategy ideas
- **Documentation**: Combine code, results, and explanations in one place
## Prerequisites
Before starting, ensure you have:
- Elixir 1.14+ installed
- LiveBook installed and running
- Basic understanding of Elixir and trading concepts
## Installation
### Option 1: Using LiveBook Desktop
1. Download and install [LiveBook Desktop](https://livebook.dev/)
2. Create a new notebook
3. Add ExPostFacto as a dependency in the setup section:
```elixir
Mix.install([
{:ex_post_facto, "~> 0.2.0"},
{:kino, "~> 0.12.0"},
{:kino_vega_lite, "~> 0.1.0"}
])
```
### Option 2: Using LiveBook Server
If running LiveBook as a server:
```bash
# Install LiveBook
mix escript.install hex livebook
# Start LiveBook
livebook server
```
Then add dependencies in your notebook as shown above.
## Quick Start Example
### Basic Backtesting in LiveBook
```elixir
# Cell 1: Setup and Dependencies
Mix.install([
{:ex_post_facto, "~> 0.2.0"},
{:kino, "~> 0.12.0"},
{:kino_vega_lite, "~> 0.1.0"}
])
alias VegaLite, as: Vl
```
```elixir
# Cell 2: Sample Data Generation
defmodule SampleData do
def generate_ohlc(days \\ 100, base_price \\ 100.0) do
Enum.reduce(1..days, [], fn day, acc ->
prev_close = if acc == [], do: base_price, else: hd(acc).close
# Generate realistic OHLC data with some randomness
open = prev_close + (:rand.uniform() - 0.5) * 2
close = open + (:rand.uniform() - 0.5) * 3
high = max(open, close) + :rand.uniform() * 2
low = min(open, close) - :rand.uniform() * 2
point = %{
open: Float.round(open, 2),
high: Float.round(high, 2),
low: Float.round(low, 2),
close: Float.round(close, 2),
volume: :rand.uniform(1000000) + 500000,
timestamp: Date.add(~D[2023-01-01], day - 1) |> Date.to_string()
}
[point | acc]
end) |> Enum.reverse()
end
end
# Generate 100 days of sample market data
market_data = SampleData.generate_ohlc(100)
IO.puts("Generated #{length(market_data)} data points")
IO.inspect(Enum.take(market_data, 3), label: "Sample data")
```
```elixir
# Cell 3: Simple Moving Average Strategy
defmodule SMAStrategy do
@doc "Simple Moving Average Crossover Strategy"
def call(%{close: price}, %{data_points: data_points, is_position_open: is_position_open}) do
# Get recent prices for moving averages
recent_prices = [price | Enum.map(data_points, & &1.datum.close)]
case length(recent_prices) do
len when len < 20 ->
:noop # Not enough data
_ ->
# Calculate 10-day and 20-day simple moving averages
sma_10 = recent_prices |> Enum.take(10) |> Enum.sum() |> Kernel./(10)
sma_20 = recent_prices |> Enum.take(20) |> Enum.sum() |> Kernel./(20)
cond do
!is_position_open && sma_10 > sma_20 -> :buy # Golden cross - buy signal
is_position_open && sma_10 < sma_20 -> :close_buy # Death cross - sell signal
true -> :noop
end
end
end
end
```
```elixir
# Cell 4: Run Backtest
{:ok, result} = ExPostFacto.backtest(
market_data,
{SMAStrategy, :call, []},
starting_balance: 100_000.0
)
# Display basic results
IO.puts("=== Backtest Results ===")
IO.puts("Starting Balance: $#{result.result.starting_balance}")
IO.puts("Final Balance: $#{result.result.final_balance}")
IO.puts("Total P&L: $#{result.result.total_profit_and_loss}")
IO.puts("Total Trades: #{length(result.result.trade_pairs)}")
# Get comprehensive statistics
stats = ExPostFacto.Result.comprehensive_summary(result.result)
IO.puts("Win Rate: #{Float.round(stats.win_rate_pct, 2)}%")
IO.puts("Sharpe Ratio: #{Float.round(stats.sharpe_ratio, 3)}")
```
## Advanced Visualization Examples
### Price Chart with Trade Signals
```elixir
# Cell 5: Create Interactive Price Chart
defmodule ChartHelpers do
def prepare_price_data(market_data) do
Enum.with_index(market_data, fn data, index ->
%{
"index" => index,
"date" => data.timestamp,
"open" => data.open,
"high" => data.high,
"low" => data.low,
"close" => data.close,
"volume" => data.volume
}
end)
end
def prepare_trade_data(trade_pairs, market_data) do
# Map trade pairs to chart points
indexed_data = Enum.with_index(market_data)
Enum.flat_map(trade_pairs, fn pair ->
entry_index = Enum.find_index(indexed_data, fn {data, _} ->
data.timestamp == pair.entry_timestamp
end)
exit_index = if pair.exit_timestamp do
Enum.find_index(indexed_data, fn {data, _} ->
data.timestamp == pair.exit_timestamp
end)
else
nil
end
signals = []
# Add entry signal
if entry_index do
signals = [%{
"index" => entry_index,
"price" => pair.entry_price,
"type" => "BUY",
"color" => "green"
} | signals]
end
# Add exit signal
if exit_index do
signals = [%{
"index" => exit_index,
"price" => pair.exit_price,
"type" => "SELL",
"color" => "red"
} | signals]
end
signals
end)
end
end
# Prepare data for visualization
price_data = ChartHelpers.prepare_price_data(market_data)
trade_signals = ChartHelpers.prepare_trade_data(result.result.trade_pairs, market_data)
# Create the price chart
price_chart =
Vl.new(width: 800, height: 400)
|> Vl.data_from_values(price_data)
|> Vl.mark(:line, color: "steelblue")
|> Vl.encode_field(:x, "index", type: :quantitative, title: "Time")
|> Vl.encode_field(:y, "close", type: :quantitative, title: "Price ($)")
|> Vl.resolve(:scale, y: :independent)
# Add trade signals as overlay
signal_chart =
Vl.new()
|> Vl.data_from_values(trade_signals)
|> Vl.mark(:circle, size: 100)
|> Vl.encode_field(:x, "index", type: :quantitative)
|> Vl.encode_field(:y, "price", type: :quantitative)
|> Vl.encode_field(:color, "color", type: :nominal, scale: [range: ["green", "red"]])
|> Vl.encode_field(:tooltip, ["type", "price"])
# Combine charts
final_chart = Vl.layer([price_chart, signal_chart])
Kino.VegaLite.new(final_chart)
```
### Performance Metrics Dashboard
```elixir
# Cell 6: Performance Dashboard
defmodule Dashboard do
def create_equity_curve(result, market_data) do
# Calculate running equity over time
equity_data =
result.result.data_points
|> Enum.with_index()
|> Enum.map(fn {point, index} ->
%{
"index" => index,
"equity" => point.running_balance,
"date" => Enum.at(market_data, index).timestamp
}
end)
Vl.new(width: 600, height: 300, title: "Equity Curve")
|> Vl.data_from_values(equity_data)
|> Vl.mark(:line, color: "green", stroke_width: 2)
|> Vl.encode_field(:x, "index", type: :quantitative, title: "Time")
|> Vl.encode_field(:y, "equity", type: :quantitative, title: "Portfolio Value ($)")
end
def create_trade_distribution(trade_pairs) do
trade_data =
trade_pairs
|> Enum.map(fn pair ->
pnl_pct = ((pair.exit_price - pair.entry_price) / pair.entry_price) * 100
%{
"pnl_percent" => Float.round(pnl_pct, 2),
"trade_type" => if pnl_pct > 0, do: "Winner", else: "Loser"
}
end)
Vl.new(width: 400, height: 300, title: "Trade P&L Distribution")
|> Vl.data_from_values(trade_data)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "pnl_percent", type: :quantitative, bin: true, title: "P&L (%)")
|> Vl.encode(:y, aggregate: :count, title: "Count")
|> Vl.encode_field(:color, "trade_type", type: :nominal,
scale: [domain: ["Winner", "Loser"], range: ["green", "red"]])
end
end
# Create equity curve
equity_chart = Dashboard.create_equity_curve(result, market_data)
Kino.VegaLite.new(equity_chart)
```
```elixir
# Cell 7: Trade Distribution Chart
trade_dist_chart = Dashboard.create_trade_distribution(result.result.trade_pairs)
Kino.VegaLite.new(trade_dist_chart)
```
## Interactive Strategy Development
### Parameter Optimization
```elixir
# Cell 8: Interactive Parameter Testing
defmodule ParameterOptimizer do
def test_sma_parameters(market_data, short_periods, long_periods) do
results = for short <- short_periods, long <- long_periods, short < long do
strategy = fn data, context ->
SMAStrategy.call_with_params(data, context, short, long)
end
{:ok, result} = ExPostFacto.backtest(
market_data,
{__MODULE__, :wrap_strategy, [strategy]},
starting_balance: 100_000.0
)
stats = ExPostFacto.Result.comprehensive_summary(result.result)
%{
short_period: short,
long_period: long,
total_return: stats.total_return_pct,
sharpe_ratio: stats.sharpe_ratio,
win_rate: stats.win_rate_pct,
max_drawdown: stats.max_drawdown_pct
}
end
# Find best performing combination
best = Enum.max_by(results, & &1.sharpe_ratio)
{results, best}
end
def wrap_strategy(data, context, strategy_fn) do
strategy_fn.(data, context)
end
end
# Test different parameter combinations
short_periods = [5, 10, 15]
long_periods = [20, 30, 50]
{param_results, best_params} = ParameterOptimizer.test_sma_parameters(
market_data,
short_periods,
long_periods
)
IO.puts("=== Parameter Optimization Results ===")
IO.puts("Best Parameters: #{best_params.short_period}/#{best_params.long_period}")
IO.puts("Sharpe Ratio: #{Float.round(best_params.sharpe_ratio, 3)}")
IO.puts("Total Return: #{Float.round(best_params.total_return, 2)}%")
IO.puts("Win Rate: #{Float.round(best_params.win_rate, 2)}%")
```
### Real-time Strategy Testing
```elixir
# Cell 9: Interactive Strategy Form
form =
Kino.Control.form([
short_ma: Kino.Control.number("Short MA Period", default: 10),
long_ma: Kino.Control.number("Long MA Period", default: 20),
initial_balance: Kino.Control.number("Starting Balance", default: 100_000)
], submit: "Run Backtest")
Kino.Control.stream(form)
|> Kino.listen(fn %{data: %{short_ma: short, long_ma: long, initial_balance: balance}} ->
if short < long do
# Create modified strategy with custom parameters
custom_strategy = fn data, context ->
SMAStrategy.call_with_params(data, context, short, long)
end
{:ok, result} = ExPostFacto.backtest(
market_data,
{ParameterOptimizer, :wrap_strategy, [custom_strategy]},
starting_balance: balance
)
stats = ExPostFacto.Result.comprehensive_summary(result.result)
IO.puts("\n=== Custom Strategy Results ===")
IO.puts("Parameters: #{short}/#{long} MA")
IO.puts("Starting Balance: $#{balance}")
IO.puts("Final Balance: $#{Float.round(result.result.final_balance, 2)}")
IO.puts("Total Return: #{Float.round(stats.total_return_pct, 2)}%")
IO.puts("Sharpe Ratio: #{Float.round(stats.sharpe_ratio, 3)}")
IO.puts("Win Rate: #{Float.round(stats.win_rate_pct, 2)}%")
IO.puts("Max Drawdown: #{Float.round(stats.max_drawdown_pct, 2)}%")
else
IO.puts("Error: Short MA period must be less than Long MA period")
end
end)
form
```
## Loading Real Market Data
### CSV Data Import
```elixir
# Cell 10: Load Real Market Data
file_input = Kino.Input.file("Upload CSV file with OHLC data")
```
```elixir
# Cell 11: Process Uploaded Data
file_data = Kino.Input.read(file_input)
real_market_data = if file_data do
content = file_data.file_ref |> Kino.Input.file_path() |> File.read!()
# Parse CSV data (assuming standard OHLC format)
lines = String.split(content, "\n", trim: true)
[_header | data_lines] = lines
Enum.map(data_lines, fn line ->
[date, open, high, low, close, volume] = String.split(line, ",")
%{
timestamp: String.trim(date),
open: String.to_float(String.trim(open)),
high: String.to_float(String.trim(high)),
low: String.to_float(String.trim(low)),
close: String.to_float(String.trim(close)),
volume: String.to_integer(String.trim(volume))
}
end)
else
IO.puts("No file uploaded, using sample data")
market_data
end
IO.puts("Loaded #{length(real_market_data)} data points")
IO.inspect(Enum.take(real_market_data, 3), label: "Sample real data")
```
## Best Practices
### 1. **Notebook Organization**
Structure your LiveBook notebooks with clear sections:
- **Setup**: Dependencies and imports
- **Data Loading**: Market data preparation
- **Strategy Definition**: Trading logic
- **Backtesting**: Running tests
- **Analysis**: Results and visualization
- **Optimization**: Parameter tuning
### 2. **Performance Considerations**
- Use `Enum.take/2` for large datasets in visualizations
- Cache expensive computations using `Kino.Process`
- Break complex analysis into smaller cells
### 3. **Data Management**
```elixir
# Use Kino.Process to cache large datasets
data_process = Kino.Process.start_link(fn ->
# Load and process your large dataset here
heavy_market_data = load_large_dataset()
heavy_market_data
end)
# Access cached data
cached_data = Kino.Process.get(data_process)
```
### 4. **Error Handling**
```elixir
# Always validate data before backtesting
case ExPostFacto.validate_data(market_data) do
:ok ->
{:ok, result} = ExPostFacto.backtest(market_data, strategy)
# Process results...
{:error, reason} ->
IO.puts("Data validation failed: #{reason}")
# Handle error...
end
```
## Common Use Cases
### 1. **Strategy Research and Development**
- Interactive strategy prototyping
- Parameter sensitivity analysis
- Comparative backtesting
### 2. **Educational and Training**
- Teaching trading concepts
- Demonstrating strategy performance
- Risk management education
### 3. **Team Collaboration**
- Sharing analysis notebooks
- Collaborative strategy development
- Results presentation
### 4. **Portfolio Analysis**
- Multi-strategy comparison
- Risk-adjusted performance metrics
- Correlation analysis
## Troubleshooting
### Common Issues
**1. Dependency Loading Errors**
```elixir
# If you get dependency errors, restart the runtime and try:
Mix.install([
{:ex_post_facto, "~> 0.2.0"},
{:kino, "~> 0.12.0"},
{:kino_vega_lite, "~> 0.1.0"}
], force: true)
```
**2. Memory Issues with Large Datasets**
```elixir
# Process data in chunks for large files
defmodule DataProcessor do
def process_in_chunks(data, chunk_size \\ 1000) do
data
|> Enum.chunk_every(chunk_size)
|> Enum.map(&process_chunk/1)
|> List.flatten()
end
defp process_chunk(chunk) do
# Process each chunk separately
chunk
end
end
```
**3. Visualization Performance**
```elixir
# Limit data points for charts
limited_data = Enum.take_every(large_dataset, 10) # Take every 10th point
```
## Additional Resources
- [LiveBook Documentation](https://livebook.dev/docs/)
- [VegaLite for Elixir](https://github.com/livebook-dev/vega_lite)
- [Kino Documentation](https://hexdocs.pm/kino/)
- [ExPostFacto Strategy API Guide](STRATEGY_API.md)
- [ExPostFacto Data Handling Examples](ENHANCED_DATA_HANDLING_EXAMPLES.md)
## Sample Notebooks
Complete example notebooks are available in the `notebooks/` directory:
- `basic_backtesting.livemd` - Introduction to backtesting in LiveBook
- `strategy_optimization.livemd` - Parameter optimization workflows
- `advanced_visualization.livemd` - Complex charting and analysis
- `real_data_analysis.livemd` - Working with real market data
Happy backtesting! 🚀📈