markowizard 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- markowizard-0.1.0/LICENSE +21 -0
- markowizard-0.1.0/PKG-INFO +225 -0
- markowizard-0.1.0/README.md +195 -0
- markowizard-0.1.0/markowizard/__init__.py +21 -0
- markowizard-0.1.0/markowizard/allocation.py +138 -0
- markowizard-0.1.0/markowizard/core.py +188 -0
- markowizard-0.1.0/markowizard/data.py +91 -0
- markowizard-0.1.0/markowizard/visualization.py +322 -0
- markowizard-0.1.0/markowizard.egg-info/PKG-INFO +225 -0
- markowizard-0.1.0/markowizard.egg-info/SOURCES.txt +14 -0
- markowizard-0.1.0/markowizard.egg-info/dependency_links.txt +1 -0
- markowizard-0.1.0/markowizard.egg-info/requires.txt +5 -0
- markowizard-0.1.0/markowizard.egg-info/top_level.txt +1 -0
- markowizard-0.1.0/pyproject.toml +76 -0
- markowizard-0.1.0/setup.cfg +4 -0
- markowizard-0.1.0/tests/test_package.py +159 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021 Gustavo Furtado
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: markowizard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Markowitz portfolio optimization and analysis library
|
|
5
|
+
Author-email: GusFurtado <gustavofurtado2@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/OutliersAnalytics/MarkoWizard
|
|
8
|
+
Project-URL: Repository, https://github.com/OutliersAnalytics/MarkoWizard
|
|
9
|
+
Project-URL: Documentation, https://github.com/OutliersAnalytics/MarkoWizard#readme
|
|
10
|
+
Project-URL: Issues, https://github.com/OutliersAnalytics/MarkoWizard/issues
|
|
11
|
+
Keywords: portfolio,finance,optimization,markowitz,efficient-frontier
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: numpy>=1.22
|
|
25
|
+
Requires-Dist: pandas>=1.4
|
|
26
|
+
Requires-Dist: scipy>=1.8
|
|
27
|
+
Requires-Dist: yfinance>=0.1.63
|
|
28
|
+
Requires-Dist: plotly>=5.10
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# MarkoWizard
|
|
32
|
+
|
|
33
|
+
A modern Python library for Markowitz portfolio optimization and analysis.
|
|
34
|
+
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
[](https://www.python.org/downloads/)
|
|
37
|
+
|
|
38
|
+
> **Previously known as _Diversificador_.** The original portfolio-analysis web app
|
|
39
|
+
> built with [Dash](https://dash.plotly.com/) is no longer maintained, but it is
|
|
40
|
+
> preserved on the [`dash-deprecated`](../../tree/dash-deprecated) branch for reference.
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **Markowitz Mean-Variance Optimization** — Compute the efficient frontier using `scipy.optimize`
|
|
45
|
+
- **Capital Allocation Line** — Mix risky portfolios with risk-free assets
|
|
46
|
+
- **Visualization** — Plotly-based charts for efficient frontier, allocation pie, CAL, correlation heatmaps, and price timelines
|
|
47
|
+
- **Data Fetching** — Optional convenience functions for downloading market data via yfinance
|
|
48
|
+
- **Web Application** — FastAPI backend with a dark-themed interactive frontend
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install markowizard
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
That's everything the library needs: optimization (`scipy`), market-data
|
|
57
|
+
fetching (`yfinance`), and visualization (`plotly`). No optional extras.
|
|
58
|
+
|
|
59
|
+
## Quick Start (Library)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from markowizard import MarkowitzOptimizer, CapitalAllocator
|
|
63
|
+
from markowizard.data import fetch_prices, compute_monthly_returns
|
|
64
|
+
from markowizard.visualization import efficiency_frontier_plot
|
|
65
|
+
|
|
66
|
+
# Fetch prices and compute monthly returns (decimal form, e.g. 0.01 = 1%)...
|
|
67
|
+
prices = fetch_prices(["AAPL", "MSFT", "GOOGL", "SPY"], period="5y")
|
|
68
|
+
returns = compute_monthly_returns(prices)
|
|
69
|
+
# ...or bring your own returns DataFrame (assets as columns).
|
|
70
|
+
|
|
71
|
+
# Optimize
|
|
72
|
+
optimizer = MarkowitzOptimizer(returns)
|
|
73
|
+
portfolios = optimizer.optimize()
|
|
74
|
+
|
|
75
|
+
# Compute Sharpe ratios (provide monthly risk-free rate)
|
|
76
|
+
risk_free_rate = 0.005 # 0.5% per month
|
|
77
|
+
portfolios = optimizer.compute_sharpe(risk_free_rate)
|
|
78
|
+
|
|
79
|
+
# Plot the efficient frontier
|
|
80
|
+
fig = efficiency_frontier_plot(portfolios, highlight_portfolio=50)
|
|
81
|
+
fig.show()
|
|
82
|
+
|
|
83
|
+
# Best portfolio (maximum Sharpe ratio)
|
|
84
|
+
best = optimizer.max_sharpe_portfolio()
|
|
85
|
+
print(best)
|
|
86
|
+
|
|
87
|
+
# Capital allocation line
|
|
88
|
+
allocator = CapitalAllocator(best, risk_free_rate)
|
|
89
|
+
cal_points = allocator.capital_allocation_line(steps=21)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Web Application
|
|
93
|
+
|
|
94
|
+
An interactive web UI (FastAPI + a dark-themed frontend) lives in `backend/` and
|
|
95
|
+
`frontend/`. It is **not part of the PyPI package** — run it from the container
|
|
96
|
+
image or a clone.
|
|
97
|
+
|
|
98
|
+
### Using Docker
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
docker run -p 8000:8000 ghcr.io/outliersanalytics/markowizard:latest
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### From a clone
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/OutliersAnalytics/MarkoWizard
|
|
108
|
+
cd MarkoWizard
|
|
109
|
+
uv run --with-requirements backend/requirements.txt uvicorn backend.main:app --port 8000
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Open [http://localhost:8000](http://localhost:8000) — the app auto-submits with
|
|
113
|
+
default tickers on load.
|
|
114
|
+
|
|
115
|
+
It exposes a single endpoint, `POST /api/analyze`:
|
|
116
|
+
|
|
117
|
+
```json
|
|
118
|
+
{
|
|
119
|
+
"tickers": ["AAPL", "MSFT", "GOOGL", "SPY"],
|
|
120
|
+
"period": "5y",
|
|
121
|
+
"risk_free_rate": 0.005
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
which returns the efficient frontier, max-Sharpe portfolio, capital-allocation-line
|
|
126
|
+
points, and correlation matrix as JSON. The frontend renders the charts.
|
|
127
|
+
|
|
128
|
+
## API Reference
|
|
129
|
+
|
|
130
|
+
### `markowizard` (top-level)
|
|
131
|
+
|
|
132
|
+
| Export | Description |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `MarkowitzOptimizer` | Efficient frontier optimization (from `core`) |
|
|
135
|
+
| `CapitalAllocator` | Risk-free asset allocation (from `allocation`) |
|
|
136
|
+
| `__version__` | Package version string |
|
|
137
|
+
|
|
138
|
+
### `markowizard.core`
|
|
139
|
+
|
|
140
|
+
#### `MarkowitzOptimizer`
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
class MarkowitzOptimizer:
|
|
144
|
+
def __init__(self, returns: pd.DataFrame) -> None
|
|
145
|
+
def optimize(self) -> pd.DataFrame
|
|
146
|
+
def compute_sharpe(self, risk_free_rate: float) -> pd.DataFrame
|
|
147
|
+
def max_sharpe_portfolio(self) -> pd.Series
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Constants**: `COL_RETURN = "Expected Return"`, `COL_RISK = "Risk"`, `COL_SHARPE = "Sharpe"`, `COL_RISK_FREE = "Risk-Free"`
|
|
151
|
+
|
|
152
|
+
**Parameters**:
|
|
153
|
+
- `returns`: DataFrame where each column is an asset and each row is a time period. Values must be in decimal form (e.g., 0.01 = 1%).
|
|
154
|
+
|
|
155
|
+
**`optimize()`** computes the efficient frontier by solving 100 quadratic programming problems with varying risk-aversion parameters. Uses warm-starting: each iteration's solution seeds the next.
|
|
156
|
+
|
|
157
|
+
**`compute_sharpe(risk_free_rate)`** adds a `Sharpe` column. `risk_free_rate` must match the period of `returns` (e.g., monthly).
|
|
158
|
+
|
|
159
|
+
**`max_sharpe_portfolio()`** returns the tangency portfolio row.
|
|
160
|
+
|
|
161
|
+
#### `MarkowitzOptimizer.portfolios` DataFrame columns
|
|
162
|
+
|
|
163
|
+
| Column | Description |
|
|
164
|
+
|---|---|
|
|
165
|
+
| (ticker columns) | Asset weights (sum to 1, all >= 0) |
|
|
166
|
+
| `Expected Return` | Expected portfolio return |
|
|
167
|
+
| `Risk` | Portfolio standard deviation (risk) |
|
|
168
|
+
| `Sharpe` | Sharpe ratio (after `compute_sharpe()`) |
|
|
169
|
+
|
|
170
|
+
### `markowizard.allocation`
|
|
171
|
+
|
|
172
|
+
#### `CapitalAllocator`
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
class CapitalAllocator:
|
|
176
|
+
def __init__(self, portfolio: pd.Series | Mapping, risk_free_rate: float) -> None
|
|
177
|
+
@staticmethod
|
|
178
|
+
def weigh_risk_free(value: float, risk_free_value: float, p: float) -> float
|
|
179
|
+
def capital_allocation_line(self, steps: int = 21) -> list[dict]
|
|
180
|
+
def final_allocation(self, p: float) -> dict[str, float]
|
|
181
|
+
def expected_returns(self, p: float) -> tuple[float, float]
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**`capital_allocation_line()`** returns points along the CAL, each with keys `p`, `expected_return`, `risk`, and `label`.
|
|
185
|
+
|
|
186
|
+
**`final_allocation(p)`** returns asset weights including `Risk-Free` (risk-free portion).
|
|
187
|
+
|
|
188
|
+
### `markowizard.visualization`
|
|
189
|
+
|
|
190
|
+
| Function | Returns | Description |
|
|
191
|
+
|---|---|---|
|
|
192
|
+
| `efficiency_frontier_plot(portfolios, highlight_portfolio=0)` | `Figure` | Scatter plot of expected return vs risk |
|
|
193
|
+
| `allocation_pie(portfolio)` | `Figure` | Pie chart of asset weights |
|
|
194
|
+
| `capital_allocation_line_plot(cal_points, highlight_point=0)` | `Figure` | CAL risk-return trade-off |
|
|
195
|
+
| `correlation_timeline(prices, ticker_a, ticker_b=None)` | `Figure` | Price history (single or normalized dual) |
|
|
196
|
+
| `correlation_heatmap(corr_matrix)` | `Figure` | Correlation matrix heatmap |
|
|
197
|
+
|
|
198
|
+
All visualization functions return Plotly `Figure` objects — call `.show()` to display.
|
|
199
|
+
|
|
200
|
+
### `markowizard.data`
|
|
201
|
+
|
|
202
|
+
| Function | Returns | Description |
|
|
203
|
+
|---|---|---|
|
|
204
|
+
| `fetch_prices(tickers, period="5y", auto_adjust=True)` | `pd.DataFrame` | Historical close prices from Yahoo Finance |
|
|
205
|
+
| `compute_monthly_returns(prices)` | `pd.DataFrame` | Monthly returns from daily close prices |
|
|
206
|
+
|
|
207
|
+
## Modules
|
|
208
|
+
|
|
209
|
+
| Module | Description |
|
|
210
|
+
|---|---|
|
|
211
|
+
| `core` | `MarkowitzOptimizer` — efficient frontier optimization |
|
|
212
|
+
| `allocation` | `CapitalAllocator` — risk-free asset allocation |
|
|
213
|
+
| `visualization` | Plotly chart functions (efficient frontier, pie, CAL, correlation) |
|
|
214
|
+
| `data` | Market-data fetching and monthly-return helpers (yfinance) |
|
|
215
|
+
|
|
216
|
+
The web application (`backend/`, `frontend/`) is kept in the repo but is not
|
|
217
|
+
part of the installable package — see [Web Application](#web-application).
|
|
218
|
+
|
|
219
|
+
## Development
|
|
220
|
+
|
|
221
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and contribution guidelines.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# MarkoWizard
|
|
2
|
+
|
|
3
|
+
A modern Python library for Markowitz portfolio optimization and analysis.
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.python.org/downloads/)
|
|
7
|
+
|
|
8
|
+
> **Previously known as _Diversificador_.** The original portfolio-analysis web app
|
|
9
|
+
> built with [Dash](https://dash.plotly.com/) is no longer maintained, but it is
|
|
10
|
+
> preserved on the [`dash-deprecated`](../../tree/dash-deprecated) branch for reference.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Markowitz Mean-Variance Optimization** — Compute the efficient frontier using `scipy.optimize`
|
|
15
|
+
- **Capital Allocation Line** — Mix risky portfolios with risk-free assets
|
|
16
|
+
- **Visualization** — Plotly-based charts for efficient frontier, allocation pie, CAL, correlation heatmaps, and price timelines
|
|
17
|
+
- **Data Fetching** — Optional convenience functions for downloading market data via yfinance
|
|
18
|
+
- **Web Application** — FastAPI backend with a dark-themed interactive frontend
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install markowizard
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That's everything the library needs: optimization (`scipy`), market-data
|
|
27
|
+
fetching (`yfinance`), and visualization (`plotly`). No optional extras.
|
|
28
|
+
|
|
29
|
+
## Quick Start (Library)
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from markowizard import MarkowitzOptimizer, CapitalAllocator
|
|
33
|
+
from markowizard.data import fetch_prices, compute_monthly_returns
|
|
34
|
+
from markowizard.visualization import efficiency_frontier_plot
|
|
35
|
+
|
|
36
|
+
# Fetch prices and compute monthly returns (decimal form, e.g. 0.01 = 1%)...
|
|
37
|
+
prices = fetch_prices(["AAPL", "MSFT", "GOOGL", "SPY"], period="5y")
|
|
38
|
+
returns = compute_monthly_returns(prices)
|
|
39
|
+
# ...or bring your own returns DataFrame (assets as columns).
|
|
40
|
+
|
|
41
|
+
# Optimize
|
|
42
|
+
optimizer = MarkowitzOptimizer(returns)
|
|
43
|
+
portfolios = optimizer.optimize()
|
|
44
|
+
|
|
45
|
+
# Compute Sharpe ratios (provide monthly risk-free rate)
|
|
46
|
+
risk_free_rate = 0.005 # 0.5% per month
|
|
47
|
+
portfolios = optimizer.compute_sharpe(risk_free_rate)
|
|
48
|
+
|
|
49
|
+
# Plot the efficient frontier
|
|
50
|
+
fig = efficiency_frontier_plot(portfolios, highlight_portfolio=50)
|
|
51
|
+
fig.show()
|
|
52
|
+
|
|
53
|
+
# Best portfolio (maximum Sharpe ratio)
|
|
54
|
+
best = optimizer.max_sharpe_portfolio()
|
|
55
|
+
print(best)
|
|
56
|
+
|
|
57
|
+
# Capital allocation line
|
|
58
|
+
allocator = CapitalAllocator(best, risk_free_rate)
|
|
59
|
+
cal_points = allocator.capital_allocation_line(steps=21)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Web Application
|
|
63
|
+
|
|
64
|
+
An interactive web UI (FastAPI + a dark-themed frontend) lives in `backend/` and
|
|
65
|
+
`frontend/`. It is **not part of the PyPI package** — run it from the container
|
|
66
|
+
image or a clone.
|
|
67
|
+
|
|
68
|
+
### Using Docker
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
docker run -p 8000:8000 ghcr.io/outliersanalytics/markowizard:latest
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### From a clone
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
git clone https://github.com/OutliersAnalytics/MarkoWizard
|
|
78
|
+
cd MarkoWizard
|
|
79
|
+
uv run --with-requirements backend/requirements.txt uvicorn backend.main:app --port 8000
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Open [http://localhost:8000](http://localhost:8000) — the app auto-submits with
|
|
83
|
+
default tickers on load.
|
|
84
|
+
|
|
85
|
+
It exposes a single endpoint, `POST /api/analyze`:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"tickers": ["AAPL", "MSFT", "GOOGL", "SPY"],
|
|
90
|
+
"period": "5y",
|
|
91
|
+
"risk_free_rate": 0.005
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
which returns the efficient frontier, max-Sharpe portfolio, capital-allocation-line
|
|
96
|
+
points, and correlation matrix as JSON. The frontend renders the charts.
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### `markowizard` (top-level)
|
|
101
|
+
|
|
102
|
+
| Export | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `MarkowitzOptimizer` | Efficient frontier optimization (from `core`) |
|
|
105
|
+
| `CapitalAllocator` | Risk-free asset allocation (from `allocation`) |
|
|
106
|
+
| `__version__` | Package version string |
|
|
107
|
+
|
|
108
|
+
### `markowizard.core`
|
|
109
|
+
|
|
110
|
+
#### `MarkowitzOptimizer`
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
class MarkowitzOptimizer:
|
|
114
|
+
def __init__(self, returns: pd.DataFrame) -> None
|
|
115
|
+
def optimize(self) -> pd.DataFrame
|
|
116
|
+
def compute_sharpe(self, risk_free_rate: float) -> pd.DataFrame
|
|
117
|
+
def max_sharpe_portfolio(self) -> pd.Series
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**Constants**: `COL_RETURN = "Expected Return"`, `COL_RISK = "Risk"`, `COL_SHARPE = "Sharpe"`, `COL_RISK_FREE = "Risk-Free"`
|
|
121
|
+
|
|
122
|
+
**Parameters**:
|
|
123
|
+
- `returns`: DataFrame where each column is an asset and each row is a time period. Values must be in decimal form (e.g., 0.01 = 1%).
|
|
124
|
+
|
|
125
|
+
**`optimize()`** computes the efficient frontier by solving 100 quadratic programming problems with varying risk-aversion parameters. Uses warm-starting: each iteration's solution seeds the next.
|
|
126
|
+
|
|
127
|
+
**`compute_sharpe(risk_free_rate)`** adds a `Sharpe` column. `risk_free_rate` must match the period of `returns` (e.g., monthly).
|
|
128
|
+
|
|
129
|
+
**`max_sharpe_portfolio()`** returns the tangency portfolio row.
|
|
130
|
+
|
|
131
|
+
#### `MarkowitzOptimizer.portfolios` DataFrame columns
|
|
132
|
+
|
|
133
|
+
| Column | Description |
|
|
134
|
+
|---|---|
|
|
135
|
+
| (ticker columns) | Asset weights (sum to 1, all >= 0) |
|
|
136
|
+
| `Expected Return` | Expected portfolio return |
|
|
137
|
+
| `Risk` | Portfolio standard deviation (risk) |
|
|
138
|
+
| `Sharpe` | Sharpe ratio (after `compute_sharpe()`) |
|
|
139
|
+
|
|
140
|
+
### `markowizard.allocation`
|
|
141
|
+
|
|
142
|
+
#### `CapitalAllocator`
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
class CapitalAllocator:
|
|
146
|
+
def __init__(self, portfolio: pd.Series | Mapping, risk_free_rate: float) -> None
|
|
147
|
+
@staticmethod
|
|
148
|
+
def weigh_risk_free(value: float, risk_free_value: float, p: float) -> float
|
|
149
|
+
def capital_allocation_line(self, steps: int = 21) -> list[dict]
|
|
150
|
+
def final_allocation(self, p: float) -> dict[str, float]
|
|
151
|
+
def expected_returns(self, p: float) -> tuple[float, float]
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**`capital_allocation_line()`** returns points along the CAL, each with keys `p`, `expected_return`, `risk`, and `label`.
|
|
155
|
+
|
|
156
|
+
**`final_allocation(p)`** returns asset weights including `Risk-Free` (risk-free portion).
|
|
157
|
+
|
|
158
|
+
### `markowizard.visualization`
|
|
159
|
+
|
|
160
|
+
| Function | Returns | Description |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `efficiency_frontier_plot(portfolios, highlight_portfolio=0)` | `Figure` | Scatter plot of expected return vs risk |
|
|
163
|
+
| `allocation_pie(portfolio)` | `Figure` | Pie chart of asset weights |
|
|
164
|
+
| `capital_allocation_line_plot(cal_points, highlight_point=0)` | `Figure` | CAL risk-return trade-off |
|
|
165
|
+
| `correlation_timeline(prices, ticker_a, ticker_b=None)` | `Figure` | Price history (single or normalized dual) |
|
|
166
|
+
| `correlation_heatmap(corr_matrix)` | `Figure` | Correlation matrix heatmap |
|
|
167
|
+
|
|
168
|
+
All visualization functions return Plotly `Figure` objects — call `.show()` to display.
|
|
169
|
+
|
|
170
|
+
### `markowizard.data`
|
|
171
|
+
|
|
172
|
+
| Function | Returns | Description |
|
|
173
|
+
|---|---|---|
|
|
174
|
+
| `fetch_prices(tickers, period="5y", auto_adjust=True)` | `pd.DataFrame` | Historical close prices from Yahoo Finance |
|
|
175
|
+
| `compute_monthly_returns(prices)` | `pd.DataFrame` | Monthly returns from daily close prices |
|
|
176
|
+
|
|
177
|
+
## Modules
|
|
178
|
+
|
|
179
|
+
| Module | Description |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `core` | `MarkowitzOptimizer` — efficient frontier optimization |
|
|
182
|
+
| `allocation` | `CapitalAllocator` — risk-free asset allocation |
|
|
183
|
+
| `visualization` | Plotly chart functions (efficient frontier, pie, CAL, correlation) |
|
|
184
|
+
| `data` | Market-data fetching and monthly-return helpers (yfinance) |
|
|
185
|
+
|
|
186
|
+
The web application (`backend/`, `frontend/`) is kept in the repo but is not
|
|
187
|
+
part of the installable package — see [Web Application](#web-application).
|
|
188
|
+
|
|
189
|
+
## Development
|
|
190
|
+
|
|
191
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup instructions and contribution guidelines.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
markowizard: Markowitz portfolio optimization and analysis library.
|
|
3
|
+
|
|
4
|
+
Provides tools for mean-variance optimization, capital allocation,
|
|
5
|
+
visualization, and optional market data fetching.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
9
|
+
|
|
10
|
+
from markowizard.allocation import CapitalAllocator
|
|
11
|
+
from markowizard.core import MarkowitzOptimizer
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = version("markowizard")
|
|
15
|
+
except PackageNotFoundError:
|
|
16
|
+
__version__ = "0.1.0"
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"CapitalAllocator",
|
|
20
|
+
"MarkowitzOptimizer",
|
|
21
|
+
]
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capital allocation line (CAL) analysis: mixing a risky portfolio with a
|
|
3
|
+
risk-free asset.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from markowizard.core import COL_RETURN, COL_RISK, COL_RISK_FREE, COL_SHARPE
|
|
11
|
+
|
|
12
|
+
# Keys expected in the portfolio dict/series
|
|
13
|
+
_PORTFOLIO_KEYS = {COL_RETURN, COL_RISK, COL_SHARPE}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CapitalAllocator:
|
|
17
|
+
"""
|
|
18
|
+
Combines a risky portfolio with a risk-free asset to explore how
|
|
19
|
+
different allocations affect overall expected return and risk.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
portfolio : pandas.Series or dict-like
|
|
24
|
+
A dictionary or Series representing a single portfolio, containing
|
|
25
|
+
at least 'Expected Return' (expected return) and 'Risk' (risk/std).
|
|
26
|
+
risk_free_rate : float
|
|
27
|
+
Risk-free rate (e.g., monthly rate).
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
portfolio : dict
|
|
32
|
+
The risky portfolio data.
|
|
33
|
+
rf : float
|
|
34
|
+
Risk-free rate.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
portfolio: pd.Series | Mapping[str, float],
|
|
40
|
+
risk_free_rate: float,
|
|
41
|
+
) -> None:
|
|
42
|
+
self.portfolio = dict(portfolio)
|
|
43
|
+
self.rf = risk_free_rate
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def weigh_risk_free(value: float, risk_free_value: float, p: float) -> float:
|
|
47
|
+
"""
|
|
48
|
+
Combine a risky value with a risk-free value given proportion ``p``
|
|
49
|
+
allocated to the risk-free asset.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
value : float
|
|
54
|
+
Value from the risky portfolio (e.g., expected return or risk).
|
|
55
|
+
risk_free_value : float
|
|
56
|
+
Corresponding value for the risk-free asset (0 for risk, rf for
|
|
57
|
+
return).
|
|
58
|
+
p : float
|
|
59
|
+
Proportion allocated to the risk-free asset (0 to 1).
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
float
|
|
64
|
+
Weighted value.
|
|
65
|
+
"""
|
|
66
|
+
return p * risk_free_value + (1 - p) * value
|
|
67
|
+
|
|
68
|
+
def capital_allocation_line(self, steps: int = 21) -> list[dict]:
|
|
69
|
+
"""
|
|
70
|
+
Generate points along the Capital Allocation Line.
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
steps : int, optional
|
|
75
|
+
Number of allocation points (default 21, i.e., 0% to 100% in 5%
|
|
76
|
+
increments).
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
list of dict
|
|
81
|
+
Each dict has keys 'p' (risk-free proportion), 'expected_return',
|
|
82
|
+
'risk', and 'label'.
|
|
83
|
+
"""
|
|
84
|
+
proportions = [i / (steps - 1) for i in range(steps)]
|
|
85
|
+
points: list[dict] = []
|
|
86
|
+
|
|
87
|
+
for p in proportions:
|
|
88
|
+
expected_return = self.weigh_risk_free(self.portfolio[COL_RETURN], self.rf, p)
|
|
89
|
+
risk = self.weigh_risk_free(self.portfolio[COL_RISK], 0.0, p)
|
|
90
|
+
points.append(
|
|
91
|
+
{
|
|
92
|
+
"p": p,
|
|
93
|
+
"expected_return": expected_return,
|
|
94
|
+
"risk": risk,
|
|
95
|
+
"label": f"{p:.0%} risk-free",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return points
|
|
100
|
+
|
|
101
|
+
def final_allocation(self, p: float) -> dict[str, float]:
|
|
102
|
+
"""
|
|
103
|
+
Compute the final allocation weights given proportion ``p`` in the
|
|
104
|
+
risk-free asset.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
p : float
|
|
109
|
+
Proportion allocated to the risk-free asset (0 to 1).
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
dict[str, float]
|
|
114
|
+
Mapping of asset names (including 'Risk-Free' for risk-free) to
|
|
115
|
+
their allocation percentages.
|
|
116
|
+
"""
|
|
117
|
+
allocation: dict[str, float] = {COL_RISK_FREE: p}
|
|
118
|
+
for key, value in self.portfolio.items():
|
|
119
|
+
if key not in _PORTFOLIO_KEYS:
|
|
120
|
+
allocation[key] = (1 - p) * value
|
|
121
|
+
return allocation
|
|
122
|
+
|
|
123
|
+
def expected_returns(self, p: float) -> tuple[float, float]:
|
|
124
|
+
"""
|
|
125
|
+
Expected return and risk for a given allocation to the risk-free asset.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
p : float
|
|
130
|
+
Proportion allocated to risk-free (0 to 1).
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
tuple of (expected_return, risk)
|
|
135
|
+
"""
|
|
136
|
+
ret = self.weigh_risk_free(self.portfolio[COL_RETURN], self.rf, p)
|
|
137
|
+
ris = self.weigh_risk_free(self.portfolio[COL_RISK], 0.0, p)
|
|
138
|
+
return ret, ris
|