diff --git a/.github/actions/setup-project/action.yml b/.github/actions/setup-project/action.yml deleted file mode 100644 index 21b22333..00000000 --- a/.github/actions/setup-project/action.yml +++ /dev/null @@ -1,81 +0,0 @@ -# Action: Setup Project (composite action) -# -# Purpose: Bootstrap a Python project within GitHub Actions by: -# - Installing uv and uvx into a local ./bin directory and adding it to PATH -# - Detecting the presence of pyproject.toml and exposing that as an output -# - Creating a virtual environment with uv and (optionally) syncing dependencies -# -# Inputs: -# - python-version: Python version for the uv-managed virtual environment (default: 3.12) -# -# Outputs: -# - pyproject_exists: "true" if pyproject.toml exists, otherwise "false" -# -# Notes: -# - Safe to run in repositories without pyproject.toml; dependency sync will be skipped. -# - Purely a CI helper — it does not modify repository files. - -name: 'Setup Project' -description: 'Setup the project' - -inputs: - python-version: - description: 'Python version to use' - required: false - default: '3.12' - -outputs: - pyproject_exists: - description: 'Flag indicating whether pyproject.toml exists' - value: ${{ steps.check_pyproject.outputs.exists }} - -runs: - using: 'composite' - steps: - - name: Set up uv, uvx and the venv - shell: bash - run: | - mkdir -p bin - - # Add ./bin to the PATH - echo "Adding ./bin to PATH" - echo "$(pwd)/bin" >> $GITHUB_PATH - - # Install uv and uvx - curl -fsSL https://astral.sh/uv/install.sh | UV_INSTALL_DIR="./bin" sh - - - name: Check version for uv - shell: bash - run: | - uv --version - - - name: Check for pyproject.toml - id: check_pyproject - shell: bash - run: | - if [ -f "pyproject.toml" ]; then - echo "exists=true" >> "$GITHUB_OUTPUT" - else - echo "exists=false" >> "$GITHUB_OUTPUT" - fi - - - name: Build the virtual environment - shell: bash - run: uv venv --python ${{ inputs.python-version }} - - - name: "Sync the virtual environment for ${{ github.repository }} if pyproject.toml exists" - shell: bash - run: | - if [ -f "pyproject.toml" ]; then - uv sync --all-extras - else - echo "No pyproject.toml found, skipping package installation" - fi - - - name: Show dependencies - shell: bash - run: uv pip list - - - name: Show Python version - shell: bash - run: uv run python -c "import sys; print(sys.version)" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6cc45e03..78f1fe19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,13 +1,12 @@ -# This workflow will install Python dependencies, run tests and lint with a variety of Python versions -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python - -name: pytest +name: Install and test on: push: - branches: ["main"] + branches: + - main pull_request: - branches: ["main"] + branches: + - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -15,22 +14,23 @@ concurrency: jobs: code-quality: + name: code-quality runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 + - name: repository checkout step + uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - python-version: '3.14' + enable-cache: true + python-version: "3.14" - - name: install pre-commit - run: python3 -m pip install pre-commit + - name: Create virtual environment + run: uv venv - - name: Checkout code - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - name: install pre-commit + run: uv pip install pre-commit - name: Get changed files id: changed-files @@ -40,7 +40,7 @@ jobs: - name: Print changed files run: | - echo "Changed files: $CHANGED_FILES" + echo "Changed files:" && echo "$CHANGED_FILES" | tr ' ' '\n' - name: Run pre-commit on changed files run: | @@ -50,17 +50,42 @@ jobs: echo "No changed files to check." fi - pytest-nosoftdeps: + detect-notebooks-change: needs: code-quality - name: nosoftdeps (${{ matrix.python-version }}, ${{ matrix.os }}) - runs-on: ${{ matrix.os }} - env: - MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 + name: detect change affecting notebooks + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + notebooks: ${{ steps.check.outputs.notebooks }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Fetch main branch + run: git fetch origin main + + - name: Check if cookbook, pypfopt or pyproject.toml changed + id: check + run: | + if git diff --quiet origin/main -- cookbook/ pypfopt/ pyproject.toml; then + echo "No notebook related changes" + echo "notebooks=false" >> $GITHUB_OUTPUT + else + echo "Detected changes in notebooks or pypfopt" + echo "notebooks=true" >> $GITHUB_OUTPUT + fi + + run-notebook-examples: + needs: detect-notebooks-change + if: ${{ needs.detect-notebooks-change.outputs.notebooks == 'true' }} + runs-on: ubuntu-latest + strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - fail-fast: false # to not fail all combinations if just one fails + fail-fast: false steps: - uses: actions/checkout@v6 @@ -69,39 +94,42 @@ jobs: uses: astral-sh/setup-uv@v7 with: enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: python-version: ${{ matrix.python-version }} - name: Display Python version run: python -c "import sys; print(sys.version)" + - name: Create virtual environment + run: uv venv + - name: Install dependencies shell: bash - run: uv pip install ".[dev]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 + run: uv pip install ".[all_extras,notebook_test]" --group dev --no-cache - name: Show dependencies run: uv pip list - - name: Test with pytest + - name: Collect notebooks + id: notebooks + shell: bash + run: | + NOTEBOOKS=$(find cookbook -name '*.ipynb' -print0 | xargs -0 echo) + echo "notebooks=$NOTEBOOKS" >> $GITHUB_OUTPUT + + - name: Run notebooks + shell: bash run: | - pytest ./tests + uv run pytest --reruns 3 --nbmake --nbmake-timeout=3600 -vv ${{ steps.notebooks.outputs.notebooks }} - pytest: - needs: pytest-nosoftdeps - name: (${{ matrix.python-version }}, ${{ matrix.os }}) + test-nosoftdeps: + needs: code-quality + name: test-nosoftdeps (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} - env: - MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - fail-fast: false # to not fail all combinations if just one fails steps: - uses: actions/checkout@v6 @@ -110,38 +138,38 @@ jobs: uses: astral-sh/setup-uv@v7 with: enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: python-version: ${{ matrix.python-version }} + - name: Create virtual environment + run: uv venv + - name: Display Python version run: python -c "import sys; print(sys.version)" + - name: Force non-GUI Matplotlib backend (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh + run: echo "MPLBACKEND=Agg" >> $env:GITHUB_ENV + - name: Install dependencies shell: bash - run: uv pip install ".[dev,all_extras]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 + run: uv pip install "." --group dev --no-cache - name: Show dependencies run: uv pip list - - name: Test with pytest - run: | - pytest ./tests + - name: Run tests + run: uv run pytest ./tests - codecov: - name: coverage (${{ matrix.python-version }} on ${{ matrix.os }} + test-full: + needs: test-nosoftdeps + name: test-full (${{ matrix.python-version }}, ${{ matrix.os }}) runs-on: ${{ matrix.os }} - needs: code-quality - env: - MPLBACKEND: Agg # https://github.com/orgs/community/discussions/26434 strategy: + fail-fast: false matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ["3.12"] steps: - uses: actions/checkout@v6 @@ -150,28 +178,64 @@ jobs: uses: astral-sh/setup-uv@v7 with: enable-cache: true + python-version: ${{ matrix.python-version }} + + - name: Create virtual environment + run: uv venv + + - name: Display Python version + run: python -c "import sys; print(sys.version)" + + - name: Force non-GUI Matplotlib backend (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh + run: echo "MPLBACKEND=Agg" >> $env:GITHUB_ENV + + - name: Install dependencies + shell: bash + run: uv pip install -e ".[all_extras]" --group dev --no-cache - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - name: Show dependencies + run: uv pip list + + - name: Run tests + run: uv run pytest ./tests + + # TODO: should we run this as a substep of test-no-deps and only upload for a specific version? + codecov: + name: coverage + needs: code-quality + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - python-version: ${{ matrix.python-version }} + enable-cache: true + python-version: 3.12 + + - name: Create virtual environment + run: uv venv - name: Display Python version run: python -c "import sys; print(sys.version)" + - name: Force non-GUI Matplotlib backend (Windows) + if: ${{ matrix.os == 'windows-latest' }} + shell: pwsh + run: echo "MPLBACKEND=Agg" >> $env:GITHUB_ENV + - name: Install dependencies shell: bash - run: uv pip install ".[dev,all_extras]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 + run: uv pip install . --group dev --group cov - name: Show dependencies run: uv pip list - name: Generate coverage report run: | - pip install pytest pytest-cov - pytest --cov=./ --cov-report=xml + uv run pytest --cov=./ --cov-report=xml - name: Upload coverage to Codecov # if false in order to skip for now @@ -180,51 +244,3 @@ jobs: with: files: ./coverage.xml fail_ci_if_error: true - - notebooks: - needs: code-quality - runs-on: ubuntu-latest - - strategy: - matrix: - python-version: [ '3.10', '3.11', '3.12', '3.13', '3.14' ] - fail-fast: false - - steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - - name: Install dependencies - shell: bash - run: uv pip install ".[dev,all_extras,notebook_test]" --no-cache-dir - env: - UV_SYSTEM_PYTHON: 1 - - - name: Show dependencies - run: uv pip list - - # Discover all notebooks - - name: Collect notebooks - id: notebooks - shell: bash - run: | - NOTEBOOKS=$(find cookbook -name '*.ipynb' -print0 | xargs -0 echo) - echo "notebooks=$NOTEBOOKS" >> $GITHUB_OUTPUT - - # Run all discovered notebooks with nbmake - - name: Test notebooks - shell: bash - run: | - uv run pytest --reruns 3 --nbmake --nbmake-timeout=3600 -vv ${{ steps.notebooks.outputs.notebooks }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6b50c32..f27da42d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: PyPI Release +name: Build wheels and publish to PyPI on: release: @@ -14,7 +14,7 @@ jobs: - uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: "3.11" - shell: bash run: | @@ -32,21 +32,24 @@ jobs: fi build_wheels: + needs: check_tag name: Build wheels runs-on: ubuntu-latest - needs: [check_tag] steps: - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 with: - python-version: '3.11' + enable-cache: true + python-version: "3.11" - name: Build wheel run: | - python -m pip install build - python -m build --wheel --sdist --outdir wheelhouse + uv build --wheel --sdist --out-dir wheelhouse + env: + UV_SYSTEM_PYTHON: 1 - name: Store wheels uses: actions/upload-artifact@v7 @@ -54,50 +57,49 @@ jobs: name: wheels path: wheelhouse/* - pytest-nosoftdeps: - name: no-softdeps + test_wheels: + needs: build_wheels + name: Test wheels on ${{ matrix.os }} with ${{ matrix.python-version }} runs-on: ${{ matrix.os }} - needs: [build_wheels] strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [windows-latest, ubuntu-latest, macos-latest] python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + - uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - - name: Setup macOS - if: runner.os == 'macOS' - run: | - brew install libomp # https://github.com/pytorch/pytorch/issues/20030 + - uses: actions/download-artifact@v7 + with: + name: wheels + path: wheelhouse - - name: Get full Python version - id: full-python-version - shell: bash - run: echo version=$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") >> $GITHUB_OUTPUT + - name: Display downloaded artifacts + run: ls -l wheelhouse - - name: Install dependencies - shell: bash - run: | - pip install ".[dev]" + - name: Get wheel filename (Unix) + if: runner.os != 'Windows' + run: echo "WHEELNAME=$(ls ./wheelhouse/pyportfolioopt-*none-any.whl)" >> $GITHUB_ENV + + - name: Get wheel filename (Windows) + if: runner.os == 'Windows' + run: echo "WHEELNAME=$(ls ./wheelhouse/pyportfolioopt-*none-any.whl)" >> $env:GITHUB_ENV - - name: Show dependencies - run: python -m pip list + - name: Install wheel and extras + run: python3 -m pip install "${{ env.WHEELNAME }}[all_extras]" --group dev - - name: Run pytest - shell: bash + - name: Run tests run: python -m pytest tests upload_wheels: name: Upload wheels to PyPI runs-on: ubuntu-latest - needs: [pytest-nosoftdeps] + needs: [build_wheels, test_wheels] permissions: id-token: write diff --git a/.gitignore b/.gitignore index 4b974123..41862b80 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ dist artifacts bin + +# uv +uv.lock diff --git a/Makefile b/Makefile index dd22c0e0..2c577f98 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,7 @@ RESET := \033[0m UV_INSTALL_DIR := ./bin +# TODO: I don't think we should install uv locally inside the repository, but rather rely on the user having it installed globally. This is because uv is a tool that is meant to be used across multiple projects, and installing it locally in each project can lead to version conflicts and unnecessary duplication. Instead, we can specify in the documentation that users should have uv installed globally, and provide instructions on how to do so if they don't already have it ##@ Bootstrap install-uv: ## ensure uv (and uvx) are installed locally @mkdir -p ${UV_INSTALL_DIR} diff --git a/cookbook/2-Mean-Variance-Optimisation.ipynb b/cookbook/2-Mean-Variance-Optimisation.ipynb index 7aba6f28..47808068 100644 --- a/cookbook/2-Mean-Variance-Optimisation.ipynb +++ b/cookbook/2-Mean-Variance-Optimisation.ipynb @@ -44,189 +44,116 @@ }, { "cell_type": "code", + "execution_count": 1, "metadata": { + "ExecuteTime": { + "end_time": "2025-11-12T08:10:54.267009Z", + "start_time": "2025-11-12T08:10:53.573242Z" + }, "colab": { "base_uri": "https://localhost:8080/" }, "id": "69vaYVwZ-Cxz", - "outputId": "dee1bf4a-82ee-4909-e26b-217899258cf2", - "ExecuteTime": { - "end_time": "2025-11-12T08:10:54.267009Z", - "start_time": "2025-11-12T08:10:53.573242Z" - } + "outputId": "dee1bf4a-82ee-4909-e26b-217899258cf2" }, - "source": [ - "!pip install pandas numpy matplotlib yfinance PyPortfolioOpt\n", - "import os\n", - "if not os.path.isdir('data'):\n", - " os.system('git clone https://github.com/pyportfolio/pyportfolioopt.git')\n", - " os.chdir('PyPortfolioOpt/cookbook')" - ], "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Requirement already satisfied: pandas in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (2.3.3)\r\n", - "Requirement already satisfied: numpy in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (2.3.4)\r\n", - "Requirement already satisfied: matplotlib in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (3.10.7)\r\n", - "Requirement already satisfied: yfinance in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (0.2.66)\r\n", - "Requirement already satisfied: PyPortfolioOpt in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (1.5.6)\r\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from pandas) (2.9.0.post0)\r\n", - "Requirement already satisfied: pytz>=2020.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from pandas) (2025.2)\r\n", - "Requirement already satisfied: tzdata>=2022.7 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from pandas) (2025.2)\r\n", - "Requirement already satisfied: contourpy>=1.0.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (1.3.3)\r\n", - "Requirement already satisfied: cycler>=0.10 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (0.12.1)\r\n", - "Requirement already satisfied: fonttools>=4.22.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (4.60.1)\r\n", - "Requirement already satisfied: kiwisolver>=1.3.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (1.4.9)\r\n", - "Requirement already satisfied: packaging>=20.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (25.0)\r\n", - "Requirement already satisfied: pillow>=8 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (12.0.0)\r\n", - "Requirement already satisfied: pyparsing>=3 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from matplotlib) (3.2.5)\r\n", - "Requirement already satisfied: requests>=2.31 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (2.32.5)\r\n", - "Requirement already satisfied: multitasking>=0.0.7 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (0.0.12)\r\n", - "Requirement already satisfied: platformdirs>=2.0.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (4.5.0)\r\n", - "Requirement already satisfied: frozendict>=2.3.4 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (2.4.7)\r\n", - "Requirement already satisfied: peewee>=3.16.2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (3.18.3)\r\n", - "Requirement already satisfied: beautifulsoup4>=4.11.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (4.14.2)\r\n", - "Requirement already satisfied: curl_cffi>=0.7 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (0.13.0)\r\n", - "Requirement already satisfied: protobuf>=3.19.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (6.33.0)\r\n", - "Requirement already satisfied: websockets>=13.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from yfinance) (15.0.1)\r\n", - "Requirement already satisfied: cvxpy>=1.1.19 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from PyPortfolioOpt) (1.7.3)\r\n", - "Requirement already satisfied: scikit-learn>=0.24.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from PyPortfolioOpt) (1.7.2)\r\n", - "Requirement already satisfied: scipy>=1.3.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from PyPortfolioOpt) (1.16.3)\r\n", - "Requirement already satisfied: soupsieve>1.2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from beautifulsoup4>=4.11.1->yfinance) (2.8)\r\n", - "Requirement already satisfied: typing-extensions>=4.0.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from beautifulsoup4>=4.11.1->yfinance) (4.15.0)\r\n", - "Requirement already satisfied: cffi>=1.12.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from curl_cffi>=0.7->yfinance) (2.0.0)\r\n", - "Requirement already satisfied: certifi>=2024.2.2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from curl_cffi>=0.7->yfinance) (2025.11.12)\r\n", - "Requirement already satisfied: osqp>=1.0.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from cvxpy>=1.1.19->PyPortfolioOpt) (1.0.5)\r\n", - "Requirement already satisfied: clarabel>=0.5.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from cvxpy>=1.1.19->PyPortfolioOpt) (0.11.1)\r\n", - "Requirement already satisfied: scs>=3.2.4.post1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from cvxpy>=1.1.19->PyPortfolioOpt) (3.2.9)\r\n", - "Requirement already satisfied: six>=1.5 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)\r\n", - "Requirement already satisfied: charset_normalizer<4,>=2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from requests>=2.31->yfinance) (3.4.4)\r\n", - "Requirement already satisfied: idna<4,>=2.5 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from requests>=2.31->yfinance) (3.11)\r\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from requests>=2.31->yfinance) (2.5.0)\r\n", - "Requirement already satisfied: joblib>=1.2.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from scikit-learn>=0.24.1->PyPortfolioOpt) (1.5.2)\r\n", - "Requirement already satisfied: threadpoolctl>=3.1.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from scikit-learn>=0.24.1->PyPortfolioOpt) (3.6.0)\r\n", - "Requirement already satisfied: pycparser in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from cffi>=1.12.0->curl_cffi>=0.7->yfinance) (2.23)\r\n", - "Requirement already satisfied: jinja2 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from osqp>=1.0.0->cvxpy>=1.1.19->PyPortfolioOpt) (3.1.6)\r\n", - "Requirement already satisfied: setuptools in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from osqp>=1.0.0->cvxpy>=1.1.19->PyPortfolioOpt) (80.9.0)\r\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /Users/thomasschmelzer/projects/PyPortfolioOpt/.venv/lib/python3.12/site-packages (from jinja2->osqp>=1.0.0->cvxpy>=1.1.19->PyPortfolioOpt) (3.0.3)\r\n", - "\r\n", - "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m A new release of pip is available: \u001B[0m\u001B[31;49m24.3.1\u001B[0m\u001B[39;49m -> \u001B[0m\u001B[32;49m25.3\u001B[0m\r\n", - "\u001B[1m[\u001B[0m\u001B[34;49mnotice\u001B[0m\u001B[1;39;49m]\u001B[0m\u001B[39;49m To update, run: \u001B[0m\u001B[32;49mpip install --upgrade pip\u001B[0m\r\n" + "\u001b[2mUsing Python 3.12.11 environment at: /home/tingiskhan/python/pyportfolioopt/.venv\u001b[0m\n", + "\u001b[2mAudited \u001b[1m6 packages\u001b[0m \u001b[2min 3ms\u001b[0m\u001b[0m\n" ] } ], - "execution_count": 7 + "source": [ + "!pip install pandas numpy matplotlib yfinance PyPortfolioOpt plotly\n", + "import os\n", + "if not os.path.isdir('data'):\n", + " os.system('git clone https://github.com/pyportfolio/pyportfolioopt.git')\n", + " os.chdir('PyPortfolioOpt/cookbook')" + ] }, { "cell_type": "code", + "execution_count": 2, "metadata": { - "id": "shuJGGeo9ny8", "ExecuteTime": { "end_time": "2025-11-12T08:10:54.280850Z", "start_time": "2025-11-12T08:10:54.278023Z" - } + }, + "id": "shuJGGeo9ny8" }, + "outputs": [], "source": [ "import yfinance as yf\n", "import matplotlib.pyplot as plt\n", "import pandas as pd\n", "import numpy as np" - ], - "outputs": [], - "execution_count": 8 + ] }, { "cell_type": "code", + "execution_count": 3, "metadata": { - "id": "c4r8LJdC9ny8", "ExecuteTime": { "end_time": "2025-11-12T08:10:54.304666Z", "start_time": "2025-11-12T08:10:54.301956Z" - } + }, + "id": "c4r8LJdC9ny8" }, + "outputs": [], "source": [ "tickers = [\"MSFT\", \"AMZN\", \"KO\", \"MA\", \"COST\", \n", " \"LUV\", \"XOM\", \"PFE\", \"JPM\", \"UNH\", \n", " \"ACN\", \"DIS\", \"GILD\", \"F\", \"TSLA\"] " - ], - "outputs": [], - "execution_count": 9 + ] }, { "cell_type": "code", + "execution_count": 4, "metadata": { + "ExecuteTime": { + "end_time": "2025-11-12T08:10:56.505627Z", + "start_time": "2025-11-12T08:10:54.345590Z" + }, "colab": { "base_uri": "https://localhost:8080/" }, "id": "XWdfXpB69ny9", - "outputId": "e60980bf-21e3-470f-a4f7-99da82c820a4", - "ExecuteTime": { - "end_time": "2025-11-12T08:10:56.505627Z", - "start_time": "2025-11-12T08:10:54.345590Z" - } + "outputId": "e60980bf-21e3-470f-a4f7-99da82c820a4" }, - "source": "ohlc = yf.download(tickers, period=\"max\").loc[\"1990\":]", "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "/var/folders/_3/k_9k5d5n5zz57w7qfll9rzs40000gn/T/ipykernel_59786/3860718606.py:1: FutureWarning: YF.download() has changed argument auto_adjust default to True\n", - " ohlc = yf.download(tickers, period=\"max\").loc[\"1990\":]\n", "[*********************100%***********************] 15 of 15 completed\n" ] } ], - "execution_count": 10 + "source": [ + "ohlc = yf.download(tickers, period=\"max\").loc[\"1990\":]" + ] }, { "cell_type": "code", + "execution_count": 5, "metadata": { + "ExecuteTime": { + "end_time": "2025-11-12T08:10:56.559652Z", + "start_time": "2025-11-12T08:10:56.550372Z" + }, "colab": { "base_uri": "https://localhost:8080/", "height": 388 }, "id": "S7voZG_T9ny-", - "outputId": "de26041a-af6d-4554-eb32-1eb9d57d5f3e", - "ExecuteTime": { - "end_time": "2025-11-12T08:10:56.559652Z", - "start_time": "2025-11-12T08:10:56.550372Z" - } + "outputId": "de26041a-af6d-4554-eb32-1eb9d57d5f3e" }, - "source": [ - "prices = ohlc[\"Close\"].dropna(how=\"all\")\n", - "prices.tail()" - ], "outputs": [ { "data": { - "text/plain": [ - "Ticker ACN AMZN COST DIS F \\\n", - "Date \n", - "2025-11-05 247.820007 250.199997 935.030029 111.360001 12.960114 \n", - "2025-11-06 241.339996 243.039993 923.580017 110.489998 12.969999 \n", - "2025-11-07 245.759995 244.410004 922.739990 110.739998 13.210000 \n", - "2025-11-10 244.550003 248.399994 915.559998 112.239998 13.160000 \n", - "2025-11-11 242.559998 249.100006 913.859985 114.849998 13.300000 \n", - "\n", - "Ticker GILD JPM KO LUV MA \\\n", - "Date \n", - "2025-11-05 122.580002 311.679993 68.510002 31.620001 553.309998 \n", - "2025-11-06 123.400002 313.420013 69.059998 31.510000 553.280029 \n", - "2025-11-07 118.839996 314.209991 70.550003 32.450001 551.969971 \n", - "2025-11-10 118.150002 316.890015 70.519997 32.660000 552.960022 \n", - "2025-11-11 122.559998 315.619995 71.610001 31.990000 558.349976 \n", - "\n", - "Ticker MSFT PFE TSLA UNH XOM \n", - "Date \n", - "2025-11-05 507.160004 24.184153 462.070007 327.739990 113.680000 \n", - "2025-11-06 497.100006 24.420000 445.910004 321.559998 114.500000 \n", - "2025-11-07 496.820007 24.430000 429.519989 324.209991 117.220001 \n", - "2025-11-10 506.000000 24.389999 445.230011 321.579987 118.220001 \n", - "2025-11-11 508.679993 25.510000 439.619995 327.450012 119.779999 " - ], "text/html": [ "
90&&i.log(\"Long binary search...\"),f-1},e.sorterAsc=function(t,e){return t-e},e.sorterDes=function(t,e){return e-t},e.distinctVals=function(t){var r,n=t.slice();for(n.sort(e.sorterAsc),r=n.length-1;r>-1&&n[r]===o;r--);for(var i,a=n[r]-n[0]||1,s=a/(r||1)/1e4,l=[],c=0;c<=r;c++){var u=n[c],h=u-i;void 0===i?(l.push(u),i=u):h>s&&(a=Math.min(a,h),l.push(u),i=u)}return{vals:l,minDiff:a}},e.roundUp=function(t,e,r){for(var n,i=0,a=e.length-1,o=0,s=r?0:1,l=r?1:0,c=r?Math.ceil:Math.floor;i0&&(n=1),r&&n)return t.sort(e)}return n?t:t.reverse()},e.findIndexOfMin=function(t,e){e=e||a;for(var r,n=1/0,i=0;i
/i;e.BR_TAG_ALL=/
/gi;var b=/(^|[\\s\"'])style\\s*=\\s*(\"([^\"]*);?\"|'([^']*);?')/i,w=/(^|[\\s\"'])href\\s*=\\s*(\"([^\"]*)\"|'([^']*)')/i,T=/(^|[\\s\"'])target\\s*=\\s*(\"([^\"\\s]*)\"|'([^'\\s]*)')/i,k=/(^|[\\s\"'])popup\\s*=\\s*(\"([\\w=,]*)\"|'([\\w=,]*)')/i;function A(t,e){if(!t)return null;var r=t.match(e),n=r&&(r[3]||r[4]);return n&&C(n)}var M=/(^|;)\\s*color:/;e.plainText=function(t,e){for(var r=void 0!==(e=e||{}).len&&-1!==e.len?e.len:1/0,n=void 0!==e.allowedTags?e.allowedTags:[\"br\"],i=t.split(v),a=[],o=\"\",s=0,l=0;l
\"+l;e.text=c}(t,o,r,c):\"log\"===u?function(t,e,r,n,a){var o=t.dtick,l=e.x,c=t.tickformat,u=\"string\"==typeof o&&o.charAt(0);if(\"never\"===a&&(a=\"\"),n&&\"L\"!==u&&(o=\"L3\",u=\"L\"),c||\"L\"===u)e.text=wt(Math.pow(10,l),t,a,n);else if(i(o)||\"D\"===u&&s.mod(l+.01,1)<.1){var h=Math.round(l),f=Math.abs(h),p=t.exponentformat;\"power\"===p||_t(p)&&bt(h)?(e.text=0===h?1:1===h?\"10\":\"10\"+(h>1?\"\":z)+f+\"\",e.fontSize*=1.25):(\"e\"===p||\"E\"===p)&&f>2?e.text=\"1\"+p+(h>0?\"+\":z)+f:(e.text=wt(Math.pow(10,l),t,\"\",\"fakehover\"),\"D1\"===o&&\"y\"===t._id.charAt(0)&&(e.dy-=e.fontSize/6))}else{if(\"D\"!==u)throw\"unrecognized dtick \"+String(o);e.text=String(Math.round(Math.pow(10,s.mod(l,1)))),e.fontSize*=.75}if(\"D1\"===t.dtick){var d=String(e.text).charAt(0);\"0\"!==d&&\"1\"!==d||(\"y\"===t._id.charAt(0)?e.dx-=e.fontSize/4:(e.dy+=e.fontSize/2,e.dx+=(t.range[1]>t.range[0]?1:-1)*e.fontSize*(l<0?.5:.25)))}}(t,o,0,c,g):\"category\"===u?function(t,e){var r=t._categories[Math.round(e.x)];void 0===r&&(r=\"\"),e.text=String(r)}(t,o):\"multicategory\"===u?function(t,e,r){var n=Math.round(e.x),i=t._categories[n]||[],a=void 0===i[1]?\"\":String(i[1]),o=void 0===i[0]?\"\":String(i[0]);r?e.text=o+\" - \"+a:(e.text=a,e.text2=o)}(t,o,r):Rt(t)?function(t,e,r,n,i){if(\"radians\"!==t.thetaunit||r)e.text=wt(e.x,t,i,n);else{var a=e.x/180;if(0===a)e.text=\"0\";else{var o=function(t){function e(t,e){return Math.abs(t-e)<=1e-6}var r=function(t){for(var r=1;!e(Math.round(t*r)/r,t);)r*=10;return r}(t),n=t*r,i=Math.abs(function t(r,n){return e(n,0)?r:t(n,r%n)}(n,r));return[Math.round(n/i),Math.round(r/i)]}(a);if(o[1]>=100)e.text=wt(s.deg2rad(e.x),t,i,n);else{var l=e.x<0;1===o[1]?1===o[0]?e.text=\"π\":e.text=o[0]+\"π\":e.text=[\"\",o[0],\"\",\"⁄\",\"\",o[1],\"\",\"π\"].join(\"\"),l&&(e.text=z+e.text)}}}}(t,o,r,c,g):function(t,e,r,n,i){\"never\"===i?i=\"\":\"all\"===t.showexponent&&Math.abs(e.x/t.dtick)<1e-6&&(i=\"hide\"),e.text=wt(e.x,t,i,n)}(t,o,0,c,g),n||(t.tickprefix&&!m(t.showtickprefix)&&(o.text=t.tickprefix+o.text),t.ticksuffix&&!m(t.showticksuffix)&&(o.text+=t.ticksuffix)),t.labelalias&&t.labelalias.hasOwnProperty(o.text)){var y=t.labelalias[o.text];\"string\"==typeof y&&(o.text=y)}return(\"boundaries\"===t.tickson||t.showdividers)&&(o.xbnd=[f(o.x-.5),f(o.x+t.dtick-.5)]),o},Z.hoverLabelText=function(t,e,r){r&&(t=s.extendFlat({},t,{hoverformat:r}));var n=s.isArrayOrTypedArray(e)?e[0]:e,i=s.isArrayOrTypedArray(e)?e[1]:void 0;if(void 0!==i&&i!==n)return Z.hoverLabelText(t,n,r)+\" - \"+Z.hoverLabelText(t,i,r);var a=\"log\"===t.type&&n<=0,o=Z.tickText(t,t.c2l(a?-n:n),\"hover\").text;return a?0===n?\"0\":z+o:o};var xt=[\"f\",\"p\",\"n\",\"μ\",\"m\",\"\",\"k\",\"M\",\"G\",\"T\"];function _t(t){return\"SI\"===t||\"B\"===t}function bt(t){return t>14||t<-15}function wt(t,e,r,n){var a=t<0,o=e._tickround,l=r||e.exponentformat||\"B\",c=e._tickexponent,u=Z.getTickFormat(e),h=e.separatethousands;if(n){var f={exponentformat:l,minexponent:e.minexponent,dtick:\"none\"===e.showexponent?e.dtick:i(t)&&Math.abs(t)||1,range:\"none\"===e.showexponent?e.range.map(e.r2d):[0,t||1]};yt(f),o=(Number(f._tickround)||0)+4,c=f._tickexponent,e.hoverformat&&(u=e.hoverformat)}if(u)return e._numFormat(u)(t).replace(/-/g,z);var p,d=Math.pow(10,-o)/2;if(\"none\"===l&&(c=0),(t=Math.abs(t))
\")):x=f.textLabel;var C={x:f.traceCoordinate[0],y:f.traceCoordinate[1],z:f.traceCoordinate[2],data:b._input,fullData:b,curveNumber:b.index,pointNumber:T};d.appendArrayPointValue(C,b,T),t._module.eventData&&(C=b._module.eventData(C,f,b,{},T));var L={points:[C]};if(e.fullSceneLayout.hovermode){var I=[];d.loneHover({trace:b,x:(.5+.5*v[0]/v[3])*s,y:(.5-.5*v[1]/v[3])*l,xLabel:k.xLabel,yLabel:k.yLabel,zLabel:k.zLabel,text:x,name:u.name,color:d.castHoverOption(b,T,\"bgcolor\")||u.color,borderColor:d.castHoverOption(b,T,\"bordercolor\"),fontFamily:d.castHoverOption(b,T,\"font.family\"),fontSize:d.castHoverOption(b,T,\"font.size\"),fontColor:d.castHoverOption(b,T,\"font.color\"),nameLength:d.castHoverOption(b,T,\"namelength\"),textAlign:d.castHoverOption(b,T,\"align\"),hovertemplate:h.castOption(b,T,\"hovertemplate\"),hovertemplateLabels:h.extendFlat({},C,k),eventData:[C]},{container:n,gd:r,inOut_bbox:I}),C.bbox=I[0]}f.distance<5&&(f.buttons||w)?r.emit(\"plotly_click\",L):r.emit(\"plotly_hover\",L),this.oldEventData=L}else d.loneUnhover(n),this.oldEventData&&r.emit(\"plotly_unhover\",this.oldEventData),this.oldEventData=void 0;e.drawAnnotations(e)},k.recoverContext=function(){var t=this;t.glplot.dispose();var e=function(){t.glplot.gl.isContextLost()?requestAnimationFrame(e):t.initializeGLPlot()?t.plot.apply(t,t.plotArgs):h.error(\"Catastrophic and unrecoverable WebGL error. Context lost.\")};requestAnimationFrame(e)};var M=[\"xaxis\",\"yaxis\",\"zaxis\"];function S(t,e,r){for(var n=t.fullSceneLayout,i=0;i<3;i++){var a=M[i],o=a.charAt(0),s=n[a],l=e[o],c=e[o+\"calendar\"],u=e[\"_\"+o+\"length\"];if(h.isArrayOrTypedArray(l))for(var f,p=0;p<(u||l.length);p++)if(h.isArrayOrTypedArray(l[p]))for(var d=0;d
\");y.text(_).attr(\"data-unformatted\",_).call(u.convertToTspans,t),v=c.bBox(y.node())}y.attr(\"transform\",i(-3,8-v.height)),g.insert(\"rect\",\".static-attribution\").attr({x:-v.width-6,y:-v.height-3,width:v.width+6,height:v.height+3,fill:\"rgba(255, 255, 255, 0.75)\"});var b=1;v.width+6>x&&(b=x/(v.width+6));var w=[n.l+n.w*p.x[1],n.t+n.h*(1-p.y[0])];g.attr(\"transform\",i(w[0],w[1])+a(b))}},e.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots[f],n=0;n
\");_.text(T).attr(\"data-unformatted\",T).call(h.convertToTspans,t),b=u.bBox(_.node())}_.attr(\"transform\",a(-3,8-b.height)),x.insert(\"rect\",\".static-attribution\").attr({x:-b.width-6,y:-b.height-3,width:b.width+6,height:b.height+3,fill:\"rgba(255, 255, 255, 0.75)\"});var k=1;b.width+6>w&&(k=w/(b.width+6));var A=[n.l+n.w*f.x[1],n.t+n.h*(1-f.y[0])];x.attr(\"transform\",a(A[0],A[1])+o(k))}},e.updateFx=function(t){for(var e=t._fullLayout,r=e._subplots[p],n=0;n