Skip to content

Commit fb373bc

Browse files
committed
pytest-codeblocks simplifies testing of README file
1 parent 8421bb7 commit fb373bc

2 files changed

Lines changed: 154 additions & 181 deletions

File tree

README.md

Lines changed: 154 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,15 @@ Head over to the **[documentation on ReadTheDocs](https://pyportfolioopt.readthe
7373

7474
### Using pip
7575

76-
```bash
76+
```
7777
pip install pyportfolioopt
7878
```
7979

8080
### From source
8181

8282
Clone the repository, navigate to the folder, and install using pip:
8383

84-
```bash
84+
```
8585
git clone https://github.com/PyPortfolio/PyPortfolioOpt.git
8686
cd PyPortfolioOpt
8787
pip install .
@@ -94,25 +94,42 @@ demonstrating how easy it is to find the long-only portfolio
9494
that maximises the Sharpe ratio (a measure of risk-adjusted returns).
9595

9696
```python
97-
>>> import pandas as pd
98-
>>> from pypfopt import EfficientFrontier
99-
>>> from pypfopt import risk_models
100-
>>> from pypfopt import expected_returns
97+
import pandas as pd
98+
from pypfopt import EfficientFrontier
99+
from pypfopt import risk_models
100+
from pypfopt import expected_returns
101101

102102
# Read in price data
103-
>>> df = pd.read_csv("tests/resources/stock_prices.csv", parse_dates=True, index_col="date")
103+
df = pd.read_csv("tests/resources/stock_prices.csv", parse_dates=True, index_col="date")
104104

105105
# Calculate expected returns and sample covariance
106-
>>> mu = expected_returns.mean_historical_return(df)
107-
>>> S = risk_models.sample_cov(df)
106+
mu = expected_returns.mean_historical_return(df)
107+
S = risk_models.sample_cov(df)
108108

109109
# Optimize for maximal Sharpe ratio
110-
>>> ef = EfficientFrontier(mu, S)
111-
>>> raw_weights = ef.max_sharpe()
112-
>>> cleaned_weights = ef.clean_weights()
113-
>>> ef.save_weights_to_file("weights.csv") # saves to file
114-
>>> for name, value in cleaned_weights.items():
115-
... print(f"{name}: {value:.4f}")
110+
ef = EfficientFrontier(mu, S)
111+
raw_weights = ef.max_sharpe()
112+
cleaned_weights = ef.clean_weights()
113+
ef.save_weights_to_file("weights.csv") # saves to file
114+
115+
for name, value in cleaned_weights.items():
116+
print(f"{name}: {value:.4f}")
117+
```
118+
119+
<!--pytest-codeblocks:cont-->
120+
121+
```python
122+
exp_return, volatility, sharpe=ef.portfolio_performance(verbose=True)
123+
124+
round(exp_return, 4), round(volatility, 4), round(sharpe, 4)
125+
print("***")
126+
```
127+
128+
<!--
129+
In this html comment we collect the output of all the (merged) cells.
130+
<!--pytest-codeblocks:expected-output-->
131+
132+
```
116133
GOOG: 0.0458
117134
AAPL: 0.0674
118135
FB: 0.2008
@@ -133,40 +150,105 @@ MA: 0.3287
133150
PFE: 0.2039
134151
JPM: 0.0000
135152
SBUX: 0.0173
136-
>>> exp_return, volatility, sharpe=ef.portfolio_performance(verbose=True)
137153
Expected annual return: 29.9%
138154
Annual volatility: 21.8%
139155
Sharpe Ratio: 1.38
140-
>>> round(exp_return, 4), round(volatility, 4), round(sharpe, 4)
141-
(0.2994, 0.2176, 1.3759)
142-
156+
***
157+
MA: 19
158+
PFE: 57
159+
FB: 12
160+
BABA: 4
161+
AAPL: 4
162+
GOOG: 1
163+
SBUX: 2
164+
BBY: 2
165+
Funds remaining: $17.46
166+
***
167+
GOOG: 0.0747
168+
AAPL: 0.0532
169+
FB: 0.0664
170+
BABA: 0.0116
171+
AMZN: 0.0518
172+
GE: -0.0595
173+
AMD: -0.0679
174+
WMT: -0.0817
175+
BAC: -0.1413
176+
GM: -0.1402
177+
T: -0.1371
178+
UAA: 0.0003
179+
SHLD: -0.0706
180+
XOM: -0.0775
181+
RRC: -0.0510
182+
BBY: 0.0349
183+
MA: 0.3758
184+
PFE: 0.1112
185+
JPM: 0.0141
186+
SBUX: 0.0330
187+
***
188+
GOOG: 0.0820
189+
AAPL: 0.0919
190+
FB: 0.1074
191+
BABA: 0.0680
192+
AMZN: 0.1011
193+
GE: 0.0309
194+
AMD: 0.0000
195+
WMT: 0.0353
196+
BAC: 0.0002
197+
GM: 0.0000
198+
T: 0.0274
199+
UAA: 0.0183
200+
SHLD: 0.0000
201+
XOM: 0.0466
202+
RRC: 0.0024
203+
BBY: 0.0645
204+
MA: 0.1426
205+
PFE: 0.0841
206+
JPM: 0.0279
207+
SBUX: 0.0695
208+
***
209+
GOOG: 0.0000
210+
AAPL: 0.1749
211+
FB: 0.0503
212+
BABA: 0.0951
213+
AMZN: 0.0000
214+
GE: 0.0000
215+
AMD: 0.0000
216+
WMT: 0.0000
217+
BAC: 0.0000
218+
GM: 0.0000
219+
T: 0.5235
220+
UAA: 0.0000
221+
SHLD: 0.0000
222+
XOM: 0.1298
223+
RRC: 0.0000
224+
BBY: 0.0000
225+
MA: 0.0000
226+
PFE: 0.0264
227+
JPM: 0.0000
228+
SBUX: 0.0000
229+
***
143230
```
231+
>>
144232
145233
This is interesting but not useful in itself.
146234
However, PyPortfolioOpt provides a method which allows you to
147235
convert the above continuous weights to an actual allocation
148236
that you could buy. Just enter the most recent prices, and the desired portfolio size ($10,000 in this example):
149237

238+
<!--pytest-codeblocks:cont-->
239+
150240
```python
151-
>>> from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
241+
from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
152242

153-
>>> latest_prices = get_latest_prices(df)
243+
latest_prices = get_latest_prices(df)
154244

155-
>>> da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=10000)
156-
>>> allocation, leftover = da.greedy_portfolio()
157-
>>> for name, value in allocation.items():
158-
... print(f"{name}: {value}")
159-
MA: 19
160-
PFE: 57
161-
FB: 12
162-
BABA: 4
163-
AAPL: 4
164-
GOOG: 1
165-
SBUX: 2
166-
BBY: 2
167-
>>> print("Funds remaining: ${:.2f}".format(leftover))
168-
Funds remaining: $17.46
245+
da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=10000)
246+
allocation, leftover = da.greedy_portfolio()
247+
for name, value in allocation.items():
248+
print(f"{name}: {value}")
169249

250+
print("Funds remaining: ${:.2f}".format(leftover))
251+
print("***")
170252
```
171253

172254
_Disclaimer: nothing about this project constitues investment advice,
@@ -255,79 +337,45 @@ The covariance matrix encodes not just the volatility of an asset, but also how
255337

256338
- Long/short: by default all of the mean-variance optimization methods in PyPortfolioOpt are long-only, but they can be initialised to allow for short positions by changing the weight bounds:
257339

258-
```python
259-
>>> ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
340+
<!--pytest-codeblocks:cont-->
260341

342+
```python
343+
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
261344
```
262345

263346
- Market neutrality: for the `efficient_risk` and `efficient_return` methods, PyPortfolioOpt provides an option to form a market-neutral portfolio (i.e weights sum to zero). This is not possible for the max Sharpe portfolio and the min volatility portfolio because in those cases because they are not invariant with respect to leverage. Market neutrality requires negative weights:
264347

265-
```python
266-
>>> ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
267-
>>> for name, value in ef.efficient_return(target_return=0.2, market_neutral=True).items():
268-
... print(f"{name}: {value:.4f}")
269-
GOOG: 0.0747
270-
AAPL: 0.0532
271-
FB: 0.0664
272-
BABA: 0.0116
273-
AMZN: 0.0518
274-
GE: -0.0595
275-
AMD: -0.0679
276-
WMT: -0.0817
277-
BAC: -0.1413
278-
GM: -0.1402
279-
T: -0.1371
280-
UAA: 0.0003
281-
SHLD: -0.0706
282-
XOM: -0.0775
283-
RRC: -0.0510
284-
BBY: 0.0349
285-
MA: 0.3758
286-
PFE: 0.1112
287-
JPM: 0.0141
288-
SBUX: 0.0330
348+
<!--pytest-codeblocks:cont-->
289349

350+
```python
351+
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
352+
for name, value in ef.efficient_return(target_return=0.2, market_neutral=True).items():
353+
print(f"{name}: {value:.4f}")
354+
print("***")
290355
```
291356

292357
- Minimum/maximum position size: it may be the case that you want no security to form more than 10% of your portfolio. This is easy to encode:
293358

294-
```python
295-
>>> ef = EfficientFrontier(mu, S, weight_bounds=(0, 0.1))
359+
<!--pytest-codeblocks:cont-->
296360

361+
```python
362+
ef = EfficientFrontier(mu, S, weight_bounds=(0, 0.1))
297363
```
298364

299365
One issue with mean-variance optimization is that it leads to many zero-weights. While these are
300366
"optimal" in-sample, there is a large body of research showing that this characteristic leads
301367
mean-variance portfolios to underperform out-of-sample. To that end, I have introduced an
302368
objective function that can reduce the number of negligible weights for any of the objective functions. Essentially, it adds a penalty (parameterised by `gamma`) on small weights, with a term that looks just like L2 regularisation in machine learning. It may be necessary to try several `gamma` values to achieve the desired number of non-negligible weights. For the test portfolio of 20 securities, `gamma ~ 1` is sufficient
303369

304-
```python
305-
>>> from pypfopt import objective_functions
306-
>>> ef = EfficientFrontier(mu, S)
307-
>>> ef.add_objective(objective_functions.L2_reg, gamma=1)
308-
>>> for name, value in ef.max_sharpe().items():
309-
... print(f"{name}: {value:.4f}")
310-
GOOG: 0.0820
311-
AAPL: 0.0919
312-
FB: 0.1074
313-
BABA: 0.0680
314-
AMZN: 0.1011
315-
GE: 0.0309
316-
AMD: 0.0000
317-
WMT: 0.0353
318-
BAC: 0.0002
319-
GM: 0.0000
320-
T: 0.0274
321-
UAA: 0.0183
322-
SHLD: 0.0000
323-
XOM: 0.0466
324-
RRC: 0.0024
325-
BBY: 0.0645
326-
MA: 0.1426
327-
PFE: 0.0841
328-
JPM: 0.0279
329-
SBUX: 0.0695
370+
<!--pytest-codeblocks:cont-->
330371

372+
```python
373+
from pypfopt import objective_functions
374+
ef = EfficientFrontier(mu, S)
375+
ef.add_objective(objective_functions.L2_reg, gamma=1)
376+
for name, value in ef.max_sharpe().items():
377+
print(f"{name}: {value:.4f}")
378+
print("***")
331379
```
332380

333381
### Black-Litterman allocation
@@ -338,38 +386,20 @@ posterior estimate. This results in much better estimates of expected returns th
338386
the mean historical return. Check out the [docs](https://pyportfolioopt.readthedocs.io/en/latest/BlackLitterman.html) for a discussion of the theory, as well as advice
339387
on formatting inputs.
340388

389+
<!--pytest-codeblocks:cont-->
390+
341391
```python
342-
>>> from pypfopt import risk_models, BlackLittermanModel
343-
344-
>>> S = risk_models.sample_cov(df)
345-
>>> viewdict = {"AAPL": 0.20, "BBY": -0.30, "BAC": 0, "SBUX": -0.2, "T": 0.131321}
346-
>>> bl = BlackLittermanModel(S, pi="equal", absolute_views=viewdict, omega="default")
347-
>>> rets = bl.bl_returns()
348-
349-
>>> ef = EfficientFrontier(rets, S)
350-
>>> for name, value in ef.max_sharpe().items():
351-
... print(f"{name}: {value:.4f}")
352-
GOOG: 0.0000
353-
AAPL: 0.1749
354-
FB: 0.0503
355-
BABA: 0.0951
356-
AMZN: 0.0000
357-
GE: 0.0000
358-
AMD: 0.0000
359-
WMT: 0.0000
360-
BAC: 0.0000
361-
GM: 0.0000
362-
T: 0.5235
363-
UAA: 0.0000
364-
SHLD: 0.0000
365-
XOM: 0.1298
366-
RRC: 0.0000
367-
BBY: 0.0000
368-
MA: 0.0000
369-
PFE: 0.0264
370-
JPM: 0.0000
371-
SBUX: 0.0000
392+
from pypfopt import risk_models, BlackLittermanModel
372393

394+
S = risk_models.sample_cov(df)
395+
viewdict = {"AAPL": 0.20, "BBY": -0.30, "BAC": 0, "SBUX": -0.2, "T": 0.131321}
396+
bl = BlackLittermanModel(S, pi="equal", absolute_views=viewdict, omega="default")
397+
rets = bl.bl_returns()
398+
399+
ef = EfficientFrontier(rets, S)
400+
for name, value in ef.max_sharpe().items():
401+
print(f"{name}: {value:.4f}")
402+
print("***")
373403
```
374404

375405
### Other optimizers
@@ -413,7 +443,7 @@ Tests are written in pytest (much more intuitive than `unittest` and the variant
413443

414444
PyPortfolioOpt provides a test dataset of daily returns for 20 tickers:
415445

416-
```python
446+
```
417447
['GOOG', 'AAPL', 'FB', 'BABA', 'AMZN', 'GE', 'AMD', 'WMT', 'BAC', 'GM', 'T', 'UAA', 'SHLD', 'XOM', 'RRC', 'BBY', 'MA', 'PFE', 'JPM', 'SBUX']
418448
```
419449

@@ -472,3 +502,7 @@ Special shout-outs to:
472502
- Rich Caputo
473503
- Nicolas Knudde
474504
- Franz Kiraly
505+
506+
507+
gives
508+

0 commit comments

Comments
 (0)