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.
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
36
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](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