Skip to content

Commit 339daba

Browse files
committed
testing the correctness of all code in README.md
1 parent 5038d93 commit 339daba

File tree

2 files changed

+233
-58
lines changed

2 files changed

+233
-58
lines changed

README.md

Lines changed: 188 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,24 @@ 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 .
8888
```
8989

9090
## Getting started
9191

92-
Here is an example on real life stock data,
93-
demonstrating how easy it is to find the long-only portfolio
92+
Here is an example on real life stock data,
93+
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
@@ -111,82 +111,119 @@ ef = EfficientFrontier(mu, S)
111111
raw_weights = ef.max_sharpe()
112112
cleaned_weights = ef.clean_weights()
113113
ef.save_weights_to_file("weights.csv") # saves to file
114-
print(cleaned_weights)
115-
ef.portfolio_performance(verbose=True)
114+
115+
for name, value in cleaned_weights.items():
116+
print(f"{name}: {value:.4f}")
117+
```
118+
119+
```result
120+
GOOG: 0.0458
121+
AAPL: 0.0674
122+
FB: 0.2008
123+
BABA: 0.0849
124+
AMZN: 0.0352
125+
GE: 0.0000
126+
AMD: 0.0000
127+
WMT: 0.0000
128+
BAC: 0.0000
129+
GM: 0.0000
130+
T: 0.0000
131+
UAA: 0.0000
132+
SHLD: 0.0000
133+
XOM: 0.0000
134+
RRC: 0.0000
135+
BBY: 0.0159
136+
MA: 0.3287
137+
PFE: 0.2039
138+
JPM: 0.0000
139+
SBUX: 0.0173
116140
```
117141

118-
This outputs the following weights:
119-
120-
```txt
121-
{'GOOG': 0.03835,
122-
'AAPL': 0.0689,
123-
'FB': 0.20603,
124-
'BABA': 0.07315,
125-
'AMZN': 0.04033,
126-
'GE': 0.0,
127-
'AMD': 0.0,
128-
'WMT': 0.0,
129-
'BAC': 0.0,
130-
'GM': 0.0,
131-
'T': 0.0,
132-
'UAA': 0.0,
133-
'SHLD': 0.0,
134-
'XOM': 0.0,
135-
'RRC': 0.0,
136-
'BBY': 0.01324,
137-
'MA': 0.35349,
138-
'PFE': 0.1957,
139-
'JPM': 0.0,
140-
'SBUX': 0.01082}
141-
142-
Expected annual return: 30.5%
143-
Annual volatility: 22.2%
144-
Sharpe Ratio: 1.28
142+
GOOG: 0.0000
143+
AAPL: 0.1749
144+
FB: 0.0503
145+
BABA: 0.0951
146+
AMZN: 0.0000
147+
GE: 0.0000
148+
AMD: 0.0000
149+
WMT: 0.0000
150+
BAC: 0.0000
151+
GM: 0.0000
152+
T: 0.5235
153+
UAA: 0.0000
154+
SHLD: 0.0000
155+
XOM: 0.1298
156+
RRC: 0.0000
157+
BBY: 0.0000
158+
MA: 0.0000
159+
PFE: 0.0264
160+
JPM: 0.0000
161+
SBUX: 0.0000
162+
***
145163
```
164+
>>
165+
166+
```python
167+
exp_return, volatility, sharpe=ef.portfolio_performance(verbose=True)
146168
147-
This is interesting but not useful in itself.
148-
However, PyPortfolioOpt provides a method which allows you to
149-
convert the above continuous weights to an actual allocation
169+
round(exp_return, 4), round(volatility, 4), round(sharpe, 4)
170+
```
171+
172+
```result
173+
Expected annual return: 29.9%
174+
Annual volatility: 21.8%
175+
Sharpe Ratio: 1.38
176+
```
177+
178+
This is interesting but not useful in itself.
179+
However, PyPortfolioOpt provides a method which allows you to
180+
convert the above continuous weights to an actual allocation
150181
that you could buy. Just enter the most recent prices, and the desired portfolio size ($10,000 in this example):
151182

152183
```python
153184
from pypfopt.discrete_allocation import DiscreteAllocation, get_latest_prices
154185

155-
156186
latest_prices = get_latest_prices(df)
157187

158-
da = DiscreteAllocation(weights, latest_prices, total_portfolio_value=10000)
188+
da = DiscreteAllocation(cleaned_weights, latest_prices, total_portfolio_value=10000)
159189
allocation, leftover = da.greedy_portfolio()
160-
print("Discrete allocation:", allocation)
190+
for name, value in allocation.items():
191+
print(f"{name}: {value}")
192+
161193
print("Funds remaining: ${:.2f}".format(leftover))
162194
```
163195

164-
```txt
165-
12 out of 20 tickers were removed
166-
Discrete allocation: {'GOOG': 1, 'AAPL': 4, 'FB': 12, 'BABA': 4, 'BBY': 2,
167-
'MA': 20, 'PFE': 54, 'SBUX': 1}
168-
Funds remaining: $11.89
196+
```result
197+
MA: 19
198+
PFE: 57
199+
FB: 12
200+
BABA: 4
201+
AAPL: 4
202+
GOOG: 1
203+
SBUX: 2
204+
BBY: 2
205+
Funds remaining: $17.46
169206
```
170207

171-
_Disclaimer: nothing about this project constitues investment advice,
172-
and the author bears no responsibiltiy for your subsequent investment decisions.
208+
_Disclaimer: nothing about this project constitues investment advice,
209+
and the author bears no responsibiltiy for your subsequent investment decisions.
173210
Please refer to the [license](https://github.com/PyPortfolio/PyPortfolioOpt/blob/main/LICENSE.txt) for more information._
174211

175212
## An overview of classical portfolio optimization methods
176213

177-
Harry Markowitz's 1952 paper is the undeniable classic,
178-
which turned portfolio optimization from an art into a science.
179-
The key insight is that by combining assets with different expected returns and volatilities,
180-
one can decide on a mathematically optimal allocation which minimises
214+
Harry Markowitz's 1952 paper is the undeniable classic,
215+
which turned portfolio optimization from an art into a science.
216+
The key insight is that by combining assets with different expected returns and volatilities,
217+
one can decide on a mathematically optimal allocation which minimises
181218
the risk for a target return – the set of all such optimal portfolios is referred to as the **efficient frontier**.
182219

183220
<center>
184221
<img src="https://github.com/PyPortfolio/PyPortfolioOpt/blob/main/media/efficient_frontier_white.png?raw=true" style="width:60%;"/>
185222
</center>
186223

187-
Although much development has been made in the subject, more than half a century later,
224+
Although much development has been made in the subject, more than half a century later,
188225
Markowitz's core ideas are still fundamentally important and see daily use in many portfolio management firms.
189-
The main drawback of mean-variance optimization is that the theoretical
226+
The main drawback of mean-variance optimization is that the theoretical
190227
treatment requires knowledge of the expected returns and the future risk-characteristics (covariance) of the assets. Obviously, if we knew the expected returns of a stock life would be much easier, but the whole game is that stock returns are notoriously hard to forecast. As a substitute, we can derive estimates of the expected return and covariance based on historical data – though we do lose the theoretical guarantees provided by Markowitz, the closer our estimates are to the real values, the better our portfolio will be.
191228

192229
Thus this project provides four major sets of functionality (though of course they are intimately related)
@@ -254,32 +291,94 @@ The covariance matrix encodes not just the volatility of an asset, but also how
254291

255292
- 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:
256293

294+
<!--pytest-codeblocks:cont-->
295+
257296
```python
258297
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
259298
```
260299

300+
```result
301+
```
302+
261303
- 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:
262304

305+
<!--pytest-codeblocks:cont-->
306+
263307
```python
264308
ef = EfficientFrontier(mu, S, weight_bounds=(-1, 1))
265-
ef.efficient_return(target_return=0.2, market_neutral=True)
309+
for name, value in ef.efficient_return(target_return=0.2, market_neutral=True).items():
310+
print(f"{name}: {value:.4f}")
311+
```
312+
313+
```result
314+
GOOG: 0.0747
315+
AAPL: 0.0532
316+
FB: 0.0664
317+
BABA: 0.0116
318+
AMZN: 0.0518
319+
GE: -0.0595
320+
AMD: -0.0679
321+
WMT: -0.0817
322+
BAC: -0.1413
323+
GM: -0.1402
324+
T: -0.1371
325+
UAA: 0.0003
326+
SHLD: -0.0706
327+
XOM: -0.0775
328+
RRC: -0.0510
329+
BBY: 0.0349
330+
MA: 0.3758
331+
PFE: 0.1112
332+
JPM: 0.0141
333+
SBUX: 0.0330
266334
```
267335

268336
- 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:
269337

338+
<!--pytest-codeblocks:cont-->
339+
270340
```python
271341
ef = EfficientFrontier(mu, S, weight_bounds=(0, 0.1))
272342
```
273343

344+
```result
345+
```
274346
One issue with mean-variance optimization is that it leads to many zero-weights. While these are
275347
"optimal" in-sample, there is a large body of research showing that this characteristic leads
276348
mean-variance portfolios to underperform out-of-sample. To that end, I have introduced an
277349
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
278350

351+
<!--pytest-codeblocks:cont-->
352+
279353
```python
354+
from pypfopt import objective_functions
280355
ef = EfficientFrontier(mu, S)
281356
ef.add_objective(objective_functions.L2_reg, gamma=1)
282-
ef.max_sharpe()
357+
for name, value in ef.max_sharpe().items():
358+
print(f"{name}: {value:.4f}")
359+
```
360+
361+
```result
362+
GOOG: 0.0820
363+
AAPL: 0.0919
364+
FB: 0.1074
365+
BABA: 0.0680
366+
AMZN: 0.1011
367+
GE: 0.0309
368+
AMD: 0.0000
369+
WMT: 0.0353
370+
BAC: 0.0002
371+
GM: 0.0000
372+
T: 0.0274
373+
UAA: 0.0183
374+
SHLD: 0.0000
375+
XOM: 0.0466
376+
RRC: 0.0024
377+
BBY: 0.0645
378+
MA: 0.1426
379+
PFE: 0.0841
380+
JPM: 0.0279
381+
SBUX: 0.0695
283382
```
284383

285384
### Black-Litterman allocation
@@ -290,14 +389,42 @@ posterior estimate. This results in much better estimates of expected returns th
290389
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
291390
on formatting inputs.
292391

392+
<!--pytest-codeblocks:cont-->
393+
293394
```python
395+
from pypfopt import risk_models, BlackLittermanModel
396+
294397
S = risk_models.sample_cov(df)
295398
viewdict = {"AAPL": 0.20, "BBY": -0.30, "BAC": 0, "SBUX": -0.2, "T": 0.131321}
296399
bl = BlackLittermanModel(S, pi="equal", absolute_views=viewdict, omega="default")
297400
rets = bl.bl_returns()
298401

299402
ef = EfficientFrontier(rets, S)
300-
ef.max_sharpe()
403+
for name, value in ef.max_sharpe().items():
404+
print(f"{name}: {value:.4f}")
405+
```
406+
407+
```result
408+
GOOG: 0.0000
409+
AAPL: 0.1749
410+
FB: 0.0503
411+
BABA: 0.0951
412+
AMZN: 0.0000
413+
GE: 0.0000
414+
AMD: 0.0000
415+
WMT: 0.0000
416+
BAC: 0.0000
417+
GM: 0.0000
418+
T: 0.5235
419+
UAA: 0.0000
420+
SHLD: 0.0000
421+
XOM: 0.1298
422+
RRC: 0.0000
423+
BBY: 0.0000
424+
MA: 0.0000
425+
PFE: 0.0264
426+
JPM: 0.0000
427+
SBUX: 0.0000
301428
```
302429

303430
### Other optimizers
@@ -342,8 +469,10 @@ Tests are written in pytest (much more intuitive than `unittest` and the variant
342469
PyPortfolioOpt provides a test dataset of daily returns for 20 tickers:
343470

344471
```python
345-
['GOOG', 'AAPL', 'FB', 'BABA', 'AMZN', 'GE', 'AMD', 'WMT', 'BAC', 'GM',
346-
'T', 'UAA', 'SHLD', 'XOM', 'RRC', 'BBY', 'MA', 'PFE', 'JPM', 'SBUX']
472+
['GOOG', 'AAPL', 'FB', 'BABA', 'AMZN', 'GE', 'AMD', 'WMT', 'BAC', 'GM', 'T', 'UAA', 'SHLD', 'XOM', 'RRC', 'BBY', 'MA', 'PFE', 'JPM', 'SBUX']
473+
```
474+
475+
```result
347476
```
348477

349478
These tickers have been informally selected to meet several criteria:
@@ -390,7 +519,7 @@ Contributions are _most welcome_. Have a look at the [Contribution Guide](https:
390519
I'd like to thank all of the people who have contributed to PyPortfolioOpt since its release in 2018.
391520
Special shout-outs to:
392521

393-
- Tuan Tran (who is now the primary maintainer!)
522+
- Tuan Tran
394523
- Philipp Schiele
395524
- Carl Peasnell
396525
- Felipe Schneider
@@ -400,3 +529,4 @@ Special shout-outs to:
400529
- Thomas Schmelzer
401530
- Rich Caputo
402531
- Nicolas Knudde
532+
- Franz Kiraly

0 commit comments

Comments
 (0)