Market-Neutral Portfolio¶
Market (and dollar) neutral strategy on the NDX100 universe.
We use standard historical means and factor covariances to build a simple example of a market neutral strategy.
We use symbolic hyper-parameters (to be improved, see also
examples.risk_models
) to choose the values that maximize Sharpe ratio
of the back-test, for illustrative purposes only.
We use realistic values for transaction cost
and holding cost (stocks borrow fees) models. These are used both for
the optimization and the back-test in
cvxportfolio.MarketSimulator
.
To improve the Sharpe ratio of this kind of strategies, in practice, one could
use returns forecasts produced by some machine learning model. It is very easy
to plug such forecasts into this strategy, either by providing them as a
Dataframe or by coding the forecasting logic as a Cvxportfolio native
forecaster class, and passing either as argument to
cvxportfolio.ReturnsForecast
.
Note
Running this example may take some time, a few minutes on a modern workstation with a well-maintained Linux distribution, about half an hour (or longer!) on a laptop or a virtual machine in the cloud.
import numpy as np
import cvxportfolio as cvx
from .universes import NDX100 as UNIVERSE
# times
START = '2016-01-01'
END = None # today
# Currently (~2024) shorting large cap US stocks costs about this,
# in annualized percentages
BORROW_FEES = 0.25
# We set the bid-ask spreads at 5 basis points
SPREAD = 5E-4
# This is the b multiplier of the (3/2) power term in TransactionCost
MARKET_IMPACT = 1.
policy = cvx.SinglePeriodOptimization(
objective=cvx.ReturnsForecast()
- cvx.Gamma() * cvx.FactorModelCovariance(num_factors=10)
- cvx.Gamma() * cvx.TransactionCost(a=SPREAD/2, b=MARKET_IMPACT)
- cvx.Gamma() * cvx.HoldingCost(short_fees=BORROW_FEES),
constraints = [
cvx.DollarNeutral(), cvx.MarketNeutral(), cvx.LeverageLimit(7)],
# this solver is somewhat more robust than ECOS, but less efficient
solver='CLARABEL',
# this is a CVXPY compilation flag that disables a feature that is very
# useful (cache a semi-compiled problem) but its implementation scales
# badly with the problem size; if you increase number of factors or
# universe size, you may have to uncomment the next line
# ignore_dpp=True,
)
simulator = cvx.MarketSimulator(
universe=UNIVERSE,
costs = [
cvx.TransactionCost(a=SPREAD/2, b=MARKET_IMPACT),
cvx.HoldingCost(short_fees=BORROW_FEES)])
# automatic hyper-parameter optimization (by greedy grid search)
simulator.optimize_hyperparameters(
policy, start_time=START, end_time=END,
objective='sharpe_ratio')
print('Optimized policy hyper-parameters:')
print(policy)
# back-test the policy with optimized hyper-parameters
result = simulator.backtest(policy, start_time=START, end_time=END)
print("Optimized policy back-test result:")
print(result)
# plot
result_figure = result.plot()
# check that back-tested returns of the strategy are uncorrelated with the
# market benchmark
market_benchmark_returns = simulator.backtest(
cvx.MarketBenchmark(), start_time=START, end_time=END).returns
print('Correlation of strategy returns with benchmark:')
print(np.corrcoef(result.returns, market_benchmark_returns)[0, 1])
This is the output printed to screen when executing this script. You can see
the steadly increasing Sharpe ratio during the hyper-parameter optimization
loop; at each iteration, small changes in hyper-parameter values are tried
and the combination with largest increase is chosen (the routine ends when
no increase is found). Finally, the optimized policy is printed.
When you do so, you can observe the values of all parameters that are contained
in the policy object and its components, including many that were set to their
defaults, in this case. Finally, you can see that the resulting strategy
back-tested returns have low correlation with the market benchmark,
thanks to the cvxportfolio.constraints.MarketNeutral
constraint.
Updating data.....................................................................................................
iteration 0
Current objective:
0.6005046029534814
iteration 1
Current objective:
0.6171139234851384
iteration 2
Current objective:
0.6247585131044959
iteration 3
Current objective:
0.6331797111859923
iteration 4
Current objective:
0.646250516194385
iteration 5
Current objective:
0.6576934195407363
iteration 6
Current objective:
0.6657143552713986
iteration 7
Current objective:
0.6720274705379033
iteration 8
Current objective:
0.6802259829875591
iteration 9
Current objective:
0.6855364861604615
iteration 10
Current objective:
0.6922642475911701
iteration 11
Current objective:
0.6999128639209367
iteration 12
Current objective:
0.7050025826558192
iteration 13
Current objective:
0.7050046146281316
iteration 14
Current objective:
0.7050115241448068
Optimized policy hyper-parameters:
SinglePeriodOptimization(objective=ReturnsForecast(r_hat=HistoricalMeanReturn(half_life=inf, rolling=inf), decay=1.0) - Gamma(current_value=2.8531167061100025) * FactorModelCovariance(Sigma=HistoricalFactorizedCovariance(half_life=inf, rolling=inf, kelly=True), num_factors=10) - Gamma(current_value=0.9090909090909091) * TransactionCost(a=0.00025, b=1.0, market_volumes=VolumeHatOrRealized(volume_hat=HistoricalMeanVolume(half_life=inf, rolling=Timedelta('365 days 05:45:36'))), sigma=HistoricalStandardDeviation(half_life=inf, rolling=Timedelta('365 days 05:45:36'), kelly=True), exponent=1.5) - Gamma(current_value=0.8264462809917354) * HoldingCost(short_fees=0.25, periods_per_year=YearDividedByTradingPeriod()) + CashReturn(), constraints=[DollarNeutral(), MarketNeutral(benchmark=MarketBenchmark(mean_volume_forecast=HistoricalMeanVolume(half_life=inf, rolling=Timedelta('365 days 05:45:36'))), covariance_forecaster=HistoricalFactorizedCovariance(half_life=inf, rolling=inf, kelly=True)), LeverageLimit(limit=7)], benchmark=AllCash(), cvxpy_kwargs={'solver': 'CLARABEL'})
Optimized policy back-test result:
#################################################################
Universe size 102
Initial timestamp 2016-01-04 14:30:00+00:00
Final timestamp 2024-03-19 13:30:00+00:00
Number of periods 2066
Initial value (USDOLLAR) 1.000e+06
Final value (USDOLLAR) 6.391e+06
Profit (USDOLLAR) 5.391e+06
Avg. return (annualized) 31.7%
Volatility (annualized) 42.6%
Avg. excess return (annualized) 30.0%
Avg. active return (annualized) 30.0%
Excess volatility (annualized) 42.6%
Active volatility (annualized) 42.6%
Avg. growth rate (annualized) 22.6%
Avg. excess growth rate (annualized) 21.0%
Avg. active growth rate (annualized) 21.0%
Avg. TransactionCost 0bp
Max. TransactionCost 4bp
Avg. HoldingCost 0bp
Max. HoldingCost 1bp
Sharpe ratio 0.71
Information ratio 0.71
Avg. drawdown -13.5%
Min. drawdown -49.4%
Avg. leverage 629.9%
Max. leverage 786.0%
Avg. turnover 2.2%
Max. turnover 43.6%
Avg. policy time 0.017s
Avg. simulator time 0.018s
Of which: market data 0.004s
Total time 71.472s
#################################################################
Correlation of strategy returns with benchmark:
0.16478970446546193
And this is the figure that is plotted, the result of hyper-parameter optimization with largest Sharpe ratio.