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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markowizard
3
- Version: 0.1.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
- # MarkoWizard
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
- A modern Python library for Markowitz portfolio optimization and analysis.
35
+ <p align="center">
36
+ <b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
37
+ </p>
34
38
 
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/)
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** — Optional convenience functions for downloading market data via yfinance
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
- # MarkoWizard
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
- A modern Python library for Markowitz portfolio optimization and analysis.
5
+ <p align="center">
6
+ <b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
7
+ </p>
4
8
 
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/)
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** — Optional convenience functions for downloading market data via yfinance
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 optional market data fetching.
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.1.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.1.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
- # MarkoWizard
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
- A modern Python library for Markowitz portfolio optimization and analysis.
35
+ <p align="center">
36
+ <b>A modern Python library for Markowitz portfolio optimization and analysis.</b>
37
+ </p>
34
38
 
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/)
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** — Optional convenience functions for downloading market data via yfinance
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
@@ -11,4 +11,5 @@ markowizard.egg-info/SOURCES.txt
11
11
  markowizard.egg-info/dependency_links.txt
12
12
  markowizard.egg-info/requires.txt
13
13
  markowizard.egg-info/top_level.txt
14
+ tests/test_api.py
14
15
  tests/test_package.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "markowizard"
7
- version = "0.1.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