In order to backtest a portfolio using SPO, we'll need:
forecast_returns
actual_returns
start_date
and end_date
For TradingCost, we'll need:
actual_prices
forecast_volume
forecast_std_dev
half_spread
For ShortHoldingCost: we'll need:
short_rates
In 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 = 40 / 10_000 # 40 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(limit=-0.03),
MaxWeightConstraint(limit=0.03),
LongCashConstraint(),
MaxAbsTurnoverConstraint(limit=0.05),
],
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 (%) 5.6%
# Annualized portfolio return (%) 5.68%
# Annualized excess portfolio return (%) 2.61%
# Annualized excess risk (%) 2.47%
# Information ratio (x) 1.06x
# Annualized risk over risk-free (%) 2.47%
# Sharpe ratio (x) 1.05x
# Max drawdown (%) 1.05%
# Annual turnover (x) 12.88x
# Portfolio hit rate (%) 60.0%
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.