In order to backtest a portfolio using SPO, we'll need:
forecast_returnsactual_returnsstart_date and end_dateFor TradingCost, we'll need:
actual_pricesforecast_volumeforecast_std_devhalf_spreadFor ShortHoldingCost: we'll need:
short_ratesIn order to make this example as easy as possible, we've prepared, and will use, actual returns and prices and forecast returns, volumes, standard deviations, spreads, and short rates from 2017 - 2018 for a universe of 319 stocks.
Set up modules and load data:
import pandas as pd
import investos as inv
from investos.portfolio.cost_model import *
from investos.portfolio.constraint_model import *
actual_returns = pd.read_parquet("https://investos.io/example_actual_returns.parquet")
forecast_returns = pd.read_parquet("https://investos.io/example_forecast_returns.parquet")
# For trading costs:
actual_prices = pd.read_parquet("https://investos.io/example_spo_actual_prices.parquet")
forecast_volume = pd.Series(
pd.read_csv("https://investos.io/example_spo_forecast_volume.csv", index_col="asset")
.squeeze(),
name="forecast_volume"
)
forecast_std_dev = pd.Series(
pd.read_csv("https://investos.io/example_spo_forecast_std_dev.csv", index_col="asset")
.squeeze(),
name="forecast_std_dev"
)
half_spread_percent = 2.5 / 10_000 # 2.5 bps
half_spread = pd.Series(index=forecast_returns.columns, data=half_spread_percent)
# For short holding costs:
short_cost_percent = 30 / 10_000 # 30 bps
trading_days_per_year = 252
short_rates = pd.Series(index=forecast_returns.columns, data=short_cost_percent/trading_days_per_year)
Run SPO:
strategy = inv.portfolio.strategy.SPO(
actual_returns = actual_returns,
forecast_returns = forecast_returns,
costs = [
ShortHoldingCost(short_rates=short_rates, exclude_assets=["cash"]),
TradingCost(
actual_prices=actual_prices,
forecast_volume=forecast_volume,
forecast_std_dev=forecast_std_dev,
half_spread=half_spread,
exclude_assets=["cash"],
),
],
constraints = [
MaxShortLeverageConstraint(limit=0.3),
MaxLongLeverageConstraint(limit=1.3),
MinWeightConstraint(),
MaxWeightConstraint(),
LongCashConstraint()
],
cash_column_name="cash"
)
portfolio = inv.portfolio.BacktestController(
strategy=strategy,
start_date='2017-01-01',
end_date='2018-01-01',
hooks = {
"after_trades": [
lambda backtest, t, u, h_next: print(".", end=''),
]
}
)
backtest_result = portfolio.generate_positions()
backtest_result.summary
When backtest_result.summary is executed, it will output summary backtest results:
# Initial timestamp 2017-01-03 00:00:00
# Final timestamp 2017-12-29 00:00:00
# Total portfolio return (%) 15.5%
# Annualized portfolio return (%) 15.75%
# Annualized excess portfolio return (%) 12.68%
# Annualized excess risk (%) 7.82%
# Information ratio (x) 1.62x
# Annualized risk over risk-free (%) 7.82%
# Sharpe ratio (x) 1.62x
# Max drawdown (%) 3.54%
# Annual turnover (x) 457.36x
# Portfolio hit rate (%) 53.6%
What a difference trading costs make vs our previous RankLongShort example!
If you have a charting library installed, like matplotlib, check out BaseResult for the many backtest_result metrics you can plot!
Next, let's explore the backtest performance reporting available to you through backtest_result (an instance of BaseResult): Analyzing Backtest Results.