diff --git a/examples/paper_examples/hello_world.py b/examples/paper_examples/hello_world.py index 593c783b6..6be6d3f55 100644 --- a/examples/paper_examples/hello_world.py +++ b/examples/paper_examples/hello_world.py @@ -12,38 +12,106 @@ # See the License for the specific language governing permissions and # limitations under the License. """This is a simple example of back-tests with Cvxportfolio. + +This is a close translation of what was done in `this notebook +`_. +In fact, you can see that the results are identical. + +The approach used here is not recommended; in particular we download +data externally (it is done better now by the automatic data download +and cleaning code we include in Cvxportfolio). The returns used here +are close-to-close total returns, while our interface computes correctly +the open-to-open total returns. + +In this example returns and covariances are forecasted externally, +while today this can be done automatically using the default forecasters +used by :class:`cvxportfolio.ReturnsForecast` and +:class:`cvxportfolio.FullCovariance`. + +Nevertheless, you can see by running this that we are still able to +reproduce exactly the behavior of the early development versions +of the library. + +.. note :: + + To run this, you need to install ``yfinance`` and + ``pandas_datareader``. + """ -import cvxportfolio as cvx -import pandas as pd import matplotlib.pyplot as plt +import pandas as pd + +import yfinance +import pandas_datareader as pdr + +import cvxportfolio as cvx + +# Download market data +tickers = ['AMZN', 'GOOGL', 'TSLA', 'NKE'] + +returns = pd.DataFrame(dict([(ticker, + yfinance.download(ticker)['Adj Close'].pct_change()) + for ticker in tickers])) + +returns["USDOLLAR"]=pdr.get_data_fred( + 'DFF', start="1900-01-01", + end=pd.Timestamp.today())['DFF']/(252*100) +returns = returns.fillna(method='ffill').iloc[1:] -# Download market data. -market_data = cvx.DownloadedMarketData( - universe = ['AMZN', 'GOOGL', 'TSLA', 'NKE'], +print('Returns') +print(returns) + +# Create market data server. +market_data = cvx.UserProvidedMarketData( + returns = returns, cash_key = 'USDOLLAR') -print('Historical open-to-open total returns:') + +# Today we'd do all the above by (no external packages needed): +# market_data = cvx.DownloadedMarketData( +# universe = ['AMZN', 'GOOGL', 'TSLA', 'NKE'], +# cash_key = 'USDOLLAR') + + +print('Historical returns:') print(market_data.returns) +# Build forecasts of expected returns and covariances. +# Note that we shift so that each day we use ones built +# using past returns only. This is done automatically +# by the forecasters used by default in the stable versions +# of Cvxportfolio. +r_hat_with_cash = market_data.returns.rolling( + window=250).mean().shift(1).dropna() +Sigma_hat_without_cash = market_data.returns.iloc[:,:-1 + ].rolling(window=250).cov().shift(4).dropna() + +r_hat = r_hat_with_cash.iloc[:, :-1] +r_hat_cash = r_hat_with_cash.iloc[:,-1] +print('Expected returns forecast:') +print(r_hat_with_cash) + # Define transaction and holding cost models. + +# half spread HALF_SPREAD = 10E-4 -BORROW_FEE = 1E-4 * (252 / 100) # in annualized percentage -tcost_model = cvx.TcostModel(a=HALF_SPREAD) -hcost_model = cvx.HcostModel(short_fees=BORROW_FEE) -# As returns forecast, we simply take the historical means -# computed at each point in the back-test (looking only -# at past returns). That's the default behavior; we -# may as well pass a dataframe here with different predictions. -r_hat = cvx.ReturnsForecast() +# In the 2016 development code borrow fees were expressed per-period. +# In the stable version we require annualized percent. +# This value corresponds to 1 basis point per period, +# which was in the original example. +BORROW_FEE = 2.552 + +tcost_model = cvx.TcostModel(a=HALF_SPREAD, b=None) +hcost_model = cvx.HcostModel(short_fees=BORROW_FEE) -# As risk model, we choose the full historical covariance. -# It is computed every day using the full past historical -# returns at that point. (We may as well provide it as a -# dataframe.) -risk_model = cvx.FullSigma() +# As risk model, we use the historical covariances computed above. +# Note that the stable version of Cvxportfolio requires the covariance +# matrix to not include cash (as it should). In the development versions +# it was there. It doesn't make any difference in numerical terms. +risk_model = cvx.FullSigma(Sigma_hat_without_cash) # Constraint. leverage_limit = cvx.LeverageLimit(3) @@ -52,18 +120,19 @@ # objective function is maximized. gamma_risk, gamma_trade, gamma_hold = 5., 1., 1. spo_policy = cvx.SinglePeriodOpt( - objective = r_hat + objective = cvx.ReturnsForecast(r_hat) + cvx.CashReturn(r_hat_cash) - gamma_risk * risk_model - - gamma_trade * tcost_model + - gamma_trade * tcost_model - gamma_hold * hcost_model, - constraints=[leverage_limit]) + constraints=[leverage_limit], + include_cash_return=False) # Define the market simulator. market_sim = cvx.MarketSimulator( market_data = market_data, costs = [ - cvx.TcostModel(a=HALF_SPREAD), - cvx.HcostModel(short_fees=BORROW_FEE)]) + cvx.TcostModel(a=HALF_SPREAD, b=None), + cvx.HcostModel(short_fees=BORROW_FEE)]) # Initial portfolio, uniform on non-cash assets. init_portfolio = pd.Series( diff --git a/examples/risk_models.py b/examples/risk_models.py index 2fd255af9..8d0ee757d 100644 --- a/examples/risk_models.py +++ b/examples/risk_models.py @@ -13,9 +13,14 @@ # limitations under the License. """Test different choices of risk models, which has best performance? -**WORK IN PROGRESS** +.. note:: -On the Nasdaq 100 index, daily trading from 2016 to today: + The output of this example is currently (Cvxportfolio ``1.0.3``) + not too easy to read; the ``__repr__`` method of a policy object + with symbolic hyper-parameters is scheduled for improvement. It + does work, though. + +On the Dow Jones, daily trading from 2016 to today: - diagonal risk model - diagonal risk model with risk forecast error - full covariance @@ -24,28 +29,26 @@ We test on a long-only portfolio and use automatic hyper-parameter optimization to maximize the information ratio, in back-test, -versus an index ETF. +versus the index ETF. """ import os from pprint import pprint import matplotlib.pyplot as plt -import pandas as pd import numpy as np +import pandas as pd import cvxportfolio as cvx -from .universes import DOW30 as UNIVERSE -#from .universes import NDX100 as UNIVERSE +from .universes import DOW30 as UNIVERSE # Index -INDEX = 'DIA' # -#INDEX = 'QQQ' +INDEX = 'DIA' # Times. -START = '2000-01-01' #'2016-01-01' -END = None #'2017-01-01' +START = '2000-01-01' +END = None # today # Leverage. LEVERAGE_LIMIT = 1. @@ -60,40 +63,41 @@ all_in_index[INDEX] = 1. benchmark = cvx.FixedWeights(all_in_index) -# Define hyper-parameter objects: +# Define hyper-parameter objects. +# These will be included in the library in a future release. class GammaTradeCoarse(cvx.RangeHyperParameter): """Transaction cost multiplier, coarse value range.""" def __init__(self): super().__init__( - values_range=np.arange(1,11), + values_range=np.arange(1, 11), current_value=1.) class GammaTradeFine(cvx.RangeHyperParameter): """Transaction cost multiplier, fine value range.""" def __init__(self): super().__init__( - values_range=np.linspace(-1.,1.,51), + values_range=np.linspace(-1., 1., 51), current_value=0) class GammaRiskCoarse(cvx.RangeHyperParameter): """Risk term multiplier, coarse value range.""" def __init__(self): super().__init__( - values_range=np.arange(1,21), + values_range=np.arange(1, 21), current_value=1.) class GammaRiskFine(cvx.RangeHyperParameter): """Risk term multiplier, fine value range.""" def __init__(self): super().__init__( - values_range=np.linspace(-1.,1.,51), + values_range=np.linspace(-1., 1., 51), current_value=0) class Kappa(cvx.RangeHyperParameter): """Risk forecast error multiplier, fine value range.""" def __init__(self): super().__init__( - values_range=np.linspace(0.,0.5), + values_range=np.linspace(0., 0.5), current_value=0) @@ -132,7 +136,7 @@ def __init__(self): - (GammaTradeCoarse() + GammaTradeFine() ) * cvx.StocksTransactionCost() - risk_model, - constraints=[cvx.LongOnly(), cvx.LeverageLimit(LEVERAGE_LIMIT)], + constraints=[cvx.LongOnly(), cvx.LeverageLimit(LEVERAGE_LIMIT)], benchmark=benchmark, solver='CLARABEL') diff --git a/pyproject.toml b/pyproject.toml index 88fdaca7e..1a52c38c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ dependencies = ["pandas", "numpy", "matplotlib", "requests", "cvxpy", docs = ["sphinx", "furo"] dev = ["build", "twine", "coverage", "diff_cover", "pylint", "isort", "autopep8", "docformatter", "beautifulsoup4"] +examples = ['beatifulsoup4', 'pandas_datareader', 'yfinance'] [project.urls] Homepage = "https://www.cvxportfolio.com"