markowizard 0.1.0__tar.gz → 0.2.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 → markowizard-0.2.0}/PKG-INFO +13 -6
- {markowizard-0.1.0 → markowizard-0.2.0}/README.md +12 -5
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard/__init__.py +2 -2
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard.egg-info/PKG-INFO +13 -6
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard.egg-info/SOURCES.txt +1 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/pyproject.toml +5 -1
- markowizard-0.2.0/tests/test_api.py +101 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/LICENSE +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard/allocation.py +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard/core.py +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard/data.py +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard/visualization.py +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard.egg-info/dependency_links.txt +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard.egg-info/requires.txt +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/markowizard.egg-info/top_level.txt +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/setup.cfg +0 -0
- {markowizard-0.1.0 → markowizard-0.2.0}/tests/test_package.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markowizard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Markowitz portfolio optimization and analysis library
|
|
5
5
|
Author-email: GusFurtado <gustavofurtado2@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,12 +28,19 @@ Requires-Dist: yfinance>=0.1.63
|
|
|
28
28
|
Requires-Dist: plotly>=5.10
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/OutliersAnalytics/MarkoWizard/main/assets/banner.png" alt="MarkoWizard" width="640">
|
|
33
|
+
</p>
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
<p align="center">
|
|
36
|
+
<b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
|
|
37
|
+
</p>
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="https://pypi.org/project/markowizard/"><img src="https://img.shields.io/pypi/v/markowizard" alt="PyPI"></a>
|
|
41
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+"></a>
|
|
42
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
43
|
+
</p>
|
|
37
44
|
|
|
38
45
|
> **Previously known as _Diversificador_.** The original portfolio-analysis web app
|
|
39
46
|
> built with [Dash](https://dash.plotly.com/) is no longer maintained, but it is
|
|
@@ -44,7 +51,7 @@ A modern Python library for Markowitz portfolio optimization and analysis.
|
|
|
44
51
|
- **Markowitz Mean-Variance Optimization** — Compute the efficient frontier using `scipy.optimize`
|
|
45
52
|
- **Capital Allocation Line** — Mix risky portfolios with risk-free assets
|
|
46
53
|
- **Visualization** — Plotly-based charts for efficient frontier, allocation pie, CAL, correlation heatmaps, and price timelines
|
|
47
|
-
- **Data Fetching** —
|
|
54
|
+
- **Data Fetching** — Built-in helpers for downloading market data via yfinance
|
|
48
55
|
- **Web Application** — FastAPI backend with a dark-themed interactive frontend
|
|
49
56
|
|
|
50
57
|
## Installation
|
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/OutliersAnalytics/MarkoWizard/main/assets/banner.png" alt="MarkoWizard" width="640">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<p align="center">
|
|
6
|
+
<b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
|
|
7
|
+
</p>
|
|
4
8
|
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<a href="https://pypi.org/project/markowizard/"><img src="https://img.shields.io/pypi/v/markowizard" alt="PyPI"></a>
|
|
11
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+"></a>
|
|
12
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
13
|
+
</p>
|
|
7
14
|
|
|
8
15
|
> **Previously known as _Diversificador_.** The original portfolio-analysis web app
|
|
9
16
|
> built with [Dash](https://dash.plotly.com/) is no longer maintained, but it is
|
|
@@ -14,7 +21,7 @@ A modern Python library for Markowitz portfolio optimization and analysis.
|
|
|
14
21
|
- **Markowitz Mean-Variance Optimization** — Compute the efficient frontier using `scipy.optimize`
|
|
15
22
|
- **Capital Allocation Line** — Mix risky portfolios with risk-free assets
|
|
16
23
|
- **Visualization** — Plotly-based charts for efficient frontier, allocation pie, CAL, correlation heatmaps, and price timelines
|
|
17
|
-
- **Data Fetching** —
|
|
24
|
+
- **Data Fetching** — Built-in helpers for downloading market data via yfinance
|
|
18
25
|
- **Web Application** — FastAPI backend with a dark-themed interactive frontend
|
|
19
26
|
|
|
20
27
|
## Installation
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
markowizard: Markowitz portfolio optimization and analysis library.
|
|
3
3
|
|
|
4
4
|
Provides tools for mean-variance optimization, capital allocation,
|
|
5
|
-
visualization, and
|
|
5
|
+
visualization, and market data fetching.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from importlib.metadata import PackageNotFoundError, version
|
|
@@ -13,7 +13,7 @@ from markowizard.core import MarkowitzOptimizer
|
|
|
13
13
|
try:
|
|
14
14
|
__version__ = version("markowizard")
|
|
15
15
|
except PackageNotFoundError:
|
|
16
|
-
__version__ = "0.
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"CapitalAllocator",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markowizard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Markowitz portfolio optimization and analysis library
|
|
5
5
|
Author-email: GusFurtado <gustavofurtado2@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,12 +28,19 @@ Requires-Dist: yfinance>=0.1.63
|
|
|
28
28
|
Requires-Dist: plotly>=5.10
|
|
29
29
|
Dynamic: license-file
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
<p align="center">
|
|
32
|
+
<img src="https://raw.githubusercontent.com/OutliersAnalytics/MarkoWizard/main/assets/banner.png" alt="MarkoWizard" width="640">
|
|
33
|
+
</p>
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
<p align="center">
|
|
36
|
+
<b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
|
|
37
|
+
</p>
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
<p align="center">
|
|
40
|
+
<a href="https://pypi.org/project/markowizard/"><img src="https://img.shields.io/pypi/v/markowizard" alt="PyPI"></a>
|
|
41
|
+
<a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.10+-blue.svg" alt="Python 3.10+"></a>
|
|
42
|
+
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
43
|
+
</p>
|
|
37
44
|
|
|
38
45
|
> **Previously known as _Diversificador_.** The original portfolio-analysis web app
|
|
39
46
|
> built with [Dash](https://dash.plotly.com/) is no longer maintained, but it is
|
|
@@ -44,7 +51,7 @@ A modern Python library for Markowitz portfolio optimization and analysis.
|
|
|
44
51
|
- **Markowitz Mean-Variance Optimization** — Compute the efficient frontier using `scipy.optimize`
|
|
45
52
|
- **Capital Allocation Line** — Mix risky portfolios with risk-free assets
|
|
46
53
|
- **Visualization** — Plotly-based charts for efficient frontier, allocation pie, CAL, correlation heatmaps, and price timelines
|
|
47
|
-
- **Data Fetching** —
|
|
54
|
+
- **Data Fetching** — Built-in helpers for downloading market data via yfinance
|
|
48
55
|
- **Web Application** — FastAPI backend with a dark-themed interactive frontend
|
|
49
56
|
|
|
50
57
|
## Installation
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "markowizard"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Markowitz portfolio optimization and analysis library"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -37,6 +37,9 @@ dev = [
|
|
|
37
37
|
"pytest>=9.1.1",
|
|
38
38
|
"pytest-cov>=7.1.0",
|
|
39
39
|
"ruff>=0.15.20",
|
|
40
|
+
# For exercising the web service (backend/) in the test suite.
|
|
41
|
+
"fastapi>=0.110.0",
|
|
42
|
+
"httpx>=0.27.0",
|
|
40
43
|
]
|
|
41
44
|
|
|
42
45
|
[project.urls]
|
|
@@ -52,6 +55,7 @@ include = ["markowizard*"]
|
|
|
52
55
|
testpaths = ["tests"]
|
|
53
56
|
python_files = ["test_*.py"]
|
|
54
57
|
addopts = "-v --tb=short"
|
|
58
|
+
pythonpath = ["."] # so tests can `import backend` (the web app is not installed)
|
|
55
59
|
|
|
56
60
|
[tool.ruff]
|
|
57
61
|
target-version = "py310"
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Tests for the FastAPI web service (`backend/`).
|
|
2
|
+
|
|
3
|
+
The market-data fetch is mocked, so these run offline; everything downstream
|
|
4
|
+
of the fetch (returns, optimization, CAL, serialization) exercises the real code.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import pytest
|
|
12
|
+
from fastapi.testclient import TestClient
|
|
13
|
+
|
|
14
|
+
from backend.main import app
|
|
15
|
+
from markowizard.data import compute_monthly_returns
|
|
16
|
+
|
|
17
|
+
TICKERS = ["AAA", "BBB", "CCC"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.fixture
|
|
21
|
+
def client() -> TestClient:
|
|
22
|
+
return TestClient(app)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def fake_prices() -> pd.DataFrame:
|
|
27
|
+
"""~3 years of synthetic daily close prices for three tickers."""
|
|
28
|
+
rng = np.random.default_rng(0)
|
|
29
|
+
dates = pd.date_range("2021-01-01", periods=780, freq="B")
|
|
30
|
+
steps = rng.normal(loc=0.0004, scale=0.012, size=(len(dates), len(TICKERS)))
|
|
31
|
+
prices = 100 * np.exp(np.cumsum(steps, axis=0))
|
|
32
|
+
return pd.DataFrame(prices, index=dates, columns=TICKERS)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_health(client: TestClient) -> None:
|
|
36
|
+
resp = client.get("/api/health")
|
|
37
|
+
assert resp.status_code == 200
|
|
38
|
+
assert resp.json() == {"status": "ok"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_root_serves_frontend(client: TestClient) -> None:
|
|
42
|
+
resp = client.get("/")
|
|
43
|
+
assert resp.status_code == 200
|
|
44
|
+
assert "text/html" in resp.headers["content-type"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_analyze_happy_path(client: TestClient, fake_prices: pd.DataFrame) -> None:
|
|
48
|
+
with patch("backend.router.fetch_prices", return_value=fake_prices) as fetch:
|
|
49
|
+
resp = client.post(
|
|
50
|
+
"/api/analyze",
|
|
51
|
+
json={"tickers": TICKERS, "period": "3y", "risk_free_rate": 0.004},
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
fetch.assert_called_once()
|
|
55
|
+
assert resp.status_code == 200
|
|
56
|
+
body = resp.json()
|
|
57
|
+
|
|
58
|
+
assert body["tickers"] == TICKERS
|
|
59
|
+
assert len(body["efficient_frontier"]) == 100
|
|
60
|
+
assert set(body["max_sharpe_portfolio"]["weights"]) == set(TICKERS)
|
|
61
|
+
assert body["max_sharpe_portfolio"]["sharpe"] is not None
|
|
62
|
+
assert len(body["capital_allocation_line"]) == 21
|
|
63
|
+
assert np.allclose(np.array(body["correlation_matrix"]).shape, (3, 3))
|
|
64
|
+
|
|
65
|
+
# Minimum-variance portfolio is the lowest-risk point on the frontier.
|
|
66
|
+
min_var = body["min_variance_portfolio"]
|
|
67
|
+
assert set(min_var["weights"]) == set(TICKERS)
|
|
68
|
+
assert min_var["risk"] == min(p["risk"] for p in body["efficient_frontier"])
|
|
69
|
+
assert min_var == body["efficient_frontier"][0]
|
|
70
|
+
|
|
71
|
+
# Per-asset standalone statistics, independent of any portfolio weighting,
|
|
72
|
+
# should match mean/std of the same monthly returns the optimizer used.
|
|
73
|
+
monthly_returns = compute_monthly_returns(fake_prices)
|
|
74
|
+
stats_by_ticker = {s["ticker"]: s for s in body["asset_statistics"]}
|
|
75
|
+
assert set(stats_by_ticker) == set(TICKERS)
|
|
76
|
+
for ticker in TICKERS:
|
|
77
|
+
stats = stats_by_ticker[ticker]
|
|
78
|
+
assert stats["expected_return"] == pytest.approx(monthly_returns[ticker].mean())
|
|
79
|
+
assert stats["volatility"] == pytest.approx(monthly_returns[ticker].std())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_analyze_reports_missing_tickers(client: TestClient, fake_prices: pd.DataFrame) -> None:
|
|
83
|
+
partial = fake_prices.drop(columns=["CCC"])
|
|
84
|
+
with patch("backend.router.fetch_prices", return_value=partial):
|
|
85
|
+
resp = client.post("/api/analyze", json={"tickers": TICKERS})
|
|
86
|
+
|
|
87
|
+
assert resp.status_code == 400
|
|
88
|
+
assert "CCC" in resp.json()["detail"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_analyze_rejects_empty_tickers(client: TestClient) -> None:
|
|
92
|
+
resp = client.post("/api/analyze", json={"tickers": []})
|
|
93
|
+
assert resp.status_code == 422
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_analyze_wraps_fetch_errors(client: TestClient) -> None:
|
|
97
|
+
with patch("backend.router.fetch_prices", side_effect=RuntimeError("yfinance down")):
|
|
98
|
+
resp = client.post("/api/analyze", json={"tickers": TICKERS})
|
|
99
|
+
|
|
100
|
+
assert resp.status_code == 500
|
|
101
|
+
assert "yfinance down" in resp.json()["detail"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|