diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 780d840e..a7cfc876 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv concurrency: diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 00000000..7aece740 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,121 @@ +name: codecov.yml + +permissions: + contents: read + actions: read # Required for setup-python and other actions to read action metadata + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + UV_VERSION: "0.12.7" + UV_PROJECT_ENVIRONMENT: venv + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: + - master + paths: + - 'openseries/**' + - 'tests/**' + - 'pyproject.toml' + - 'uv.lock' + - '.python-version' + - '.github/workflows/codecov.yml' + - 'Makefile' + - 'make.ps1' + - '.pre-commit-config.yaml' + - 'scripts/ci-pr-paths.sh' + workflow_dispatch: {} + +jobs: + upload_coverage: + name: Upload coverage + runs-on: ubuntu-latest + environment: codecov + defaults: + run: + shell: bash + permissions: + contents: read + actions: read # Required for setup-python and other actions to read action metadata + issues: write # Required for actions/github-script to create issues on test failure + + steps: + - name: Require master branch + env: + REF: ${{ github.ref }} + run: | + if [ "$REF" != "refs/heads/master" ]; then + echo "::error::Codecov reporting is limited to the master branch." + exit 1 + fi + + - name: Check out GitHub repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 1 + ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.sha }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version-file: '.python-version' + + - name: Set up uv + uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: ${{ env.UV_VERSION }} + + - name: Sync dependencies (locked) + run: uv sync --locked --extra dev + + - name: Tests with Pytest + id: pytest + continue-on-error: true + run: uv run pytest + env: + PYTHONPATH: ${{ github.workspace }} + + - name: Create GitHub issue on failure + if: ${{ steps.pytest.outcome == 'failure' }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + GH_OWNER: ${{ github.repository_owner }} + GH_REPO: ${{ github.event.repository.name }} + GH_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + with: + script: | + const owner = process.env.GH_OWNER; + const repo = process.env.GH_REPO; + const runUrl = process.env.GH_RUN_URL; + + await github.rest.issues.create({ + owner, + repo, + title: `Tests failed on ${new Date().toDateString()}`, + body: `See the full logs here: ${runUrl}`, + }); + + - name: Fail job if tests failed + if: ${{ steps.pytest.outcome == 'failure' }} + run: exit 1 + + - name: Upload test results to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: junit.xml + report_type: test_results + verbose: true + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: CaptorAB/openseries + files: coverage.xml + verbose: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 3a5ff871..f03b2dc4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -3,20 +3,19 @@ name: codeql.yml permissions: contents: read actions: read # Required for setup-python and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv concurrency: - group: codeql-${{ github.ref }} + group: codeql-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} cancel-in-progress: true on: workflow_dispatch: {} - push: - branches: [master] pull_request: branches: [master] schedule: @@ -28,6 +27,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + actions: read # Required for setup-python and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering security-events: write # Required for github/codeql-action/analyze to upload results steps: - name: Check out repository @@ -35,25 +36,45 @@ jobs: with: persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + 'openseries/*' + 'tests/*' + 'pyproject.toml' + 'uv.lock' + '.github/workflows/codeql.yml' + 'scripts/ci-pr-paths.sh' + - name: Initialize CodeQL + if: ${{ steps.paths.outputs.run == 'true' }} uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: languages: python - name: Set up Python + if: ${{ steps.paths.outputs.run == 'true' }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Set up uv + if: ${{ steps.paths.outputs.run == 'true' }} uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ${{ env.UV_VERSION }} - name: Sync dependencies (locked) + if: ${{ steps.paths.outputs.run == 'true' }} run: uv sync --locked --extra dev - name: Perform CodeQL analysis + if: ${{ steps.paths.outputs.run == 'true' }} uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: category: /language:python diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 98967457..21f4a092 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv concurrency: @@ -33,7 +33,6 @@ jobs: name: Build and test needs: gate runs-on: ubuntu-latest - environment: codecov permissions: contents: read issues: write # Required for actions/github-script to create issues on test failure @@ -93,24 +92,6 @@ jobs: body: `See the full logs here: ${GH_RUN_URL}`, }); - - name: Upload test results to Codecov - if: ${{ github.ref_name == 'master' && success() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - report_type: test_results - files: junit.xml - verbose: true - - - name: Upload coverage to Codecov - if: ${{ github.ref_name == 'master' && success() }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: CaptorAB/openseries - files: coverage.xml - verbose: true - tag_and_release: name: Tag and create release needs: build_test diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 60072f34..29948c14 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,7 +5,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv concurrency: @@ -13,32 +13,39 @@ concurrency: cancel-in-progress: true on: - # Only run on pushes to master for documentation-related changes - # This prevents running on every commit, only when docs actually change push: branches: - master paths: - 'docs/**' + - 'openseries/**' + - 'pyproject.toml' + - 'uv.lock' + - '.python-version' - '.readthedocs.yaml' - # Manual trigger for immediate updates (e.g., after releases) + - '.github/workflows/docs.yml' + pull_request: + branches: + - master + paths: + - 'docs/**' + - 'openseries/**' + - 'pyproject.toml' + - 'uv.lock' + - '.python-version' + - '.readthedocs.yaml' + - '.github/workflows/docs.yml' workflow_dispatch: {} jobs: - build-and-deploy: - name: Build and deploy documentation + build: + name: Build documentation defaults: run: shell: bash runs-on: ubuntu-latest - if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/master' permissions: contents: read - pages: write # Required for actions/deploy-pages to deploy to GitHub Pages - id-token: write # Required for OIDC token for GitHub Pages deployment - environment: - name: github-pages - steps: - name: Check out GitHub repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -65,12 +72,36 @@ jobs: run: uv run --directory docs make builddocs - name: Check for documentation warnings - run: | - uv run --directory docs make strict || { - echo "Documentation build failed with warnings/errors:" - echo "This indicates potential issues that should be fixed." - exit 1 - } + run: uv run --directory docs make strict + + - name: Upload HTML artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: docs-html + path: docs/build/html + if-no-files-found: error + retention-days: 1 + + deploy: + name: Deploy documentation + needs: build + if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'push' }} + defaults: + run: + shell: bash + runs-on: ubuntu-latest + permissions: + contents: read + pages: write # Required for actions/deploy-pages to deploy to GitHub Pages + id-token: write # Required for OIDC token for GitHub Pages deployment + environment: + name: github-pages + steps: + - name: Download HTML artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: docs-html + path: docs/build/html - name: Setup Pages uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 diff --git a/.github/workflows/supply-chain.yml b/.github/workflows/supply-chain.yml index cd1843e7..e78fe6b9 100644 --- a/.github/workflows/supply-chain.yml +++ b/.github/workflows/supply-chain.yml @@ -3,10 +3,11 @@ name: supply-chain.yml permissions: contents: read actions: read # Required for setup-uv and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv PIP_AUDIT_VERSION: "2.10.0" @@ -16,10 +17,10 @@ concurrency: on: workflow_dispatch: {} - push: - branches: [master] pull_request: branches: [master] + schedule: + - cron: "15 6 * * 1" jobs: lockfile: @@ -31,12 +32,27 @@ jobs: with: persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + 'uv.lock' + 'pyproject.toml' + '.github/workflows/supply-chain.yml' + 'scripts/ci-pr-paths.sh' + - name: Set up uv + if: ${{ steps.paths.outputs.run == 'true' }} uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ${{ env.UV_VERSION }} - name: Check uv.lock is up to date + if: ${{ steps.paths.outputs.run == 'true' }} run: uv lock --check osv-scanner: @@ -48,7 +64,21 @@ jobs: with: persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + 'uv.lock' + 'pyproject.toml' + '.github/workflows/supply-chain.yml' + 'scripts/ci-pr-paths.sh' + - name: Run OSV-Scanner + if: ${{ steps.paths.outputs.run == 'true' }} uses: google/osv-scanner-action/osv-scanner-action@8deb546fdb875b9996d27d4950be7312dac076a1 # v2.5.0 with: scan-args: --lockfile=uv.lock @@ -63,23 +93,41 @@ jobs: with: persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + 'uv.lock' + 'pyproject.toml' + '.github/workflows/supply-chain.yml' + 'scripts/ci-pr-paths.sh' + - name: Set up Python + if: ${{ steps.paths.outputs.run == 'true' }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: .python-version - name: Set up uv + if: ${{ steps.paths.outputs.run == 'true' }} uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ${{ env.UV_VERSION }} - name: Sync dependencies (locked) + if: ${{ steps.paths.outputs.run == 'true' }} run: uv sync --locked --extra dev - name: Export requirements for pip-audit + if: ${{ steps.paths.outputs.run == 'true' }} run: uv export --locked --extra dev --no-emit-project -o requirements-audit.txt - name: Run pip-audit + if: ${{ steps.paths.outputs.run == 'true' }} env: PIP_AUDIT_VERSION: ${{ env.PIP_AUDIT_VERSION }} run: uvx pip-audit==${PIP_AUDIT_VERSION} -r requirements-audit.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f4c77b76..0a77483f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,10 +3,11 @@ name: tests.yml permissions: contents: read actions: read # Required for setup-python and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" + UV_VERSION: "0.12.7" UV_PROJECT_ENVIRONMENT: venv concurrency: @@ -15,9 +16,6 @@ concurrency: on: workflow_dispatch: {} - push: - branches: - - master pull_request: branches: - master @@ -26,11 +24,13 @@ jobs: run_tests: name: Run tests runs-on: ubuntu-latest - environment: codecov defaults: run: shell: bash permissions: + contents: read + actions: read # Required for setup-python and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering issues: write # Required for actions/github-script to create issues on test failure steps: @@ -41,27 +41,52 @@ jobs: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.sha }} persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + 'openseries/*' + 'tests/*' + 'pyproject.toml' + 'uv.lock' + '.python-version' + '.github/workflows/test.yml' + '.github/workflows/codecov.yml' + 'Makefile' + 'make.ps1' + '.pre-commit-config.yaml' + 'scripts/ci-pr-paths.sh' + - name: Set up Python + if: ${{ steps.paths.outputs.run == 'true' }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version-file: '.python-version' - name: Set up uv + if: ${{ steps.paths.outputs.run == 'true' }} uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ${{ env.UV_VERSION }} - name: Sync dependencies (locked) + if: ${{ steps.paths.outputs.run == 'true' }} run: uv sync --locked --extra dev - name: Check and fix with Ruff + if: ${{ steps.paths.outputs.run == 'true' }} run: uv run ruff check ./tests/*.py ./openseries/*.py --fix --exit-non-zero-on-fix - name: Format with Ruff + if: ${{ steps.paths.outputs.run == 'true' }} run: uv run ruff format - name: Manage Mypy cache - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + if: ${{ steps.paths.outputs.run == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: mypy-cache with: @@ -70,9 +95,11 @@ jobs: restore-keys: mypy-cache- - name: Type check with Mypy + if: ${{ steps.paths.outputs.run == 'true' }} run: uv run mypy --cache-dir .mypy_cache . - name: Tests with Pytest + if: ${{ steps.paths.outputs.run == 'true' }} id: pytest continue-on-error: true run: uv run pytest @@ -102,21 +129,3 @@ jobs: - name: Fail job if tests failed if: ${{ steps.pytest.outcome == 'failure' }} run: exit 1 - - - name: Upload test results to Codecov - if: ${{ github.ref_name == 'master' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: junit.xml - report_type: test_results - verbose: true - - - name: Upload coverage to Codecov - if: ${{ github.ref_name == 'master' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} - uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 - with: - token: ${{ secrets.CODECOV_TOKEN }} - slug: CaptorAB/openseries - files: coverage.xml - verbose: true diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 57621d47..8aefd481 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -3,11 +3,12 @@ name: zizmor.yml permissions: contents: read actions: read # Required for setup-uv and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" - UV_VERSION: "0.11.21" - ZIZMOR_VERSION: "1.25.2" + UV_VERSION: "0.12.7" + ZIZMOR_VERSION: "1.29.0" concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }} @@ -15,8 +16,6 @@ concurrency: on: workflow_dispatch: {} - push: - branches: [master] pull_request: branches: [master] @@ -25,6 +24,9 @@ jobs: name: Run zizmor audit runs-on: ubuntu-latest permissions: + contents: read + actions: read # Required for setup-uv and other actions to read action metadata + pull-requests: read # Required to list PR files for path filtering security-events: write # Required for github/codeql-action/upload-sarif to upload SARIF results steps: - name: Checkout repository @@ -34,26 +36,40 @@ jobs: ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || github.sha }} persist-credentials: false + - name: Filter paths + id: paths + env: + GH_TOKEN: ${{ github.token }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REPOSITORY: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash scripts/ci-pr-paths.sh + '.github/*' + 'scripts/run-zizmor.sh' + 'scripts/ci-pr-paths.sh' + - name: Set up uv + if: ${{ steps.paths.outputs.run == 'true' }} uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 with: version: ${{ env.UV_VERSION }} - name: Run zizmor audit + if: ${{ steps.paths.outputs.run == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZIZMOR_VERSION: ${{ env.ZIZMOR_VERSION }} run: uvx zizmor==${ZIZMOR_VERSION} --strict-collection --pedantic . - name: Generate zizmor SARIF report - if: always() + if: ${{ always() && steps.paths.outputs.run == 'true' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ZIZMOR_VERSION: ${{ env.ZIZMOR_VERSION }} run: uvx zizmor==${ZIZMOR_VERSION} --strict-collection --pedantic --format=sarif . > results.sarif - name: Upload SARIF file - if: always() + if: ${{ always() && steps.paths.outputs.run == 'true' }} uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7 with: sarif_file: results.sarif diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b49afa2..f4051697 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,12 +3,12 @@ default_language_version: repos: - repo: https://github.com/astral-sh/uv-pre-commit - rev: 0.11.21 + rev: 0.12.7 hooks: - id: uv-lock args: [ --check ] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.18 + rev: v0.16.5 hooks: - id: ruff-check args: [ --fix, --exit-non-zero-on-fix ] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index e411019b..8fb2ff3a 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -15,6 +15,7 @@ sphinx: python: install: - - requirements: docs/requirements.txt - method: pip path: . + extra_requirements: + - docs diff --git a/Makefile b/Makefile index 0afe3122..e11b6544 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .ONESHELL: -UV_VERSION ?= 0.11.21 +UV_VERSION ?= 0.12.7 PIP_AUDIT_VERSION ?= 2.10.0 .PHONY: all install update test lint audit clean builddocs servedocs cleandocs diff --git a/README.md b/README.md index 01c891b0..d7096641 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [](https://www.python.org/) [](https://github.com/CaptorAB/openseries/actions/workflows/test.yml) [](https://codecov.io/gh/CaptorAB/openseries/branch/master) -[](https://captorab.github.io/openseries/) +[](https://openseries.readthedocs.io/) [](https://github.com/astral-sh/uv) [](https://beta.ruff.rs/docs/) [](https://github.com/CaptorAB/openseries/blob/master/LICENSE.md) @@ -20,7 +20,7 @@ Tools for analyzing financial timeseries of a single asset or a group of assets. ## Documentation -Complete documentation is available at: [https://captorab.github.io/openseries/](https://captorab.github.io/openseries/) +Complete documentation is available at: [https://openseries.readthedocs.io/](https://openseries.readthedocs.io/) The documentation includes: @@ -47,11 +47,11 @@ conda install -c conda-forge openseries from openseries import OpenTimeSeries import yfinance as yf -move=yf.Ticker(ticker="^MOVE") -history=move.history(period="max") -series=OpenTimeSeries.from_df(dframe=history.loc[:, "Close"]) -_=series.set_new_label(lvl_zero="ICE BofAML MOVE Index") -_,_=series.plot_series() +move = yf.Ticker(ticker="^MOVE") +history = move.history(period="max") +series = OpenTimeSeries.from_df(dframe=history.loc[:, "Close"]) +_ = series.set_new_label(lvl_zero="ICE BofAML MOVE Index") +_, _ = series.plot_series() ``` ### Sample output using the report_html() function diff --git a/SECURITY.md b/SECURITY.md index 2cb5d5c7..31e53fd7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -19,11 +19,22 @@ vulnerabilities. - **CI workflows** use hash-pinned actions, default read-only `contents`, and [zizmor](https://github.com/woodruffw/zizmor) audits. - **Dependencies** are locked in `uv.lock`; CI runs `uv sync --locked` and - supply-chain scans (`supply-chain.yml`). + supply-chain scans (`supply-chain.yml`) on pull requests that change lockfiles + and on a weekly schedule. - **Releases** build from the signed git tag, record `SHA256SUMS` for artifacts, and verify checksums before publish. - **`pull_request_target` is not used**; PR CI runs on `pull_request` with read-only defaults and fork guards on cache restore and issue creation. +- **Validation workflows** (`tests.yml`, `supply-chain.yml`, `zizmor.yml`, + `codeql.yml`) run on pull requests, not again on merge to `master`. Path + filters skip heavy steps when the PR does not touch relevant files, while the + jobs still report success so required checks are not left pending. +- **CodeQL** full analysis runs on pull requests that change Python sources and + on a weekly schedule; it is not repeated on every push to `master`. +- **Documentation** is published at + [openseries.readthedocs.io](https://openseries.readthedocs.io/) (the URL in + PyPI and conda-forge metadata) and also deployed to GitHub Pages by + `docs.yml`. Pull requests build docs without deploying Pages. - **Release tagging** is isolated in a reusable workflow (`release-tag.yml`); build and PyPI publish run in `deploy.yml` because PyPI Trusted Publishing does not support reusable workflows. `deploy.yml` is the only manual entry @@ -47,10 +58,16 @@ YAML alone: - Required reviewers before deployment - Restrict deployment branches to `master` - Do not expose secrets to fork PR workflows + - `codecov` is used only by `codecov.yml` on `master` (not PR tests or + `deploy.yml`) 4. **Branch protection on `master`**: - - Require status checks from `tests.yml`, `supply-chain.yml`, `zizmor.yml`, - and CodeQL before merge + - Require status checks from `tests.yml`, `supply-chain.yml`, and + `zizmor.yml` before merge. Do not require the Read the Docs PR check. + CodeQL may remain required; the `codeql.yml` job no-ops on PRs that do + not change Python sources. - Require review for changes under `.github/workflows/` + - Keep the Read the Docs GitHub integration so `latest` still builds; turn + off **Build pull requests** in the RTD project so PRs are not double-built. 5. **Dependabot**: keep weekly updates with cooldown enabled (see `.github/dependabot.yml`). 6. **Audit log**: periodically review GitHub audit log for workflow or secret diff --git a/docs/README.md b/docs/README.md index 1a3bfb20..bc37cedc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -83,13 +83,23 @@ docs/ └── make.bat # Build commands (Windows) ``` -## ReadTheDocs Integration - -This documentation is configured for [ReadTheDocs](https://readthedocs.org/) hosting: - -- Configuration: `.readthedocs.yaml` in the project root -- Dependencies: Managed through `pyproject.toml` optional docs dependencies -- Build process: Automated on ReadTheDocs +## Hosting + +Canonical documentation is [https://openseries.readthedocs.io/](https://openseries.readthedocs.io/) +(the URL published on PyPI and conda-forge). The same Sphinx site is also +deployed to [GitHub Pages](https://captorab.github.io/openseries/) by +`.github/workflows/docs.yml` (Homepage in package metadata). + +- Pull requests that touch documentation sources or library code build docs + in GitHub Actions (warnings as errors) without deploying Pages +- Pushes to `master` and manual `workflow_dispatch` (including after a PyPI + release) build and deploy Pages +- Read the Docs builds `latest` from `master` using `.readthedocs.yaml`; + disable **Build pull requests** in the RTD dashboard so PRs are not also + built there +- Dependencies come from the `docs` extra in `pyproject.toml` / `uv.lock`. + Read the Docs installs that extra via `.readthedocs.yaml` + (`pip install .[docs]`) ## Writing Documentation @@ -196,7 +206,6 @@ When contributing to documentation: **Missing modules:** - Install missing dependencies: `uv sync --locked --extra docs` -- For ReadTheDocs builds, `docs/requirements.txt` must match the `docs` extra in `pyproject.toml` **Broken links:** diff --git a/docs/build/html/.buildinfo b/docs/build/html/.buildinfo deleted file mode 100644 index 226d20d2..00000000 --- a/docs/build/html/.buildinfo +++ /dev/null @@ -1,4 +0,0 @@ -# Sphinx build info version 1 -# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. -config: c7ef2e0edce5614b95a3d168aacf8abb -tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/build/html/.doctrees/api/datefixer.doctree b/docs/build/html/.doctrees/api/datefixer.doctree deleted file mode 100644 index c15443d0..00000000 Binary files a/docs/build/html/.doctrees/api/datefixer.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/frame.doctree b/docs/build/html/.doctrees/api/frame.doctree deleted file mode 100644 index 4eb04237..00000000 Binary files a/docs/build/html/.doctrees/api/frame.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.OpenFrame.doctree b/docs/build/html/.doctrees/api/generated/openseries.OpenFrame.doctree deleted file mode 100644 index d62ce082..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.OpenFrame.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.OpenTimeSeries.doctree b/docs/build/html/.doctrees/api/generated/openseries.OpenTimeSeries.doctree deleted file mode 100644 index 686008b4..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.OpenTimeSeries.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.ReturnSimulation.doctree b/docs/build/html/.doctrees/api/generated/openseries.ReturnSimulation.doctree deleted file mode 100644 index d50751f3..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.ReturnSimulation.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.ValueType.doctree b/docs/build/html/.doctrees/api/generated/openseries.ValueType.doctree deleted file mode 100644 index 96bbcd0f..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.ValueType.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.constrain_optimized_portfolios.doctree b/docs/build/html/.doctrees/api/generated/openseries.constrain_optimized_portfolios.doctree deleted file mode 100644 index 8502ec06..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.constrain_optimized_portfolios.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.date_fix.doctree b/docs/build/html/.doctrees/api/generated/openseries.date_fix.doctree deleted file mode 100644 index a78cc23e..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.date_fix.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.date_offset_foll.doctree b/docs/build/html/.doctrees/api/generated/openseries.date_offset_foll.doctree deleted file mode 100644 index 769805cc..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.date_offset_foll.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.efficient_frontier.doctree b/docs/build/html/.doctrees/api/generated/openseries.efficient_frontier.doctree deleted file mode 100644 index 116f1407..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.efficient_frontier.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.generate_calendar_date_range.doctree b/docs/build/html/.doctrees/api/generated/openseries.generate_calendar_date_range.doctree deleted file mode 100644 index 46576a04..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.generate_calendar_date_range.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.get_previous_business_day_before_today.doctree b/docs/build/html/.doctrees/api/generated/openseries.get_previous_business_day_before_today.doctree deleted file mode 100644 index ce0091bd..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.get_previous_business_day_before_today.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.holiday_calendar.doctree b/docs/build/html/.doctrees/api/generated/openseries.holiday_calendar.doctree deleted file mode 100644 index 1746d627..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.holiday_calendar.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.load_plotly_dict.doctree b/docs/build/html/.doctrees/api/generated/openseries.load_plotly_dict.doctree deleted file mode 100644 index 365bd04d..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.load_plotly_dict.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.offset_business_days.doctree b/docs/build/html/.doctrees/api/generated/openseries.offset_business_days.doctree deleted file mode 100644 index b960059e..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.offset_business_days.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.prepare_plot_data.doctree b/docs/build/html/.doctrees/api/generated/openseries.prepare_plot_data.doctree deleted file mode 100644 index 5fcb044d..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.prepare_plot_data.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.report_html.doctree b/docs/build/html/.doctrees/api/generated/openseries.report_html.doctree deleted file mode 100644 index 7b736cfd..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.report_html.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.sharpeplot.doctree b/docs/build/html/.doctrees/api/generated/openseries.sharpeplot.doctree deleted file mode 100644 index 4cbe6eb6..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.sharpeplot.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.simulate_portfolios.doctree b/docs/build/html/.doctrees/api/generated/openseries.simulate_portfolios.doctree deleted file mode 100644 index 8d89a578..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.simulate_portfolios.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/generated/openseries.timeseries_chain.doctree b/docs/build/html/.doctrees/api/generated/openseries.timeseries_chain.doctree deleted file mode 100644 index 0391d743..00000000 Binary files a/docs/build/html/.doctrees/api/generated/openseries.timeseries_chain.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/openseries.doctree b/docs/build/html/.doctrees/api/openseries.doctree deleted file mode 100644 index 6de3d26f..00000000 Binary files a/docs/build/html/.doctrees/api/openseries.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/portfoliotools.doctree b/docs/build/html/.doctrees/api/portfoliotools.doctree deleted file mode 100644 index 87044831..00000000 Binary files a/docs/build/html/.doctrees/api/portfoliotools.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/report.doctree b/docs/build/html/.doctrees/api/report.doctree deleted file mode 100644 index 131d6bf6..00000000 Binary files a/docs/build/html/.doctrees/api/report.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/series.doctree b/docs/build/html/.doctrees/api/series.doctree deleted file mode 100644 index 5f8937c6..00000000 Binary files a/docs/build/html/.doctrees/api/series.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/simulation.doctree b/docs/build/html/.doctrees/api/simulation.doctree deleted file mode 100644 index 6c135d8d..00000000 Binary files a/docs/build/html/.doctrees/api/simulation.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/api/types.doctree b/docs/build/html/.doctrees/api/types.doctree deleted file mode 100644 index 5a017b19..00000000 Binary files a/docs/build/html/.doctrees/api/types.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/development/changelog.doctree b/docs/build/html/.doctrees/development/changelog.doctree deleted file mode 100644 index c85b625f..00000000 Binary files a/docs/build/html/.doctrees/development/changelog.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/development/contributing.doctree b/docs/build/html/.doctrees/development/contributing.doctree deleted file mode 100644 index 5f417bf5..00000000 Binary files a/docs/build/html/.doctrees/development/contributing.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/environment.pickle b/docs/build/html/.doctrees/environment.pickle deleted file mode 100644 index 9fe326b1..00000000 Binary files a/docs/build/html/.doctrees/environment.pickle and /dev/null differ diff --git a/docs/build/html/.doctrees/examples/custom_reports.doctree b/docs/build/html/.doctrees/examples/custom_reports.doctree deleted file mode 100644 index 34906b7e..00000000 Binary files a/docs/build/html/.doctrees/examples/custom_reports.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/examples/multi_asset.doctree b/docs/build/html/.doctrees/examples/multi_asset.doctree deleted file mode 100644 index 776c5443..00000000 Binary files a/docs/build/html/.doctrees/examples/multi_asset.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/examples/portfolio_optimization.doctree b/docs/build/html/.doctrees/examples/portfolio_optimization.doctree deleted file mode 100644 index 0dfcda1a..00000000 Binary files a/docs/build/html/.doctrees/examples/portfolio_optimization.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/examples/single_asset.doctree b/docs/build/html/.doctrees/examples/single_asset.doctree deleted file mode 100644 index 3c862fe4..00000000 Binary files a/docs/build/html/.doctrees/examples/single_asset.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/index.doctree b/docs/build/html/.doctrees/index.doctree deleted file mode 100644 index 9c16b540..00000000 Binary files a/docs/build/html/.doctrees/index.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/tutorials/advanced_features.doctree b/docs/build/html/.doctrees/tutorials/advanced_features.doctree deleted file mode 100644 index bb2624fb..00000000 Binary files a/docs/build/html/.doctrees/tutorials/advanced_features.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/tutorials/basic_analysis.doctree b/docs/build/html/.doctrees/tutorials/basic_analysis.doctree deleted file mode 100644 index 5778be24..00000000 Binary files a/docs/build/html/.doctrees/tutorials/basic_analysis.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/tutorials/portfolio_analysis.doctree b/docs/build/html/.doctrees/tutorials/portfolio_analysis.doctree deleted file mode 100644 index 9c5bc063..00000000 Binary files a/docs/build/html/.doctrees/tutorials/portfolio_analysis.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/tutorials/risk_management.doctree b/docs/build/html/.doctrees/tutorials/risk_management.doctree deleted file mode 100644 index d2d2cdbf..00000000 Binary files a/docs/build/html/.doctrees/tutorials/risk_management.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/user_guide/core_concepts.doctree b/docs/build/html/.doctrees/user_guide/core_concepts.doctree deleted file mode 100644 index 68079153..00000000 Binary files a/docs/build/html/.doctrees/user_guide/core_concepts.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/user_guide/data_handling.doctree b/docs/build/html/.doctrees/user_guide/data_handling.doctree deleted file mode 100644 index bf8333c9..00000000 Binary files a/docs/build/html/.doctrees/user_guide/data_handling.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/user_guide/installation.doctree b/docs/build/html/.doctrees/user_guide/installation.doctree deleted file mode 100644 index 0208f8d1..00000000 Binary files a/docs/build/html/.doctrees/user_guide/installation.doctree and /dev/null differ diff --git a/docs/build/html/.doctrees/user_guide/quickstart.doctree b/docs/build/html/.doctrees/user_guide/quickstart.doctree deleted file mode 100644 index 7358a818..00000000 Binary files a/docs/build/html/.doctrees/user_guide/quickstart.doctree and /dev/null differ diff --git a/docs/build/html/.nojekyll b/docs/build/html/.nojekyll deleted file mode 100644 index e69de29b..00000000 diff --git a/docs/build/html/_modules/index.html b/docs/build/html/_modules/index.html deleted file mode 100644 index b530b689..00000000 --- a/docs/build/html/_modules/index.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - -
- - -
-"""Date related utilities."""
-
-from __future__ import annotations
-
-import datetime as dt
-from typing import TYPE_CHECKING, cast
-
-import exchange_calendars as exchcal # type: ignore[import-untyped]
-from dateutil.relativedelta import relativedelta
-from holidays import (
- country_holidays,
- list_supported_countries,
-)
-from numpy import array, busdaycalendar, datetime64, is_busday, where
-from pandas import (
- DataFrame,
- DatetimeIndex,
- Index,
- Timestamp,
- concat,
- date_range,
-)
-from pandas.tseries.offsets import CustomBusinessDay
-
-from .owntypes import (
- BothStartAndEndError,
- CountriesNotStringNorListStrError,
- MarketsNotStringNorListStrError,
- TradingDaysNotAboveZeroError,
-)
-
-if TYPE_CHECKING:
- from .owntypes import ( # pragma: no cover
- CountriesType,
- DateType,
- LiteralBizDayFreq,
- )
-
-__all__ = [
- "date_fix",
- "date_offset_foll",
- "generate_calendar_date_range",
- "get_previous_business_day_before_today",
- "holiday_calendar",
- "offset_business_days",
-]
-
-
-def market_holidays(
- startyear: int,
- endyear: int,
- markets: str | list[str],
-) -> list[str]:
- """Return a list of holiday dates mapping to list of markets closed.
-
- Args:
- startyear: First year (inclusive) to consider.
- endyear: Last year (inclusive) to consider.
- markets: String or list of market codes supported by exchange_calendars.
-
- Returns:
- List of holiday dates.
-
- Raises:
- MarketsNotStringNorListStrError: If any market code is not supported by
- ``exchange_calendars`` or the input is not a string or list of strings.
- """
- market_list = [markets] if isinstance(markets, str) else list(markets)
-
- supported = exchcal.get_calendar_names()
-
- if not all(m in supported for m in market_list):
- msg = (
- "Argument markets must be a string market code or a list of market "
- "codes supported by exchange_calendars."
- )
- raise MarketsNotStringNorListStrError(msg)
-
- holidays: list[str] = []
- for m in market_list:
- cal = exchcal.get_calendar(m)
- cal_hols = cal.regular_holidays.holidays()
- my_hols: list[str] = [
- date.date().strftime("%Y-%m-%d")
- for date in cal_hols
- if (startyear <= date.date().year <= endyear)
- ]
- holidays.extend(my_hols)
-
- return list(set(holidays))
-
-
-
-[docs]
-def holiday_calendar(
- startyear: int,
- endyear: int,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
-) -> busdaycalendar:
- """Generate a business calendar.
-
- Args:
- startyear: First year in date range generated.
- endyear: Last year in date range generated.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
-
- Returns:
- Generate a business calendar.
-
- Raises:
- CountriesNotStringNorListStrError: If ``countries`` is not a supported
- ISO 3166-1 alpha-2 string or a list of such strings.
- """
- startyear -= 1
- endyear += 1
- if startyear == endyear:
- endyear += 1
- years = list(range(startyear, endyear))
-
- if isinstance(countries, str) and countries in list_supported_countries():
- staging = country_holidays(country=countries, years=years)
- hols = list(staging.keys())
- elif isinstance(countries, (list, set)) and all(
- country in list_supported_countries() for country in countries
- ):
- country: str
- countryholidays: list[dt.date | str] = []
- for country in countries:
- staging = country_holidays(country=country, years=years)
- countryholidays += list(staging)
- hols = cast("list[dt.date]", list(countryholidays))
- else:
- msg = (
- "Argument countries must be a string country code or "
- "a list of string country codes according to ISO 3166-1 alpha-2."
- )
- raise CountriesNotStringNorListStrError(msg)
-
- if markets:
- market_hols = market_holidays(
- startyear=startyear,
- endyear=endyear,
- markets=markets,
- )
- dt_mkt_hols = [date_fix(fixerdate=ddate) for ddate in market_hols]
- hols.extend(dt_mkt_hols)
-
- if custom_holidays:
- custom_list = (
- [custom_holidays]
- if isinstance(custom_holidays, str)
- else list(custom_holidays)
- )
- hols.extend([date_fix(fixerdate=ddate) for ddate in custom_list])
-
- return busdaycalendar(holidays=array(sorted(set(hols)), dtype="datetime64[D]"))
-
-
-
-
-[docs]
-def date_fix(
- fixerdate: DateType,
-) -> dt.date:
- """Parse different date formats into datetime.date.
-
- Args:
- fixerdate: The data item to parse.
-
- Returns:
- Parsed date.
-
- Raises:
- TypeError: If the provided ``fixerdate`` type is not supported.
- """
- msg = f"Unknown date format {fixerdate!s} of type {type(fixerdate)!s} encountered"
- if isinstance(fixerdate, Timestamp | dt.datetime):
- return fixerdate.date()
- if isinstance(fixerdate, dt.date):
- return fixerdate
- if isinstance(fixerdate, datetime64):
- return (
- dt.datetime.strptime(str(fixerdate)[:10], "%Y-%m-%d").astimezone().date()
- )
- if isinstance(fixerdate, str):
- return dt.datetime.strptime(fixerdate, "%Y-%m-%d").astimezone().date()
- raise TypeError(msg)
-
-
-
-
-[docs]
-def date_offset_foll(
- raw_date: DateType,
- months_offset: int = 12,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
- *,
- adjust: bool = False,
- following: bool = True,
-) -> dt.date:
- """Offset dates according to a given calendar.
-
- Args:
- raw_date: The date to offset from.
- months_offset: Number of months as integer. Defaults to 12.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
- adjust: Determines if offset should adjust for business days.
- Defaults to False.
- following: Determines if days should be offset forward (following) or backward.
- Defaults to True.
-
- Returns:
- Offset date.
- """
- raw_date = date_fix(raw_date)
- month_delta = relativedelta(months=months_offset)
-
- day_delta = relativedelta(days=1) if following else relativedelta(days=-1)
-
- new_date = raw_date + month_delta
-
- if adjust:
- startyear = min([raw_date.year, new_date.year])
- endyear = max([raw_date.year, new_date.year])
- calendar = holiday_calendar(
- startyear=startyear,
- endyear=endyear,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- )
- while not is_busday(dates=new_date, busdaycal=calendar):
- new_date += day_delta
-
- return new_date
-
-
-
-
-[docs]
-def get_previous_business_day_before_today(
- today: dt.date | None = None,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
-) -> dt.date:
- """Bump date backwards to find the previous business day.
-
- Args:
- today: Manual input of the day from where the previous business day is found.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
-
- Returns:
- The previous business day.
- """
- if today is None:
- today = dt.datetime.now().astimezone().date()
-
- return date_offset_foll(
- raw_date=today - dt.timedelta(days=1),
- months_offset=0,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- adjust=True,
- following=False,
- )
-
-
-
-
-[docs]
-def offset_business_days(
- ddate: dt.date,
- days: int,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
-) -> dt.date:
- """Bump date by business days.
-
- It first adjusts to a valid business day and then bumps with given
- number of business days from there.
-
- Args:
- ddate: A starting date that does not have to be a business day.
- days: The number of business days to offset from the business day
- that is given.
- If days is set as anything other than an integer its value is set to zero.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
-
- Returns:
- The new offset business day.
- """
- try:
- days = int(days)
- except TypeError:
- days = 0
-
- if days <= 0:
- scaledtoyeardays = int((days * 372 / 250) // 1) - 365
- ndate = ddate + dt.timedelta(days=scaledtoyeardays)
- calendar = holiday_calendar(
- startyear=ndate.year,
- endyear=ddate.year,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- )
- local_bdays: list[dt.date] = [
- bday.date()
- for bday in date_range(
- periods=abs(scaledtoyeardays),
- end=ddate,
- freq=CustomBusinessDay(calendar=calendar),
- )
- ]
- else:
- scaledtoyeardays = int((days * 372 / 250) // 1) + 365
- ndate = ddate + dt.timedelta(days=scaledtoyeardays)
- calendar = holiday_calendar(
- startyear=ddate.year,
- endyear=ndate.year,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- )
- local_bdays = [
- bday.date()
- for bday in date_range(
- start=ddate,
- periods=scaledtoyeardays,
- freq=CustomBusinessDay(calendar=calendar),
- )
- ]
-
- while ddate not in local_bdays:
- if days <= 0:
- ddate -= dt.timedelta(days=1)
- else:
- ddate += dt.timedelta(days=1)
-
- idx = where(array(local_bdays) == ddate)[0]
-
- return cast("dt.date", local_bdays[idx[0] + days])
-
-
-
-
-[docs]
-def generate_calendar_date_range(
- trading_days: int,
- start: dt.date | None = None,
- end: dt.date | None = None,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
-) -> list[dt.date]:
- """Generate a list of business day calendar dates.
-
- Args:
- trading_days: Number of days to generate. Must be greater than zero.
- start: Date when the range starts.
- end: Date when the range ends.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
-
- Returns:
- List of business day calendar dates.
- """
- if trading_days < 1:
- msg = "Argument trading_days must be greater than zero."
- raise TradingDaysNotAboveZeroError(msg)
-
- if start and not end:
- adjusted_start = date_offset_foll(
- raw_date=start,
- months_offset=0,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- adjust=True,
- following=True,
- )
- tmp_range = date_range(
- start=adjusted_start,
- periods=trading_days * 365 // 252,
- freq="D",
- )
- calendar = holiday_calendar(
- startyear=adjusted_start.year,
- endyear=date_fix(tmp_range.tolist()[-1]).year,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- )
- return [
- d.date()
- for d in date_range(
- start=adjusted_start,
- periods=trading_days,
- freq=CustomBusinessDay(calendar=calendar),
- )
- ]
-
- if end and not start:
- adjusted_end = date_offset_foll(
- raw_date=end,
- months_offset=0,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- adjust=True,
- following=False,
- )
- tmp_range = date_range(
- end=adjusted_end,
- periods=trading_days * 365 // 252,
- freq="D",
- )
- calendar = holiday_calendar(
- startyear=date_fix(tmp_range.tolist()[0]).year,
- endyear=adjusted_end.year,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- )
- return [
- d.date()
- for d in date_range(
- end=adjusted_end,
- periods=trading_days,
- freq=CustomBusinessDay(calendar=calendar),
- )
- ]
-
- msg = (
- "Provide exactly one of start or end date. "
- "Date range is inferred from number of trading days."
- )
- raise BothStartAndEndError(msg)
-
-
-
-def _do_resample_to_business_period_ends(
- data: DataFrame,
- freq: LiteralBizDayFreq,
- countries: CountriesType,
- markets: list[str] | str | None = None,
- custom_holidays: list[str] | str | None = None,
-) -> DatetimeIndex:
- """Resample timeseries frequency to business calendar month end dates.
-
- Stubs left in place. Stubs will be aligned to the shortest stub.
-
- Args:
- data: The timeseries data.
- freq: The date offset string that sets the resampled frequency.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- markets: (List of) markets code(s) supported by exchange_calendars.
- custom_holidays: Argument where missing holidays can be added.
-
- Returns:
- A date range aligned to business period ends.
-
- """
- copydata = data.copy()
- copydata.index = DatetimeIndex(copydata.index)
- copydata = copydata.resample(rule=freq).last()
- copydata = copydata.drop(index=copydata.index[-1])
- copydata.index = Index(d.date() for d in DatetimeIndex(copydata.index))
-
- copydata = concat([data.head(n=1), copydata, data.tail(n=1)]).sort_index()
-
- dates = DatetimeIndex(
- [copydata.index[0]]
- + [
- date_offset_foll(
- raw_date=dt.date(d.year, d.month, 1)
- + relativedelta(months=1)
- - dt.timedelta(days=1),
- months_offset=0,
- countries=countries,
- markets=markets,
- custom_holidays=custom_holidays,
- adjust=True,
- following=False,
- )
- for d in copydata.index[1:-1]
- ]
- + [copydata.index[-1]],
- )
- return DatetimeIndex(dates.drop_duplicates())
-
-"""The OpenFrame class."""
-
-from __future__ import annotations
-
-from copy import deepcopy
-from functools import reduce
-from logging import getLogger
-from typing import TYPE_CHECKING, Any, Self, cast
-
-from numpy import (
- array,
- asarray,
- bool_,
- concatenate,
- corrcoef,
- cov,
- diff,
- divide,
- float64,
- isinf,
- isnan,
- linalg,
- log,
- nan,
- sqrt,
- std,
-)
-from pandas import (
- DataFrame,
- DatetimeIndex,
- Index,
- MultiIndex,
- Series,
- concat,
- merge,
-)
-
-if TYPE_CHECKING: # pragma: no cover
- import datetime as dt
-
- from numpy.typing import NDArray
- from pandas import Series as _Series
- from pandas import Timestamp
-
- SeriesFloat = _Series[float]
-else:
- SeriesFloat = Series
-
-from pydantic import field_validator
-from sklearn.linear_model import LinearRegression # type: ignore[import-untyped]
-
-from ._common_model import _calculate_time_factor, _CommonModel, _get_base_column_data
-from .datefixer import _do_resample_to_business_period_ends
-from .owntypes import (
- DaysInYearType,
- LabelsNotUniqueError,
- LiteralBizDayFreq,
- LiteralCaptureRatio,
- LiteralFrameProps,
- LiteralHowMerge,
- LiteralPandasReindexMethod,
- LiteralPortfolioWeightings,
- LiteralTrunc,
- MaxDiversificationNaNError,
- MaxDiversificationNegativeWeightsError,
- MergingResultedInEmptyError,
- MixedValuetypesError,
- MultipleCurrenciesError,
- NoWeightsError,
- OpenFramePropertiesList,
- PortfolioItemsNotWithinFrameError,
- RatioInputError,
- ResampleDataLossError,
- ValueType,
- WeightsNotProvidedError,
-)
-from .series import OpenTimeSeries
-
-logger = getLogger(__name__)
-
-__all__ = ["OpenFrame"]
-
-
-
-[docs]
-class OpenFrame(_CommonModel[SeriesFloat]):
- """OpenFrame objects hold OpenTimeSeries in the list constituents.
-
- The intended use is to allow comparisons across these timeseries.
-
- Args:
- constituents: List of objects of Class OpenTimeSeries.
- weights: List of weights in float format. Optional.
- """
-
- @field_validator("constituents")
- @classmethod
- def _check_labels_unique(
- cls: type[OpenFrame],
- tseries: list[OpenTimeSeries],
- ) -> list[OpenTimeSeries]:
- """Pydantic validator ensuring that OpenFrame labels are unique."""
- labls = [x.label for x in tseries]
- if len(set(labls)) != len(labls):
- msg = "TimeSeries names/labels must be unique"
- raise LabelsNotUniqueError(msg)
- return tseries
-
-
-[docs]
- def __init__(
- self: Self,
- constituents: list[OpenTimeSeries],
- weights: list[float] | None = None,
- ) -> None:
- """OpenFrame objects hold OpenTimeSeries in the list constituents.
-
- The intended use is to allow comparisons across these timeseries.
-
- Args:
- constituents: List of objects of Class OpenTimeSeries.
- weights: List of weights in float format. Optional.
- """
- copied_constituents = [ts.from_deepcopy() for ts in constituents]
-
- super().__init__(
- constituents=copied_constituents,
- weights=weights,
- )
- self._set_tsdf()
-
-
- def _set_tsdf(self: Self) -> None:
- """Set the tsdf DataFrame."""
- if self.constituents is not None and len(self.constituents) != 0:
- if len(self.constituents) == 1:
- self.tsdf = self.constituents[0].tsdf.copy()
- else:
- self.tsdf = concat(
- [x.tsdf for x in self.constituents], axis="columns", sort=True
- )
- else:
- logger.warning("OpenFrame() was passed an empty list.")
-
- def _coerce_result(
- self: Self,
- result: Series[float],
- name: str,
- ) -> SeriesFloat:
- return Series(
- data=result,
- index=self.tsdf.columns,
- name=name,
- dtype="float64",
- )
-
-
-[docs]
- def from_deepcopy(self: Self) -> Self:
- """Create copy of the OpenFrame object.
-
- Returns:
- An OpenFrame object.
- """
- return deepcopy(self)
-
-
-
-[docs]
- def merge_series(
- self: Self,
- how: LiteralHowMerge = "outer",
- ) -> Self:
- """Merge index of Pandas Dataframes of the constituent OpenTimeSeries.
-
- Args:
- how: The Pandas merge method. Defaults to "outer".
-
- Returns:
- An OpenFrame object.
- """
- lvl_zero = list(self.columns_lvl_zero)
- self.tsdf = reduce(
- lambda left, right: merge(
- left=left,
- right=right,
- how=how,
- left_index=True,
- right_index=True,
- ),
- [x.tsdf for x in self.constituents],
- )
-
- mapper = dict(zip(self.columns_lvl_zero, lvl_zero, strict=True))
- self.tsdf = self.tsdf.rename(columns=mapper, level=0)
-
- if self.tsdf.empty:
- msg = (
- "Merging OpenTimeSeries DataFrames with "
- f"argument how={how} produced an empty DataFrame."
- )
- raise MergingResultedInEmptyError(msg)
-
- if how == "inner":
- for xerie in self.constituents:
- xerie.tsdf = xerie.tsdf.loc[self.tsdf.index]
- return self
-
-
-
-[docs]
- def all_properties(
- self: Self,
- properties: list[LiteralFrameProps] | None = None,
- ) -> DataFrame:
- """Calculate chosen timeseries properties.
-
- Args:
- properties: The properties to calculate. Defaults to calculating all
- available. Optional.
-
- Returns:
- Properties of the constituent OpenTimeSeries.
- """
- if properties:
- props = OpenFramePropertiesList(*properties)
- prop_list = [getattr(self, x) for x in props]
- else:
- prop_list = [
- getattr(self, x) for x in OpenFramePropertiesList.allowed_strings
- ]
- return cast("DataFrame", concat(prop_list, axis="columns").T)
-
-
- @property
- def lengths_of_items(self: Self) -> Series[int]:
- """Number of observations of all constituents.
-
- Returns:
- Number of observations of all constituents.
- """
- return Series(
- data=[self.tsdf[col].count() for col in self.tsdf.columns],
- index=self.tsdf.columns,
- name="observations",
- ).astype(int)
-
- @property
- def item_count(self: Self) -> int:
- """Number of constituents.
-
- Returns:
- Number of constituents.
- """
- return len(self.constituents)
-
- @property
- def columns_lvl_zero(self: Self) -> list[str]:
- """Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-
- Returns:
- Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
- """
- return list(self.tsdf.columns.get_level_values(0))
-
- @property
- def columns_lvl_one(self: Self) -> list[ValueType]:
- """Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-
- Returns:
- Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
- """
- return list(self.tsdf.columns.get_level_values(1))
-
- @property
- def _value_types(self: Self) -> list[bool]:
- """Cached value type checks for efficiency.
-
- Returns:
- List of booleans indicating if each column is ValueType.RTRN.
- """
- return [x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)]
-
- @property
- def first_indices(self: Self) -> Series[dt.date]:
- """The first dates in the timeseries of all constituents.
-
- Returns:
- The first dates in the timeseries of all constituents.
- """
- return Series(
- data=[i.first_idx for i in self.constituents],
- index=self.tsdf.columns,
- name="first indices",
- dtype="datetime64[ns]",
- ).dt.date
-
- @property
- def last_indices(self: Self) -> Series[dt.date]:
- """The last dates in the timeseries of all constituents.
-
- Returns:
- The last dates in the timeseries of all constituents.
- """
- return Series(
- data=[i.last_idx for i in self.constituents],
- index=self.tsdf.columns,
- name="last indices",
- dtype="datetime64[ns]",
- ).dt.date
-
- @property
- def span_of_days_all(self: Self) -> Series[int]:
- """Number of days from the first date to the last for all items in the frame.
-
- Returns:
- Number of days from the first date to the last for all
- items in the frame.
- """
- return Series(
- data=[c.span_of_days for c in self.constituents],
- index=self.tsdf.columns,
- name="span of days",
- ).astype(int)
-
-
-[docs]
- def value_to_ret(self: Self) -> Self:
- """Convert series of values into series of returns.
-
- Returns:
- The returns of the values in the series.
- """
- returns = self.tsdf.ffill().pct_change()
- returns.iloc[0] = 0
- new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
- arrays = cast(
- "Any",
- [
- self.tsdf.columns.get_level_values(0),
- new_labels,
- ],
- )
- returns.columns = MultiIndex.from_arrays(arrays)
- self.tsdf = returns.copy()
- return self
-
-
-
-[docs]
- def value_to_diff(self: Self, periods: int = 1) -> Self:
- """Convert series of values to series of their period differences.
-
- Args:
- periods: The number of periods between observations over which
- difference is calculated. Defaults to 1.
-
- Returns:
- An OpenFrame object.
- """
- self.tsdf = self.tsdf.diff(periods=periods)
- self.tsdf.iloc[0] = 0
- new_labels: list[ValueType] = [ValueType.RTRN] * self.item_count
- arrays = cast(
- "Any",
- [
- self.tsdf.columns.get_level_values(0),
- new_labels,
- ],
- )
- self.tsdf.columns = MultiIndex.from_arrays(arrays)
- return self
-
-
-
-[docs]
- def to_cumret(self: Self) -> Self:
- """Convert series of returns into cumulative series of values.
-
- Returns:
- An OpenFrame object.
- """
- vtypes = self._value_types
- if not any(vtypes):
- returns = self.tsdf.ffill().pct_change()
- returns.iloc[0] = 0
- elif all(vtypes):
- returns = self.tsdf.copy()
- returns.iloc[0] = 0
- else:
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- returns = returns.add(1.0)
- self.tsdf = returns.cumprod(axis=0) / returns.iloc[0]
-
- new_labels: list[ValueType] = [ValueType.PRICE] * self.item_count
- arrays = cast(
- "Any",
- [
- self.tsdf.columns.get_level_values(0),
- new_labels,
- ],
- )
- self.tsdf.columns = MultiIndex.from_arrays(arrays)
- return self
-
-
-
-[docs]
- def resample(
- self: Self,
- freq: LiteralBizDayFreq | str = "BME",
- ) -> Self:
- """Resample the timeseries frequency.
-
- Args:
- freq: The date offset string that sets the resampled frequency.
- Defaults to "BME".
-
- Returns:
- An OpenFrame object.
- """
- vtypes = self._value_types
- if not any(vtypes):
- value_type = ValueType.PRICE
- elif all(vtypes):
- value_type = ValueType.RTRN
- else:
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- self.tsdf.index = DatetimeIndex(self.tsdf.index)
- if value_type == ValueType.PRICE:
- self.tsdf = self.tsdf.resample(freq).last()
- else:
- self.tsdf = self.tsdf.resample(freq).sum()
- self.tsdf.index = Index(DatetimeIndex(self.tsdf.index).date)
- for xerie in self.constituents:
- xerie.tsdf.index = DatetimeIndex(xerie.tsdf.index)
- if value_type == ValueType.PRICE:
- xerie.tsdf = xerie.tsdf.resample(freq).last()
- else:
- xerie.tsdf = xerie.tsdf.resample(freq).sum()
- xerie.tsdf.index = Index(DatetimeIndex(xerie.tsdf.index).date)
-
- return self
-
-
-
-[docs]
- def resample_to_business_period_ends(
- self: Self,
- freq: LiteralBizDayFreq = "BME",
- method: LiteralPandasReindexMethod = "nearest",
- ) -> Self:
- """Resamples timeseries frequency to the business calendar month end dates.
-
- Stubs left in place. Stubs will be aligned to the shortest stub.
-
- Args:
- freq: The date offset string that sets the resampled frequency.
- Defaults to "BME".
- method: Controls the method used to align values across columns.
- Defaults to nearest.
-
- Returns:
- An OpenFrame object.
- """
- vtypes = self._value_types
- if any(vtypes):
- msg = (
- "Do not run resample_to_business_period_ends on return series. "
- "The operation will pick the last data point in the sparser series. "
- "It will not sum returns and therefore data will be lost."
- )
- raise ResampleDataLossError(msg)
-
- for xerie in self.constituents:
- dates = _do_resample_to_business_period_ends(
- data=xerie.tsdf,
- freq=freq,
- countries=xerie.countries,
- markets=xerie.markets,
- )
- xerie.tsdf = xerie.tsdf.reindex(
- [deyt.date() for deyt in dates],
- method=method,
- )
-
- arrays = [
- self.tsdf.columns.get_level_values(0),
- self.tsdf.columns.get_level_values(1),
- ]
-
- self._set_tsdf()
-
- self.tsdf.columns = MultiIndex.from_arrays(arrays)
-
- return self
-
-
-
-[docs]
- def ewma_risk(
- self: Self,
- lmbda: float = 0.94,
- day_chunk: int = 11,
- dlta_degr_freedms: int = 0,
- first_column: int = 0,
- second_column: int = 1,
- corr_scale: float = 2.0,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> DataFrame:
- """Exponentially Weighted Moving Average Volatilities and Correlation.
-
- Exponentially Weighted Moving Average (EWMA) for Volatilities and
- Correlation.
-
- Reference: https://www.investopedia.com/articles/07/ewma.asp.
-
- Args:
- lmbda: Scaling factor to determine weighting. Defaults to 0.94.
- day_chunk: Sampling the data which is assumed to be daily. Defaults to 11.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 0.
- first_column: Column of first timeseries. Defaults to 0.
- second_column: Column of second timeseries. Defaults to 1.
- corr_scale: Correlation scale factor. Defaults to 2.0.
- months_from_last: Number of months offset as positive integer. Overrides
- use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Series volatilities and correlation.
- """
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
- if periods_in_a_year_fixed is None:
- fraction = (later - earlier).days / 365.25
- how_many = (
- self.tsdf.loc[cast("Timestamp", earlier) : cast("Timestamp", later)]
- .count()
- .iloc[0]
- )
- time_factor = how_many / fraction
- else:
- time_factor = periods_in_a_year_fixed
-
- corr_label = (
- cast("tuple[str, str]", self.tsdf.iloc[:, first_column].name)[0]
- + "_VS_"
- + cast("tuple[str, str]", self.tsdf.iloc[:, second_column].name)[0]
- )
- cols = [
- cast("tuple[str, str]", self.tsdf.iloc[:, first_column].name)[0],
- cast("tuple[str, str]", self.tsdf.iloc[:, second_column].name)[0],
- ]
-
- data = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ].copy()
-
- for rtn in cols:
- arr = concatenate([array([nan]), diff(log(data[(rtn, ValueType.PRICE)]))])
- data[rtn, ValueType.RTRN] = arr
-
- raw_one = [
- data[(cols[0], ValueType.RTRN)]
- .iloc[1:day_chunk]
- .std(ddof=dlta_degr_freedms)
- * sqrt(time_factor),
- ]
- raw_two = [
- data[(cols[1], ValueType.RTRN)]
- .iloc[1:day_chunk]
- .std(ddof=dlta_degr_freedms)
- * sqrt(time_factor),
- ]
- rm = data[(cols[0], ValueType.RTRN)].iloc[1:day_chunk]
- m: NDArray[float64] = asarray(rm, dtype=float64)
- ry = data[(cols[1], ValueType.RTRN)].iloc[1:day_chunk]
- y: NDArray[float64] = asarray(ry, dtype=float64)
-
- raw_cov = [cov(m=m, y=y, ddof=dlta_degr_freedms)[0][1]]
-
- r1 = data[(cols[0], ValueType.RTRN)]
- r2 = data[(cols[1], ValueType.RTRN)]
-
- alpha = 1.0 - lmbda
-
- s1 = r1.pow(2) * time_factor
- s2 = r2.pow(2) * time_factor
- sc = r1 * r2 * time_factor
-
- s1.iloc[0] = float(raw_one[0] ** 2)
- s2.iloc[0] = float(raw_two[0] ** 2)
- sc.iloc[0] = float(raw_cov[0])
-
- m1 = s1.ewm(alpha=alpha, adjust=False).mean()
- m2 = s2.ewm(alpha=alpha, adjust=False).mean()
- mc = sc.ewm(alpha=alpha, adjust=False).mean()
-
- m1v = m1.to_numpy(copy=False)
- m2v = m2.to_numpy(copy=False)
- mcv = mc.to_numpy(copy=False)
-
- vol1 = sqrt(m1v)
- vol2 = sqrt(m2v)
- denom = corr_scale * vol1 * vol2
-
- corr = mcv / denom
- corr[denom == 0.0] = nan
-
- return DataFrame(
- index=[*cols, corr_label],
- columns=data.index,
- data=[vol1, vol2, corr],
- ).T
-
-
- @property
- def correl_matrix(self: Self) -> DataFrame:
- """Correlation matrix.
-
- This property returns the correlation matrix of the time series
- in the frame.
-
- Returns:
- Correlation matrix of the time series in the frame.
- """
- corr_matrix = (
- self.tsdf.ffill()
- .pct_change()
- .corr(
- method="pearson",
- min_periods=1,
- )
- )
- corr_matrix.columns = corr_matrix.columns.get_level_values(0)
- corr_matrix.index = corr_matrix.index.get_level_values(0)
- corr_matrix.index.name = "Correlation"
- return corr_matrix
-
-
-[docs]
- def add_timeseries(
- self: Self,
- new_series: OpenTimeSeries,
- ) -> Self:
- """To add an OpenTimeSeries object.
-
- Args:
- new_series: The timeseries to add.
-
- Returns:
- An OpenFrame object.
- """
- self.constituents += [new_series]
- self.tsdf = concat([self.tsdf, new_series.tsdf], axis="columns", sort=True)
- return self
-
-
-
-[docs]
- def delete_timeseries(self: Self, lvl_zero_item: str) -> Self:
- """To delete an OpenTimeSeries object.
-
- Args:
- lvl_zero_item: The .tsdf column level 0 value of the timeseries to delete.
-
- Returns:
- An OpenFrame object.
- """
- if self.weights:
- new_c, new_w = [], []
- for serie, weight in zip(self.constituents, self.weights, strict=True):
- if serie.label != lvl_zero_item:
- new_c.append(serie)
- new_w.append(weight)
- self.constituents = new_c
- self.weights = new_w
- else:
- self.constituents = [
- item for item in self.constituents if item.label != lvl_zero_item
- ]
- self.tsdf = self.tsdf.drop(lvl_zero_item, axis="columns", level=0)
- return self
-
-
-
-[docs]
- def trunc_frame(
- self: Self,
- start_cut: dt.date | None = None,
- end_cut: dt.date | None = None,
- where: LiteralTrunc = "both",
- ) -> Self:
- """Truncate DataFrame such that all timeseries have the same time span.
-
- Args:
- start_cut: New first date. Optional.
- end_cut: New last date. Optional.
- where: Determines where dataframe is truncated also when start_cut
- or end_cut is None. Defaults to both.
-
- Returns:
- An OpenFrame object.
- """
- if not start_cut and where in ["before", "both"]:
- start_cut = self.first_indices.max()
- if not end_cut and where in ["after", "both"]:
- end_cut = self.last_indices.min()
- self.tsdf = self.tsdf.sort_index()
- self.tsdf = self.tsdf.truncate(before=start_cut, after=end_cut)
-
- for xerie in self.constituents:
- xerie.tsdf = xerie.tsdf.truncate(
- before=start_cut,
- after=end_cut,
- )
- if len(set(self.first_indices)) != 1:
- msg = (
- f"One or more constituents still "
- f"not truncated to same start dates.\n"
- f"{self.tsdf.head()}"
- )
- logger.warning(msg)
- if len(set(self.last_indices)) != 1:
- msg = (
- f"One or more constituents still "
- f"not truncated to same end dates.\n"
- f"{self.tsdf.tail()}"
- )
- logger.warning(msg)
- return self
-
-
-
-[docs]
- def relative(
- self: Self,
- long_column: int = 0,
- short_column: int = 1,
- *,
- base_zero: bool = True,
- ) -> None:
- """Calculate cumulative relative return between two series.
-
- Args:
- long_column: Column number of timeseries bought. Defaults to 0.
- short_column: Column number of timeseries sold. Defaults to 1.
- base_zero: If set to False 1.0 is added to allow for a capital base and
- to allow a volatility calculation. Defaults to True.
- """
- rel_label = (
- cast("tuple[str, str]", self.tsdf.iloc[:, long_column].name)[0]
- + "_over_"
- + cast("tuple[str, str]", self.tsdf.iloc[:, short_column].name)[0]
- )
- if base_zero:
- self.tsdf[rel_label, ValueType.RELRTRN] = (
- self.tsdf.iloc[:, long_column] - self.tsdf.iloc[:, short_column]
- )
- else:
- self.tsdf[rel_label, ValueType.RELRTRN] = (
- 1.0 + self.tsdf.iloc[:, long_column] - self.tsdf.iloc[:, short_column]
- )
- self.constituents += [
- OpenTimeSeries.from_df(self.tsdf.iloc[:, -1]),
- ]
-
-
-
-[docs]
- def tracking_error_func(
- self: Self,
- base_column: tuple[str, ValueType] | int = -1,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> Series[float]:
- """Tracking Error.
-
- Calculates Tracking Error which is the standard deviation of the
- difference between the fund and its index returns.
-
- Reference: https://www.investopedia.com/terms/t/trackingerror.asp.
-
- Args:
- base_column: Column of timeseries that is the denominator in the ratio.
- Defaults to -1.
- months_from_last: Number of months offset as positive integer. Overrides
- use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Tracking Errors.
- """
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
-
- shortdf, short_item, short_label = _get_base_column_data(
- self=self,
- base_column=base_column,
- earlier=earlier,
- later=later,
- )
-
- time_factor = _calculate_time_factor(
- data=shortdf,
- earlier=earlier,
- later=later,
- periods_in_a_year_fixed=periods_in_a_year_fixed,
- )
-
- shortdf_returns = shortdf.ffill().pct_change()
-
- terrors = []
- for item in self.tsdf:
- if item == short_item:
- terrors.append(0.0)
- else:
- longdf = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ][item]
- relative = longdf.ffill().pct_change() - shortdf_returns
- vol = float(relative.std() * sqrt(time_factor))
- terrors.append(vol)
-
- return Series(
- data=terrors,
- index=self.tsdf.columns,
- name=f"Tracking Errors vs {short_label}",
- dtype="float64",
- )
-
-
-
-[docs]
- def info_ratio_func(
- self: Self,
- base_column: tuple[str, ValueType] | int = -1,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> Series[float]:
- """Information Ratio.
-
- The Information Ratio equals ( fund return less index return ) divided
- by the Tracking Error. And the Tracking Error is the standard deviation of
- the difference between the fund and its index returns.
- The ratio is calculated using the annualized arithmetic mean of returns.
-
- Args:
- base_column: Column of timeseries that is the denominator in the ratio.
- Defaults to -1.
- months_from_last: Number of months offset as positive integer. Overrides
- use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Information Ratios.
- """
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
-
- shortdf, short_item, short_label = _get_base_column_data(
- self=self,
- base_column=base_column,
- earlier=earlier,
- later=later,
- )
-
- time_factor = _calculate_time_factor(
- data=shortdf,
- earlier=earlier,
- later=later,
- periods_in_a_year_fixed=periods_in_a_year_fixed,
- )
-
- shortdf_returns = shortdf.ffill().pct_change()
-
- ratios = []
- for item in self.tsdf:
- if item == short_item:
- ratios.append(0.0)
- else:
- longdf = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ][item]
- relative = longdf.ffill().pct_change() - shortdf_returns
- ret = float(relative.mean() * time_factor)
- vol = float(relative.std() * sqrt(time_factor))
- ratios.append(ret / vol)
-
- return Series(
- data=ratios,
- index=self.tsdf.columns,
- name=f"Info Ratios vs {short_label}",
- dtype="float64",
- )
-
-
- def _calculate_cagr_from_returns(
- self: Self,
- returns_array: NDArray[float64],
- mask: NDArray[bool_],
- time_factor: float,
- ) -> float:
- """Calculate CAGR from returns array with mask.
-
- Args:
- returns_array: Returns array.
- mask: Boolean mask.
- time_factor: Time factor for annualization.
-
- Returns:
- CAGR value.
- """
- masked_array = returns_array[mask] + 1.0
- if len(masked_array) == 0:
- return 0.0
- exponent = 1 / (len(masked_array) / time_factor)
- return float(masked_array.prod() ** exponent - 1)
-
- def _calculate_capture_ratio_for_item(
- self: Self,
- ratio: LiteralCaptureRatio,
- longdf_returns_np: NDArray[float64],
- shortdf_returns_np: NDArray[float64],
- up_mask: NDArray[bool_],
- down_mask: NDArray[bool_],
- time_factor: float,
- ) -> float:
- """Calculate capture ratio for a single item.
-
- Args:
- ratio: Ratio type to calculate.
- longdf_returns_np: Long returns array.
- shortdf_returns_np: Short returns array.
- up_mask: Up mask.
- down_mask: Down mask.
- time_factor: Time factor.
-
- Returns:
- Capture ratio value.
-
- Raises:
- RatioInputError: If ratio is invalid.
- """
- if ratio == "up":
- up_rtrn = self._calculate_cagr_from_returns(
- longdf_returns_np, up_mask, time_factor
- )
- up_idx_return = self._calculate_cagr_from_returns(
- shortdf_returns_np, up_mask, time_factor
- )
- if up_idx_return == 0.0:
- return 0.0
- return up_rtrn / up_idx_return
-
- if ratio == "down":
- down_return = self._calculate_cagr_from_returns(
- longdf_returns_np, down_mask, time_factor
- )
- down_idx_return = self._calculate_cagr_from_returns(
- shortdf_returns_np, down_mask, time_factor
- )
- if down_idx_return == 0.0:
- return 0.0
- return down_return / down_idx_return
-
- if ratio == "both":
- up_rtrn = self._calculate_cagr_from_returns(
- longdf_returns_np, up_mask, time_factor
- )
- up_idx_return = self._calculate_cagr_from_returns(
- shortdf_returns_np, up_mask, time_factor
- )
- down_return = self._calculate_cagr_from_returns(
- longdf_returns_np, down_mask, time_factor
- )
- down_idx_return = self._calculate_cagr_from_returns(
- shortdf_returns_np, down_mask, time_factor
- )
- if up_idx_return == 0.0 or down_idx_return == 0.0:
- return 0.0
- return (up_rtrn / up_idx_return) / (down_return / down_idx_return)
-
- msg = "ratio must be one of 'up', 'down' or 'both'."
- raise RatioInputError(msg)
-
-
-[docs]
- def capture_ratio_func(
- self: Self,
- ratio: LiteralCaptureRatio,
- base_column: tuple[str, ValueType] | int = -1,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> Series[float]:
- """Capture Ratio.
-
- The Up (Down) Capture Ratio is calculated by dividing the CAGR
- of the asset during periods that the benchmark returns are positive (negative)
- by the CAGR of the benchmark during the same periods.
- CaptureRatio.BOTH is the Up ratio divided by the Down ratio.
- Source: 'Capture Ratios: A Popular Method of Measuring Portfolio Performance
- in Practice', Don R. Cox and Delbert C. Goff, Journal of Economics and
- Finance Education (Vol 2 Winter 2013).
-
- Reference: https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf.
-
- Args:
- ratio: The ratio to calculate.
- base_column: Column of timeseries that is the denominator in the ratio.
- Defaults to -1.
- months_from_last: Number of months offset as positive integer. Overrides
- use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Capture Ratios.
- """
- loss_limit: float = 0.0
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
- fraction: float = (later - earlier).days / 365.25
-
- shortdf, short_item, short_label = _get_base_column_data(
- self=self,
- base_column=base_column,
- earlier=earlier,
- later=later,
- )
-
- if periods_in_a_year_fixed:
- time_factor = float(periods_in_a_year_fixed)
- else:
- time_factor = shortdf.count() / fraction
-
- shortdf_returns = shortdf.ffill().pct_change()
- shortdf_returns_np = cast("NDArray[float64]", shortdf_returns.to_numpy())
- up_mask = shortdf_returns_np > loss_limit
- down_mask = shortdf_returns_np < loss_limit
-
- ratios = []
- for item in self.tsdf:
- if item == short_item:
- ratios.append(0.0)
- else:
- longdf = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ][item]
- longdf_returns = longdf.ffill().pct_change()
- longdf_returns_np = cast("NDArray[float64]", longdf_returns.to_numpy())
- ratio_value = self._calculate_capture_ratio_for_item(
- ratio=ratio,
- longdf_returns_np=longdf_returns_np,
- shortdf_returns_np=shortdf_returns_np,
- up_mask=up_mask,
- down_mask=down_mask,
- time_factor=time_factor,
- )
- ratios.append(ratio_value)
-
- ratio_names = {
- "up": f"Up Capture Ratios vs {short_label}",
- "down": f"Down Capture Ratios vs {short_label}",
- "both": f"Up-Down Capture Ratios vs {short_label}",
- }
- resultname = ratio_names[ratio]
-
- return Series(
- data=ratios,
- index=self.tsdf.columns,
- name=resultname,
- dtype="float64",
- )
-
-
- def _extract_column_value(
- self: Self,
- column: tuple[str, ValueType] | int,
- vtypes: list[bool],
- param_name: str = "column",
- ) -> Series[float]:
- """Extract column value based on value types.
-
- Args:
- column: Column reference.
- vtypes: Value types list.
- param_name: Parameter name for error messages.
-
- Returns:
- Series value.
-
- Raises:
- TypeError: If column type is invalid.
- """
- msg = f"{param_name} should be a tuple[str, ValueType] or an integer."
- if isinstance(column, tuple):
- if all(vtypes):
- return self.tsdf[column]
- return self.tsdf[column].ffill().pct_change().iloc[1:]
- if isinstance(column, int):
- if all(vtypes):
- return self.tsdf.iloc[:, column]
- return self.tsdf.iloc[:, column].ffill().pct_change().iloc[1:]
- raise TypeError(msg)
-
-
-[docs]
- def beta(
- self: Self,
- asset: tuple[str, ValueType] | int,
- market: tuple[str, ValueType] | int,
- dlta_degr_freedms: int = 1,
- ) -> float:
- """Market Beta.
-
- Calculates Beta as Co-variance of asset & market divided by Variance
- of the market.
-
- Reference: https://www.investopedia.com/terms/b/beta.asp.
-
- Args:
- asset: The column of the asset.
- market: The column of the market against which Beta is measured.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 1.
-
- Returns:
- Beta as Co-variance of x & y divided by Variance of x.
- """
- vtypes = self._value_types
- if not (all(vtypes) or not any(vtypes)):
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- y_value = self._extract_column_value(asset, vtypes, param_name="asset")
- x_value = self._extract_column_value(market, vtypes, param_name="market")
-
- covariance = cov(m=y_value, y=x_value, ddof=dlta_degr_freedms)
- beta = covariance[0, 1] / covariance[1, 1]
-
- return float(beta)
-
-
-
-[docs]
- def ord_least_squares_fit(
- self: Self,
- y_column: tuple[str, ValueType] | int,
- x_column: tuple[str, ValueType] | int,
- *,
- fitted_series: bool = True,
- ) -> dict[str, float]:
- """Ordinary Least Squares fit.
-
- Performs a linear regression and adds a new column with a fitted line
- using Ordinary Least Squares fit.
-
- Args:
- y_column: The column level values of the dependent variable y.
- x_column: The column level values of the exogenous variable x.
- fitted_series: If True the fit is added as a new column in the .tsdf
- Pandas.DataFrame. Defaults to True.
-
- Returns:
- A dictionary with the coefficient, intercept and rsquared outputs.
- """
- msg = "y_column should be a tuple[str, ValueType] or an integer."
- if isinstance(y_column, tuple):
- y_value = self.tsdf[y_column].to_numpy()
- y_label = cast(
- "tuple[str, str]",
- self.tsdf[y_column].name,
- )[0]
- elif isinstance(y_column, int):
- y_value = self.tsdf.iloc[:, y_column].to_numpy()
- y_label = cast("tuple[str, str]", self.tsdf.iloc[:, y_column].name)[0]
- else:
- raise TypeError(msg)
-
- msg = "x_column should be a tuple[str, ValueType] or an integer."
- if isinstance(x_column, tuple):
- x_value = self.tsdf[x_column].to_numpy().reshape(-1, 1)
- x_label = cast(
- "tuple[str, str]",
- self.tsdf[x_column].name,
- )[0]
- elif isinstance(x_column, int):
- x_value = self.tsdf.iloc[:, x_column].to_numpy().reshape(-1, 1)
- x_label = cast("tuple[str, str]", self.tsdf.iloc[:, x_column].name)[0]
- else:
- raise TypeError(msg)
-
- model = LinearRegression(fit_intercept=True)
- model.fit(x_value, y_value)
- if fitted_series:
- self.tsdf[y_label, x_label] = model.predict(x_value)
- return {
- "coefficient": float(model.coef_[0]),
- "intercept": float(model.intercept_),
- "rsquared": model.score(x_value, y_value),
- }
-
-
-
-[docs]
- def jensen_alpha(
- self: Self,
- asset: tuple[str, ValueType] | int,
- market: tuple[str, ValueType] | int,
- riskfree_rate: float = 0.0,
- dlta_degr_freedms: int = 1,
- ) -> float:
- """Jensen's alpha.
-
- The Jensen's measure, or Jensen's alpha, is a risk-adjusted performance
- measure that represents the average return on a portfolio or investment,
- above or below that predicted by the capital asset pricing model (CAPM),
- given the portfolio's or investment's beta and the average market return.
- This metric is also commonly referred to as simply alpha.
-
- Reference: https://www.investopedia.com/terms/j/jensensmeasure.asp.
-
- Args:
- asset: The column of the asset.
- market: The column of the market against which Jensen's alpha is measured.
- riskfree_rate: The return of the zero volatility riskfree asset.
- Defaults to 0.0.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 1.
-
- Returns:
- Jensen's alpha.
- """
- vtypes = self._value_types
- if not (all(vtypes) or not any(vtypes)):
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- asset_rtn = self._extract_column_value(asset, vtypes, param_name="asset")
- market_rtn = self._extract_column_value(market, vtypes, param_name="market")
-
- asset_rtn_mean = float(asset_rtn.mean() * self.periods_in_a_year)
- market_rtn_mean = float(market_rtn.mean() * self.periods_in_a_year)
-
- covariance = cov(m=asset_rtn, y=market_rtn, ddof=dlta_degr_freedms)
- beta = covariance[0, 1] / covariance[1, 1]
-
- return float(
- asset_rtn_mean - riskfree_rate - beta * (market_rtn_mean - riskfree_rate),
- )
-
-
- def _prepare_returns_for_portfolio(self: Self) -> DataFrame:
- """Prepare returns DataFrame for portfolio calculation.
-
- Returns:
- Returns DataFrame.
-
- Raises:
- MixedValuetypesError: If series types are mixed.
- """
- vtypes = self._value_types
- if not any(vtypes):
- returns = self.tsdf.ffill().pct_change()
- returns.iloc[0] = 0
- return returns
- if all(vtypes):
- return self.tsdf
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- def _calculate_eq_weights(self: Self) -> list[float]:
- """Calculate equal weights.
-
- Returns:
- List of equal weights.
- """
- return [1.0 / self.item_count] * self.item_count
-
- def _calculate_inv_vol_weights(self: Self, returns: DataFrame) -> list[float]:
- """Calculate inverse volatility weights.
-
- Args:
- returns: Returns DataFrame.
-
- Returns:
- List of inverse volatility weights.
- """
- vol = divide(1.0, std(returns, axis=0, ddof=1))
- vol[isinf(vol)] = nan
- return list(divide(vol, vol.sum()))
-
- def _calculate_max_div_weights(self: Self, returns: DataFrame) -> list[float]:
- """Calculate maximum diversification weights.
-
- Args:
- returns: Returns DataFrame.
-
- Returns:
- List of maximum diversification weights.
-
- Raises:
- MaxDiversificationNaNError: If correlation matrix has NaN values.
- MaxDiversificationNegativeWeightsError: If weights are negative.
- """
- corr_matrix = corrcoef(returns.T)
- corr_matrix[isinf(corr_matrix)] = nan
- corr_matrix[isnan(corr_matrix)] = nan
-
- msga = "max_div weight strategy failed: correlation matrix contains NaN values"
- if isnan(corr_matrix).any():
- raise MaxDiversificationNaNError(msga)
-
- try:
- inv_corr_sum = linalg.inv(corr_matrix).sum(axis=1)
-
- msgb = (
- "max_div weight strategy failed: "
- "inverse correlation matrix sum contains NaN values"
- )
- if isnan(inv_corr_sum).any():
- raise MaxDiversificationNaNError(msgb)
-
- weights = list(divide(inv_corr_sum, inv_corr_sum.sum()))
-
- msgc = "max_div weight strategy failed: final weights contain NaN values"
- if any(isnan(weight) for weight in weights): # pragma: no cover
- raise MaxDiversificationNaNError(msgc)
-
- msgd = (
- "max_div weight strategy failed: negative weights detected"
- f" - weights: {[round(w, 6) for w in weights]}"
- )
- if any(weight < 0 for weight in weights):
- raise MaxDiversificationNegativeWeightsError(msgd)
-
- except linalg.LinAlgError as e:
- msge = (
- "max_div weight strategy failed: "
- f"correlation matrix is singular - {e!s}"
- )
- raise MaxDiversificationNaNError(msge) from e
- else:
- return weights
-
- def _calculate_min_vol_overweight_weights(
- self: Self,
- returns: DataFrame,
- ) -> list[float]:
- """Calculate minimum volatility overweight weights.
-
- Args:
- returns: Returns DataFrame.
-
- Returns:
- List of minimum volatility overweight weights.
- """
- vols = std(returns, axis=0, ddof=1)
- min_vol_idx = vols.argmin()
- min_vol_weight = 0.6
- remaining_weight = 0.4
- weights = [remaining_weight / (self.item_count - 1)] * self.item_count
- weights[min_vol_idx] = min_vol_weight
- return weights
-
- def _calculate_weights_from_strategy(
- self: Self,
- weight_strat: LiteralPortfolioWeightings,
- returns: DataFrame,
- ) -> list[float]:
- """Calculate weights based on strategy.
-
- Args:
- weight_strat: Weight calculation strategy.
- returns: Returns DataFrame.
-
- Returns:
- List of weights.
-
- Raises:
- NotImplementedError: If strategy is not implemented.
- """
- if weight_strat == "eq_weights":
- return self._calculate_eq_weights()
- if weight_strat == "inv_vol":
- return self._calculate_inv_vol_weights(returns)
- if weight_strat == "max_div":
- return self._calculate_max_div_weights(returns)
- if weight_strat == "min_vol_overweight":
- return self._calculate_min_vol_overweight_weights(returns)
-
- msg = "Weight strategy not implemented"
- raise NotImplementedError(msg)
-
-
-[docs]
- def make_portfolio(
- self: Self,
- name: str,
- weight_strat: LiteralPortfolioWeightings | None = None,
- ) -> DataFrame:
- """Calculate a basket timeseries based on the supplied weights.
-
- Args:
- name: Name of the basket timeseries.
- weight_strat: Weight calculation strategies. Optional.
-
- Returns:
- A basket timeseries.
- """
- if self.weights is None and weight_strat is None:
- msg = (
- "OpenFrame weights property must be provided "
- "to run the make_portfolio method."
- )
- raise NoWeightsError(msg)
-
- returns = self._prepare_returns_for_portfolio()
-
- if weight_strat:
- self.weights = self._calculate_weights_from_strategy(
- weight_strat=weight_strat,
- returns=returns,
- )
-
- return DataFrame(
- data=(returns @ array(self.weights)).add(1.0).cumprod(),
- index=self.tsdf.index,
- columns=[[name], [ValueType.PRICE]],
- dtype="float64",
- )
-
-
-
-[docs]
- def rolling_info_ratio(
- self: Self,
- long_column: int = 0,
- short_column: int = 1,
- observations: int = 21,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> DataFrame:
- """Calculate rolling Information Ratio.
-
- The Information Ratio equals ( fund return less index return ) divided by
- the Tracking Error. And the Tracking Error is the standard deviation of the
- difference between the fund and its index returns.
-
- Args:
- long_column: Column of timeseries that is the numerator in the ratio.
- Defaults to 0.
- short_column: Column of timeseries that is the denominator in the ratio.
- Defaults to 1.
- observations: The length of the rolling window to use is set as number of
- observations. Defaults to 21.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Rolling Information Ratios.
- """
- long_label = cast(
- "tuple[str, str]",
- self.tsdf.iloc[:, long_column].name,
- )[0]
- short_label = cast(
- "tuple[str, str]",
- self.tsdf.iloc[:, short_column].name,
- )[0]
- ratio_label = f"{long_label} / {short_label}"
- if periods_in_a_year_fixed:
- time_factor = float(periods_in_a_year_fixed)
- else:
- time_factor = self.periods_in_a_year
-
- relative = (
- 1.0 + self.tsdf.iloc[:, long_column] - self.tsdf.iloc[:, short_column]
- )
-
- retseries = (
- relative.ffill()
- .pct_change()
- .rolling(observations, min_periods=observations)
- .sum()
- )
- retdf = retseries.dropna().to_frame()
-
- voldf = relative.ffill().pct_change().rolling(
- observations,
- min_periods=observations,
- ).std() * sqrt(time_factor)
- voldf = voldf.dropna().to_frame()
-
- ratiodf = (retdf.iloc[:, 0] / voldf.iloc[:, 0]).to_frame()
- ratiodf.columns = [[ratio_label], ["Information Ratio"]]
-
- return DataFrame(ratiodf)
-
-
-
-[docs]
- def rolling_beta(
- self: Self,
- asset_column: int = 0,
- market_column: int = 1,
- observations: int = 21,
- dlta_degr_freedms: int = 1,
- ) -> DataFrame:
- """Calculate rolling Market Beta.
-
- Calculates Beta as Co-variance of asset & market divided by Variance
- of the market.
-
- Reference: https://www.investopedia.com/terms/b/beta.asp.
-
- Args:
- asset_column: Column of timeseries that is the asset. Defaults to 0.
- market_column: Column of timeseries that is the market. Defaults to 1.
- observations: The length of the rolling window to use is set as number of
- observations. Defaults to 21.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 1.
-
- Returns:
- Rolling Betas.
- """
- market_label = cast("tuple[str, str]", self.tsdf.iloc[:, market_column].name)[
- 0
- ]
- asset_label = cast("tuple[str, str]", self.tsdf.iloc[:, asset_column].name)[0]
- beta_label = f"{asset_label} / {market_label}"
-
- rolling = (
- self.tsdf.ffill()
- .pct_change()
- .rolling(
- observations,
- min_periods=observations,
- )
- )
-
- rcov = rolling.cov(ddof=dlta_degr_freedms)
- rcov = rcov.dropna()
-
- rollbetaseries = rcov.iloc[:, asset_column].xs(
- market_label,
- level=1,
- ) / rcov.iloc[
- :,
- market_column,
- ].xs(
- market_label,
- level=1,
- )
- rollbeta = rollbetaseries.to_frame()
- rollbeta.index = rollbeta.index.get_level_values(0)
- rollbeta.columns = MultiIndex.from_arrays([[beta_label], ["Beta"]])
-
- return rollbeta
-
-
-
-[docs]
- def rolling_corr(
- self: Self,
- first_column: int = 0,
- second_column: int = 1,
- observations: int = 21,
- ) -> DataFrame:
- """Calculate rolling Correlation.
-
- Calculates correlation between two series. The period with
- at least the given number of observations is the first period calculated.
-
- Args:
- first_column: The position as integer of the first timeseries to compare.
- Defaults to 0.
- second_column: The position as integer of the second timeseries to compare.
- Defaults to 1.
- observations: The length of the rolling window to use is set as number of
- observations. Defaults to 21.
-
- Returns:
- Rolling Correlations.
- """
- corr_label = (
- cast("tuple[str, str]", self.tsdf.iloc[:, first_column].name)[0]
- + "_VS_"
- + cast("tuple[str, str]", self.tsdf.iloc[:, second_column].name)[0]
- )
- first_series = (
- self.tsdf.iloc[:, first_column]
- .ffill()
- .pct_change()[1:]
- .rolling(observations, min_periods=observations)
- )
- second_series = self.tsdf.iloc[:, second_column].ffill().pct_change()[1:]
- corrdf = first_series.corr(other=second_series).dropna().to_frame()
- corrdf.columns = MultiIndex.from_arrays(
- [
- [corr_label],
- ["Rolling correlation"],
- ],
- )
-
- return DataFrame(corrdf)
-
-
-
-[docs]
- def multi_factor_linear_regression(
- self: Self,
- dependent_column: tuple[str, ValueType],
- ) -> tuple[DataFrame, OpenTimeSeries]:
- """Perform a multi-factor linear regression.
-
- This function treats one specified column in the DataFrame as the dependent
- variable (y) and uses all remaining columns as independent variables (X).
- It utilizes a scikit-learn LinearRegression model and returns a DataFrame
- with summary output and an OpenTimeSeries of predicted values.
-
- Args:
- dependent_column: A tuple key to select the column in the
- OpenFrame.tsdf.columns to use as the dependent variable.
-
- Returns:
- A tuple containing:
- - A DataFrame with the R-squared, the intercept and the regression
- coefficients
- - An OpenTimeSeries of predicted values
-
- Raises:
- KeyError: If the column tuple is not found in the OpenFrame.tsdf.columns.
- ValueError: If not all series are returnseries (ValueType.RTRN).
- """
- key_msg = (
- f"Tuple ({dependent_column[0]}, "
- f"{dependent_column[1].value}) not found in data."
- )
- if dependent_column not in self.tsdf.columns:
- raise KeyError(key_msg)
-
- vtype_msg = "All series should be of ValueType.RTRN."
- if not all(x == ValueType.RTRN for x in self.tsdf.columns.get_level_values(1)):
- raise MixedValuetypesError(vtype_msg)
-
- dependent = self.tsdf[dependent_column]
- factors = self.tsdf.drop(columns=[dependent_column])
- indx = ["R-square", "Intercept", *factors.columns.get_level_values(0)]
-
- model = LinearRegression()
- model.fit(factors, dependent)
-
- predictions = OpenTimeSeries.from_arrays(
- name=f"Predicted {dependent_column[0]}",
- dates=[date.strftime("%Y-%m-%d") for date in self.tsdf.index],
- values=list(model.predict(factors)),
- valuetype=ValueType.RTRN,
- )
-
- output = [model.score(factors, dependent), model.intercept_, *model.coef_]
-
- result = DataFrame(data=output, index=indx, columns=[dependent_column[0]])
-
- return result, predictions.to_cumret()
-
-
- def _validate_and_prepare_rebalance_inputs(
- self: Self,
- items: list[str] | None,
- bal_weights: list[float] | None,
- *,
- equal_weights: bool,
- ) -> tuple[list[str], list[float]]:
- """Validate and prepare inputs for rebalanced portfolio.
-
- Args:
- items: List of items to include. If None, uses all items.
- bal_weights: List of weights. If None, uses frame weights.
- equal_weights: If True, use equal weights.
-
- Returns:
- Tuple of (validated items, validated weights).
-
- Raises:
- WeightsNotProvidedError: If weights are required but not provided.
- TypeError: If items is not a list.
- PortfolioItemsNotWithinFrameError: If items are invalid.
- """
- if bal_weights is None and not equal_weights:
- if self.weights is None:
- msg = "Weights must be provided."
- raise WeightsNotProvidedError(msg)
- bal_weights = list(self.weights)
-
- if items is None:
- items = list(self.columns_lvl_zero)
- else:
- msg = "Items must be passed as list."
- if not isinstance(items, list):
- raise TypeError(msg)
- if not items:
- msg = "Items for portfolio must be within SeriesFrame items."
- raise PortfolioItemsNotWithinFrameError(msg)
- if not set(items) <= set(self.columns_lvl_zero):
- msg = "Items for portfolio must be within SeriesFrame items."
- raise PortfolioItemsNotWithinFrameError(msg)
-
- if equal_weights:
- bal_weights = [1 / len(items)] * len(items)
-
- return items, cast("list[float]", bal_weights)
-
- def _initialize_rebalance_output(
- self: Self,
- items: list[str],
- name: str,
- cash_values: list[float],
- ) -> dict[str, dict[str, list[float]]]:
- """Initialize output structure for rebalanced portfolio.
-
- Args:
- items: List of items in portfolio.
- name: Name of the portfolio.
- cash_values: Cash index values.
-
- Returns:
- Initialized output dictionary.
- """
- output = {
- item: {
- ValueType.PRICE: [],
- "buysell_qty": [0.0] * self.length,
- "position": [0.0] * self.length,
- "value": [0.0] * self.length,
- "twr": [0.0] * self.length,
- "settle": [0.0] * self.length,
- }
- for item in items
- }
- output.update(
- {
- "cash": {
- ValueType.PRICE: cash_values,
- "buysell_qty": [0.0] * self.length,
- "position": [0.0] * self.length,
- "value": [0.0] * self.length,
- "twr": [0.0] * self.length,
- "settle": [0.0] * self.length,
- },
- name: {
- ValueType.PRICE: [1.0] + [0.0] * (self.length - 1),
- "buysell_qty": [-1.0] + [0.0] * (self.length - 1),
- "position": [-1.0] + [0.0] * (self.length - 1),
- "value": [-1.0] + [0.0] * (self.length - 1),
- "twr": [1.0] + [0.0] * (self.length - 1),
- "settle": [1.0] + [0.0] * (self.length - 1),
- },
- },
- )
- return output
-
- def _initialize_first_day_positions(
- self: Self,
- items: list[str],
- bal_weights: list[float],
- output: dict[str, dict[str, list[float]]],
- name: str,
- ) -> None:
- """Initialize positions for the first day.
-
- Args:
- items: List of items in portfolio.
- bal_weights: Weights for each item.
- output: Output dictionary to update.
- name: Name of the portfolio.
- """
- for item, weight in zip(items, bal_weights, strict=False):
- output[item][ValueType.PRICE] = cast(
- "list[float]",
- self.tsdf[(item, ValueType.PRICE)].to_numpy().tolist(),
- )
- output[item]["buysell_qty"][0] = (
- weight / self.tsdf[(item, ValueType.PRICE)].iloc[0]
- )
- output[item]["position"][0] = output[item]["buysell_qty"][0]
- output[item]["value"][0] = (
- output[item]["position"][0] * output[item][ValueType.PRICE][0]
- )
- output[item]["settle"][0] = (
- -output[item]["buysell_qty"][0] * output[item][ValueType.PRICE][0]
- )
- output["cash"]["buysell_qty"][0] += output[item]["settle"][0]
- output[item]["twr"][0] = (
- output[item]["value"][0] / -output[item]["settle"][0]
- )
-
- output["cash"]["position"][0] = (
- output["cash"]["buysell_qty"][0] + output[name]["settle"][0]
- )
- output["cash"]["settle"][0] = -output["cash"]["position"][0]
-
- def _process_rebalancing_day(
- self: Self,
- day: int,
- items: list[str],
- bal_weights: list[float],
- output: dict[str, dict[str, list[float]]],
- name: str,
- ) -> tuple[float, float]:
- """Process a rebalancing day.
-
- Args:
- day: Current day index.
- items: List of items in portfolio.
- bal_weights: Target weights for rebalancing.
- output: Output dictionary to update.
- name: Name of the portfolio.
-
- Returns:
- Tuple of (portfolio_value, settle_value).
- """
- portfolio_value = 0.0
- settle_value = 0.0
-
- for item, weight in zip(items, bal_weights, strict=False):
- output[item]["buysell_qty"][day] = (
- weight
- - output[item]["value"][day - 1] / -output[name]["value"][day - 1]
- ) / output[item][ValueType.PRICE][day]
- output[item]["position"][day] = (
- output[item]["position"][day - 1] + output[item]["buysell_qty"][day]
- )
- output[item]["value"][day] = (
- output[item]["position"][day] * output[item][ValueType.PRICE][day]
- )
- portfolio_value += output[item]["value"][day]
- output[item]["twr"][day] = (
- output[item]["value"][day]
- / (output[item]["value"][day - 1] - output[item]["settle"][day])
- * output[item]["twr"][day - 1]
- )
- output[item]["settle"][day] = (
- -output[item]["buysell_qty"][day] * output[item][ValueType.PRICE][day]
- )
- settle_value += output[item]["settle"][day]
-
- return portfolio_value, settle_value
-
- def _process_non_rebalancing_day(
- self: Self,
- day: int,
- items: list[str],
- output: dict[str, dict[str, list[float]]],
- ) -> float:
- """Process a non-rebalancing day.
-
- Args:
- day: Current day index.
- items: List of items in portfolio.
- output: Output dictionary to update.
-
- Returns:
- Portfolio value.
- """
- portfolio_value = 0.0
-
- for item in items:
- output[item]["position"][day] = output[item]["position"][day - 1]
- output[item]["value"][day] = (
- output[item]["position"][day] * output[item][ValueType.PRICE][day]
- )
- portfolio_value += output[item]["value"][day]
- output[item]["twr"][day] = (
- output[item]["value"][day]
- / (output[item]["value"][day - 1] - output[item]["settle"][day])
- * output[item]["twr"][day - 1]
- )
-
- return portfolio_value
-
- def _update_cash_and_portfolio(
- self: Self,
- day: int,
- portfolio_value: float,
- settle_value: float,
- output: dict[str, dict[str, list[float]]],
- name: str,
- ) -> None:
- """Update cash and portfolio values for a day.
-
- Args:
- day: Current day index.
- portfolio_value: Total portfolio value (before cash).
- settle_value: Total settle value.
- output: Output dictionary to update.
- name: Name of the portfolio.
- """
- output["cash"]["buysell_qty"][day] = settle_value
- output["cash"]["position"][day] = (
- output["cash"]["position"][day - 1]
- * output["cash"][ValueType.PRICE][day]
- / output["cash"][ValueType.PRICE][day - 1]
- + output["cash"]["buysell_qty"][day]
- )
- output["cash"]["value"][day] = output["cash"]["position"][day]
- total_portfolio_value = portfolio_value + output["cash"]["value"][day]
- output[name]["position"][day] = output[name]["position"][day - 1]
- output[name]["value"][day] = -total_portfolio_value
- output[name]["twr"][day] = (
- output[name]["value"][day] / output[name]["position"][day]
- )
- output[name][ValueType.PRICE][day] = output[name]["twr"][day]
-
- def _build_rebalance_result(
- self: Self,
- output: dict[str, dict[str, list[float]]],
- instruments: list[str],
- subheaders: list[str | ValueType],
- ) -> DataFrame:
- """Build result DataFrame from output dictionary.
-
- Args:
- output: Output dictionary with all calculated values.
- instruments: List of instrument names.
- subheaders: List of subheader names.
-
- Returns:
- DataFrame with MultiIndex columns.
- """
- result = DataFrame()
- for outvalue in output.values():
- result = concat(
- [
- result,
- DataFrame(data=outvalue, index=self.tsdf.index),
- ],
- axis="columns",
- )
- lvlone, lvltwo = [], []
- for instr in instruments:
- lvlone.extend([instr] * 6)
- lvltwo.extend(subheaders)
- result.columns = MultiIndex.from_arrays([lvlone, lvltwo])
- return result
-
-
-[docs]
- def rebalanced_portfolio(
- self: Self,
- name: str,
- items: list[str] | None = None,
- bal_weights: list[float] | None = None,
- frequency: int = 1,
- cash_index: OpenTimeSeries | None = None,
- *,
- equal_weights: bool = False,
- drop_extras: bool = True,
- ) -> OpenFrame:
- """Create a rebalanced portfolio from the OpenFrame constituents.
-
- Args:
- name: Name of the portfolio.
- items: List of items to include in the portfolio. If None, uses all items.
- Optional.
- bal_weights: List of weights for rebalancing. If None, uses frame weights.
- Optional.
- frequency: Rebalancing frequency. Defaults to 1.
- cash_index: Cash index series for cash component. Optional.
- equal_weights: If True, use equal weights for all items. Defaults to False.
- drop_extras: If True, only return TWR series; if False, return all details.
- Defaults to True.
-
- Returns:
- OpenFrame containing the rebalanced portfolio.
- """
- items, bal_weights = self._validate_and_prepare_rebalance_inputs(
- items,
- bal_weights,
- equal_weights=equal_weights,
- )
-
- if cash_index:
- cash_index.tsdf = cash_index.tsdf.reindex(self.tsdf.index)
- cash_values: list[float] = cast(
- "list[float]", cash_index.tsdf.iloc[:, 0].to_numpy().tolist()
- )
- else:
- cash_values = [1.0] * self.length
-
- if self.tsdf.isna().to_numpy().any():
- self.value_nan_handle()
-
- ccies = list({serie.currency for serie in self.constituents})
- if len(ccies) != 1:
- msg = "Items for portfolio must be denominated in same currency."
- raise MultipleCurrenciesError(msg)
- currency = ccies[0]
-
- instruments = [*items, "cash", name]
- subheaders = [
- ValueType.PRICE,
- "buysell_qty",
- "position",
- "value",
- "twr",
- "settle",
- ]
-
- output = self._initialize_rebalance_output(
- items=items,
- name=name,
- cash_values=cash_values,
- )
-
- self._initialize_first_day_positions(
- items=items,
- bal_weights=bal_weights,
- output=output,
- name=name,
- )
-
- counter = 1
- for day in range(1, self.length):
- if day == frequency * counter:
- portfolio_value, settle_value = self._process_rebalancing_day(
- day=day,
- items=items,
- bal_weights=bal_weights,
- output=output,
- name=name,
- )
- counter += 1
- else:
- portfolio_value = self._process_non_rebalancing_day(
- day=day,
- items=items,
- output=output,
- )
- settle_value = 0.0
-
- self._update_cash_and_portfolio(
- day=day,
- portfolio_value=portfolio_value,
- settle_value=settle_value,
- output=output,
- name=name,
- )
-
- result = self._build_rebalance_result(
- output=output,
- instruments=instruments,
- subheaders=subheaders,
- )
-
- series = []
- if drop_extras:
- used_constituents = [
- item for item in self.constituents if item.label in items
- ]
- series.extend(
- [
- OpenTimeSeries.from_df(
- dframe=result[(item.label, "twr")],
- valuetype=ValueType.PRICE,
- baseccy=item.currency,
- local_ccy=item.local_ccy,
- )
- for item in used_constituents
- ]
- )
- series.append(
- OpenTimeSeries.from_df(
- dframe=result[(name, "twr")],
- valuetype=ValueType.PRICE,
- baseccy=currency,
- local_ccy=True,
- ),
- )
- else:
- series.extend(
- [
- OpenTimeSeries.from_df(
- dframe=result.loc[:, col],
- valuetype=ValueType.PRICE,
- baseccy=currency,
- local_ccy=True,
- ).set_new_label(f"{col[0]}, {col[1]!s}")
- for col in result.columns
- ]
- )
-
- return OpenFrame(series)
-
-
-
-"""Function to load plotly layout and configuration from local json file."""
-
-from __future__ import annotations
-
-from json import load
-from logging import getLogger
-from pathlib import Path
-from typing import TYPE_CHECKING
-
-import requests
-from requests.exceptions import ConnectionError as RequestsConnectionError
-
-if TYPE_CHECKING:
- from .owntypes import CaptorLogoType, PlotlyLayoutType # pragma: no cover
-
-logger = getLogger(__name__)
-
-__all__ = ["load_plotly_dict"]
-
-
-def _check_remote_file_existence(url: str) -> bool:
- """Check if remote file exists.
-
- Args:
- url: Path to remote file.
-
- Returns:
- True if url is valid and False otherwise.
- """
- ok_code = 200
-
- try:
- response = requests.head(url, timeout=30)
- if response.status_code != ok_code:
- return False
- except RequestsConnectionError:
- return False
- return True
-
-
-
-[docs]
-def load_plotly_dict(
- *,
- responsive: bool = True,
-) -> tuple[PlotlyLayoutType, CaptorLogoType]:
- """Load Plotly defaults.
-
- Args:
- responsive: Flag whether to load as responsive. Defaults to True.
-
- Returns:
- tuple[PlotlyLayoutType, CaptorLogoType]: A tuple (config_and_layout, logo)
- where config_and_layout is the Plotly config and layout template dict,
- and logo is the Captor logo dict (may be empty if the remote logo is
- unavailable).
- """
- package_dir = Path(__file__).parent
- layoutfile = package_dir / "plotly_layouts.json"
- logofile = package_dir / "plotly_captor_logo.json"
-
- with layoutfile.open(mode="r", encoding="utf-8") as layout_file:
- fig = load(layout_file)
- with logofile.open(mode="r", encoding="utf-8") as logo_file:
- logo = load(logo_file)
-
- if not _check_remote_file_existence(url=logo["source"]):
- msg = f"Failed to add logo image from URL {logo['source']}"
- logger.warning(msg)
- logo = {}
-
- fig["config"].update({"responsive": responsive})
-
- return fig, logo
-
-
-"""Declaring types used throughout the project."""
-
-from __future__ import annotations
-
-import datetime as dt
-from enum import StrEnum
-from pprint import pformat
-from typing import (
- TYPE_CHECKING,
- Annotated,
- ClassVar,
- Literal,
- Self,
- TypeAlias,
- TypeVar,
-)
-
-from annotated_types import MinLen
-from numpy import datetime64
-from pandas import Series, Timestamp
-from pydantic import BaseModel, Field, StringConstraints
-
-if TYPE_CHECKING:
- from pandas import Series as _Series
-
- SeriesFloat = _Series[float]
-else:
- SeriesFloat = Series
-
-__all__ = ["ValueType"]
-
-
-SeriesOrFloat_co = TypeVar("SeriesOrFloat_co", float, SeriesFloat, covariant=True)
-
-
-CountryStringType = Annotated[
- str,
- StringConstraints(
- strip_whitespace=True,
- pattern=r"^[A-Z]{2}$",
- to_upper=True,
- min_length=2,
- max_length=2,
- strict=True,
- ),
-]
-CountrySetType: TypeAlias = Annotated[set[CountryStringType], MinLen(1)]
-CountriesType: TypeAlias = CountrySetType | CountryStringType
-
-
-
-
-
-
-CurrencyStringType = Annotated[
- str,
- StringConstraints(
- pattern=r"^[A-Z]{3}$",
- to_upper=True,
- min_length=3,
- max_length=3,
- strict=True,
- strip_whitespace=True,
- ),
-]
-
-
-
-
-
-
-DateStringType = Annotated[
- str,
- StringConstraints(
- pattern=r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$",
- strip_whitespace=True,
- strict=True,
- min_length=10,
- max_length=10,
- ),
-]
-DateListType: TypeAlias = Annotated[list[DateStringType], MinLen(1)]
-
-ValueListType: TypeAlias = Annotated[list[float], MinLen(1)]
-
-DaysInYearType = Annotated[int, Field(strict=True, ge=1, le=366)]
-
-DateType = str | dt.date | dt.datetime | datetime64 | Timestamp
-
-PlotlyConfigType = (
- str
- | int
- | float
- | bool
- | list[str]
- | dict[str, str | int | float | bool | list[str]]
-)
-
-PlotlyLayoutType = dict[str, PlotlyConfigType]
-
-CaptorLogoType = dict[str, str | float]
-
-LiteralJsonOutput = Literal["values", "tsdf"]
-LiteralTrunc = Literal["before", "after", "both"]
-LiteralLinePlotMode = (
- Literal[
- "lines",
- "markers",
- "lines+markers",
- "lines+text",
- "markers+text",
- "lines+markers+text",
- ]
- | None
-)
-LiteralHowMerge = Literal["outer", "inner"]
-LiteralQuantileInterp = Literal["linear", "lower", "higher", "midpoint", "nearest"]
-LiteralBizDayFreq = Literal["B", "BME", "BQE", "BYE"]
-LiteralPandasReindexMethod = (
- Literal["pad", "ffill", "backfill", "bfill", "nearest"] | None
-)
-LiteralNanMethod = Literal["fill", "drop"]
-LiteralCaptureRatio = Literal["up", "down", "both"]
-LiteralBarPlotMode = Literal["stack", "group", "overlay", "relative"]
-LiteralPlotlyOutput = Literal["file", "div"]
-LiteralPlotlyJSlib = Literal[True, False, "cdn"]
-LiteralPlotlyHistogramPlotType = Literal["bars", "lines"]
-LiteralPlotlyHistogramBarMode = Literal["stack", "group", "overlay", "relative"]
-LiteralPlotlyHistogramCurveType = Literal["normal", "kde"]
-LiteralPlotlyHistogramHistNorm = Literal[
- "percent",
- "probability",
- "density",
- "probability density",
-]
-LiteralPortfolioWeightings = Literal[
- "eq_weights", "inv_vol", "max_div", "min_vol_overweight"
-]
-LiteralMinimizeMethods = Literal[
- "SLSQP",
- "Nelder-Mead",
- "Powell",
- "CG",
- "BFGS",
- "Newton-CG",
- "L-BFGS-B",
- "TNC",
- "COBYLA",
- "trust-constr",
- "dogleg",
- "trust-ncg",
- "trust-exact",
- "trust-krylov",
-]
-
-LiteralSeriesProps = Literal[
- "value_ret",
- "geo_ret",
- "arithmetic_ret",
- "vol",
- "downside_deviation",
- "ret_vol_ratio",
- "sortino_ratio",
- "kappa3_ratio",
- "z_score",
- "skew",
- "kurtosis",
- "positive_share",
- "var_down",
- "cvar_down",
- "vol_from_var",
- "worst",
- "worst_month",
- "max_drawdown_cal_year",
- "max_drawdown",
- "max_drawdown_date",
- "first_idx",
- "last_idx",
- "length",
- "span_of_days",
- "yearfrac",
- "periods_in_a_year",
- "autocorr",
- "partial_autocorr",
-]
-LiteralFrameProps = Literal[
- "value_ret",
- "geo_ret",
- "arithmetic_ret",
- "autocorr",
- "vol",
- "downside_deviation",
- "ret_vol_ratio",
- "sortino_ratio",
- "kappa3_ratio",
- "z_score",
- "skew",
- "kurtosis",
- "positive_share",
- "var_down",
- "cvar_down",
- "vol_from_var",
- "worst",
- "worst_month",
- "max_drawdown",
- "max_drawdown_date",
- "max_drawdown_cal_year",
- "first_indices",
- "last_indices",
- "lengths_of_items",
- "span_of_days_all",
-]
-
-
-
-[docs]
-class PropertiesList(list[str]):
- """Base class for allowed property arguments definition."""
-
- allowed_strings: ClassVar[set[str]] = {
- "value_ret",
- "geo_ret",
- "arithmetic_ret",
- "vol",
- "downside_deviation",
- "ret_vol_ratio",
- "sortino_ratio",
- "kappa3_ratio",
- "omega_ratio",
- "z_score",
- "skew",
- "kurtosis",
- "positive_share",
- "var_down",
- "cvar_down",
- "vol_from_var",
- "worst",
- "worst_month",
- "max_drawdown",
- "max_drawdown_date",
- "max_drawdown_cal_year",
- }
-
- def _validate(self: Self) -> None:
- """Validate the string input of the all_properties method."""
- seen = set()
- invalids = set()
- duplicates = set()
- msg = ""
- for item in self:
- if item not in self.allowed_strings:
- invalids.add(item)
- if item in seen:
- duplicates.add(item)
- seen.add(item)
- if len(invalids) != 0:
- msg += (
- f"Invalid string(s): {list(invalids)}.\nAllowed strings are:"
- f"\n{pformat(self.allowed_strings)}\n"
- )
- if len(duplicates) != 0:
- msg += f"Duplicate string(s): {list(duplicates)}."
- if len(msg) != 0:
- raise PropertiesInputValidationError(msg)
-
-
-
-
-[docs]
-class OpenTimeSeriesPropertiesList(PropertiesList):
- """Allowed property arguments for the OpenTimeSeries class."""
-
- allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
- "first_idx",
- "last_idx",
- "length",
- "span_of_days",
- "yearfrac",
- "periods_in_a_year",
- "autocorr",
- "partial_autocorr",
- }
-
-
-[docs]
- def __init__(
- self: Self,
- *args: LiteralSeriesProps,
- ) -> None:
- """Property arguments for the OpenTimeSeries class."""
- super().__init__(args)
- self._validate()
-
-
-
-
-
-[docs]
-class OpenFramePropertiesList(PropertiesList):
- """Allowed property arguments for the OpenFrame class."""
-
- allowed_strings: ClassVar[set[str]] = PropertiesList.allowed_strings | {
- "autocorr",
- "first_indices",
- "last_indices",
- "lengths_of_items",
- "span_of_days_all",
- }
-
-
-[docs]
- def __init__(self: Self, *args: LiteralFrameProps) -> None:
- """Property arguments for the OpenFrame class."""
- super().__init__(args)
- self._validate()
-
-
-
-
-
-[docs]
-class ValueType(StrEnum):
- """Enum types of OpenTimeSeries to identify the output."""
-
- EWMA_VOL = "EWMA volatility"
- EWMA_VAR = "EWMA VaR"
- PRICE = "Price(Close)"
- RTRN = "Return(Total)"
- RELRTRN = "Relative return"
- ROLLBETA = "Beta"
- ROLLCORR = "Rolling correlation"
- ROLLCVAR = "Rolling CVaR"
- ROLLINFORATIO = "Information Ratio"
- ROLLRTRN = "Rolling returns"
- ROLLVAR = "Rolling VaR"
- ROLLVOL = "Rolling volatility"
-
-
-
-
-[docs]
-class MixedValuetypesError(Exception):
- """Raised when provided timeseries valuetypes are not the same."""
-
-
-
-
-[docs]
-class AtLeastOneFrameError(Exception):
- """Raised when none of the possible frame inputs is provided."""
-
-
-
-
-[docs]
-class DateAlignmentError(Exception):
- """Raised when date input is not aligned with existing range."""
-
-
-
-
-[docs]
-class NumberOfItemsAndLabelsNotSameError(Exception):
- """Raised when number of labels is not matching the number of timeseries."""
-
-
-
-
-[docs]
-class InitialValueZeroError(Exception):
- """Raised when a calculation cannot be performed due to initial value(s) zero."""
-
-
-
-
-[docs]
-class CountriesNotStringNorListStrError(Exception):
- """Raised when countries argument is not provided in correct format."""
-
-
-
-
-[docs]
-class MarketsNotStringNorListStrError(Exception):
- """Raised when markets argument is not provided in correct format."""
-
-
-
-
-[docs]
-class TradingDaysNotAboveZeroError(Exception):
- """Raised when trading days argument is not above zero."""
-
-
-
-
-[docs]
-class BothStartAndEndError(Exception):
- """Raised when both start and end dates are provided."""
-
-
-
-
-[docs]
-class NoWeightsError(Exception):
- """Raised when no weights are provided to function where necessary."""
-
-
-
-
-[docs]
-class LabelsNotUniqueError(Exception):
- """Raised when provided label names are not unique."""
-
-
-
-
-[docs]
-class RatioInputError(Exception):
- """Raised when ratio keyword not provided correctly."""
-
-
-
-
-[docs]
-class MergingResultedInEmptyError(Exception):
- """Raised when a merge resulted in an empty DataFrame."""
-
-
-
-
-[docs]
-class IncorrectArgumentComboError(Exception):
- """Raised when correct combination of arguments is not provided."""
-
-
-
-
-[docs]
-class PropertiesInputValidationError(Exception):
- """Raised when duplicate strings are provided."""
-
-
-
-
-[docs]
-class ResampleDataLossError(Exception):
- """Raised when user attempts to run resample_to_business_period_ends on returns."""
-
-
-
-class WeightsNotProvidedError(Exception):
- """Raised when weights are not provided."""
-
-
-class MultipleCurrenciesError(Exception):
- """Raised when multiple currencies are provided."""
-
-
-class PortfolioItemsNotWithinFrameError(Exception):
- """Raised when portfolio items are not within frame."""
-
-
-class MaxDiversificationNaNError(Exception):
- """Raised when max_div weight strategy produces NaN values."""
-
-
-class MaxDiversificationNegativeWeightsError(Exception):
- """Raised when max_div weight strategy produces negative weights."""
-
-"""Defining the portfolio tools for the OpenFrame class."""
-
-from __future__ import annotations
-
-from inspect import stack
-from pathlib import Path
-from typing import TYPE_CHECKING, Any, cast
-
-from numpy import (
- append,
- array,
- einsum,
- float64,
- inf,
- isnan,
- linspace,
- nan,
- sqrt,
-)
-from numpy import (
- sum as npsum,
-)
-from pandas import (
- DataFrame,
- Series,
- concat,
-)
-from plotly.graph_objs import Figure # type: ignore[import-untyped]
-from plotly.io import to_html # type: ignore[import-untyped]
-from plotly.offline import plot # type: ignore[import-untyped]
-from scipy.optimize import minimize
-
-from .load_plotly import load_plotly_dict
-from .owntypes import (
- AtLeastOneFrameError,
- LiteralLinePlotMode,
- LiteralMinimizeMethods,
- LiteralPlotlyJSlib,
- LiteralPlotlyOutput,
- MixedValuetypesError,
- ValueType,
-)
-from .series import OpenTimeSeries
-from .simulation import _random_generator
-
-if TYPE_CHECKING: # pragma: no cover
- from collections.abc import Callable
-
- from numpy.typing import NDArray
- from pydantic import DirectoryPath
-
- from .frame import OpenFrame
-
-__all__ = [
- "constrain_optimized_portfolios",
- "efficient_frontier",
- "prepare_plot_data",
- "sharpeplot",
- "simulate_portfolios",
-]
-
-
-
-[docs]
-def simulate_portfolios(
- simframe: OpenFrame,
- num_ports: int,
- seed: int,
-) -> DataFrame:
- """Generate random weights for simulated portfolios.
-
- Args:
- simframe: Return data for portfolio constituents.
- num_ports: Number of possible portfolios to simulate.
- seed: The seed for the random process.
-
- Returns:
- The resulting data.
- """
- copi = simframe.from_deepcopy()
-
- vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
- if not any(vtypes):
- copi.value_to_ret()
- log_ret = copi.tsdf.copy()[1:]
- elif all(vtypes):
- log_ret = copi.tsdf.copy()
- else:
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- log_ret.columns = log_ret.columns.get_level_values(0)
-
- cov_matrix = log_ret.cov() * simframe.periods_in_a_year
- mean_returns = log_ret.mean() * simframe.periods_in_a_year
-
- randomizer = _random_generator(seed=seed)
- all_weights = randomizer.random((num_ports, simframe.item_count))
- all_weights = all_weights / all_weights.sum(axis=1, keepdims=True)
-
- ret_arr = all_weights @ mean_returns
- vol_arr = sqrt(einsum("ij,jk,ik->i", all_weights, cov_matrix, all_weights))
- sharpe_arr = ret_arr / vol_arr
-
- simdf = concat(
- [
- DataFrame({"stdev": vol_arr, "ret": ret_arr, "sharpe": sharpe_arr}),
- DataFrame(all_weights, columns=simframe.columns_lvl_zero),
- ],
- axis="columns",
- )
- simdf = simdf.replace([inf, -inf], nan)
- return simdf.dropna()
-
-
-
-def _prepare_returns_for_frontier(eframe: OpenFrame) -> tuple[DataFrame, OpenFrame]:
- """Prepare returns DataFrame for frontier calculation.
-
- Args:
- eframe: Portfolio data.
-
- Returns:
- Tuple of (log_ret DataFrame, copied frame).
-
- Raises:
- MixedValuetypesError: If series types are mixed.
- """
- if eframe.weights is None:
- eframe.weights = [1.0 / eframe.item_count] * eframe.item_count
-
- copi = eframe.from_deepcopy()
-
- vtypes = [x == ValueType.RTRN for x in copi.tsdf.columns.get_level_values(1)]
- if not any(vtypes):
- copi.value_to_ret()
- log_ret = copi.tsdf.copy()[1:]
- elif all(vtypes):
- log_ret = copi.tsdf.copy()
- else:
- msg = "Mix of series types will give inconsistent results"
- raise MixedValuetypesError(msg)
-
- log_ret.columns = log_ret.columns.get_level_values(0)
- return log_ret, copi
-
-
-def _calculate_frontier_bounds(
- simulated: DataFrame,
- log_ret: DataFrame,
- periods_in_a_year: float,
-) -> tuple[float, float]:
- """Calculate frontier return bounds.
-
- Args:
- simulated: Simulated portfolios DataFrame.
- log_ret: Returns DataFrame.
- periods_in_a_year: Periods in a year.
-
- Returns:
- Tuple of (min_return, max_return).
- """
- min_stdev_idx = simulated["stdev"].idxmin()
- frontier_min = cast("float", simulated.loc[min_stdev_idx, "ret"])
-
- arithmetic_means = array(log_ret.mean() * periods_in_a_year)
- cleaned_arithmetic_means = arithmetic_means[~isnan(arithmetic_means)]
-
- frontier_max = float(cleaned_arithmetic_means.max())
- return frontier_min, frontier_max
-
-
-def _build_frontier_line(
- log_ret: DataFrame,
- frontier_min: float,
- frontier_max: float,
- frontier_points: int,
- periods_in_a_year: float,
- init_guess: NDArray[float64],
- bounds: tuple[tuple[float, float], ...],
- minimize_method: LiteralMinimizeMethods,
-) -> tuple[list[float], list[NDArray[float64]]]:
- """Build frontier line points.
-
- Args:
- log_ret: Returns DataFrame.
- frontier_min: Minimum return.
- frontier_max: Maximum return.
- frontier_points: Number of points.
- periods_in_a_year: Periods in a year.
- init_guess: Initial guess for optimization.
- bounds: Optimization bounds.
- minimize_method: Minimization method.
-
- Returns:
- Tuple of (frontier_x, frontier_weights).
- """
-
- def _check_sum(weights: NDArray[float64]) -> float:
- return cast("float", npsum(weights) - 1)
-
- def _get_ret_vol_sr(
- lg_ret: DataFrame,
- weights: NDArray[float64],
- per_in_yr: float,
- ) -> NDArray[float64]:
- ret = npsum(lg_ret.mean() * weights) * per_in_yr
- volatility = sqrt(weights.T @ (lg_ret.cov() * per_in_yr @ weights))
- sr = ret / volatility
- return cast("NDArray[float64]", array([ret, volatility, sr]))
-
- def _diff_return(
- lg_ret: DataFrame,
- weights: NDArray[float64],
- per_in_yr: float,
- poss_return: float,
- ) -> float64:
- return cast(
- "float64",
- _get_ret_vol_sr(lg_ret=lg_ret, weights=weights, per_in_yr=per_in_yr)[0]
- - poss_return,
- )
-
- def _minimize_volatility(
- weights: NDArray[float64],
- ) -> float64:
- return cast(
- "float64",
- _get_ret_vol_sr(
- lg_ret=log_ret,
- weights=weights,
- per_in_yr=periods_in_a_year,
- )[1],
- )
-
- frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
- frontier_x = []
- frontier_weights = []
-
- for possible_return in frontier_y:
- cons = cast(
- "Any",
- [
- {"type": "eq", "fun": _check_sum},
- {
- "type": "eq",
- "fun": lambda w, poss_return=possible_return: _diff_return(
- lg_ret=log_ret,
- weights=w,
- per_in_yr=periods_in_a_year,
- poss_return=poss_return,
- ),
- },
- ],
- )
-
- result = minimize(
- fun=_minimize_volatility,
- x0=init_guess,
- method=minimize_method,
- bounds=bounds,
- constraints=cons,
- )
-
- frontier_x.append(result["fun"])
- frontier_weights.append(result["x"])
-
- return frontier_x, frontier_weights
-
-
-def _build_frontier_dataframe(
- frontier_x: list[float],
- frontier_y: NDArray[float64],
- frontier_weights: list[NDArray[float64]],
- columns_lvl_zero: list[str],
-) -> DataFrame:
- """Build frontier DataFrame.
-
- Args:
- frontier_x: Frontier volatility values.
- frontier_y: Frontier return values.
- frontier_weights: Frontier weight arrays.
- columns_lvl_zero: Column names.
-
- Returns:
- Frontier DataFrame.
- """
- line_df = concat(
- [
- DataFrame(data=frontier_weights, columns=columns_lvl_zero),
- DataFrame({"stdev": frontier_x, "ret": frontier_y}),
- ],
- axis="columns",
- )
- line_df["sharpe"] = line_df.ret / line_df.stdev
-
- limit_small = 0.0001
- line_df = line_df.mask(line_df.abs() < limit_small, 0.0)
-
- weight_cols = columns_lvl_zero
- weight_header = "<br><br>Weights:<br>"
- line_df["text"] = line_df[weight_cols].apply(
- lambda row: (
- weight_header
- + "<br>".join([f"{row[col]:.1%} {col}" for col in weight_cols])
- ),
- axis=1,
- )
-
- return line_df
-
-
-def _apply_tweak(line_df: DataFrame) -> DataFrame:
- """Apply tweak to frontier DataFrame.
-
- Args:
- line_df: Frontier DataFrame.
-
- Returns:
- Tweaked DataFrame.
- """
- limit_tweak = 0.001
- line_df["stdev_diff"] = line_df.stdev.ffill().pct_change()
- line_df = line_df.loc[line_df.stdev_diff.abs() > limit_tweak]
- return line_df.drop(columns="stdev_diff")
-
-
-def _create_optimization_functions(
- log_ret: DataFrame,
- periods_in_a_year: float,
-) -> tuple[
- Callable[[NDArray[float64]], float],
- Callable[[NDArray[float64]], NDArray[float64]],
- Callable[[NDArray[float64]], float64],
-]:
- """Create optimization helper functions.
-
- Args:
- log_ret: Returns DataFrame.
- periods_in_a_year: Periods in a year.
-
- Returns:
- Tuple of (_check_sum, _get_ret_vol_sr, _neg_sharpe) functions.
- """
-
- def _check_sum(weights: NDArray[float64]) -> float:
- return cast("float", npsum(weights) - 1)
-
- def _get_ret_vol_sr(weights: NDArray[float64]) -> NDArray[float64]:
- ret = npsum(log_ret.mean() * weights) * periods_in_a_year
- volatility = sqrt(weights.T @ (log_ret.cov() * periods_in_a_year @ weights))
- sr = ret / volatility
- return cast("NDArray[float64]", array([ret, volatility, sr]))
-
- def _neg_sharpe(weights: NDArray[float64]) -> float64:
- return cast("float64", _get_ret_vol_sr(weights)[2] * -1)
-
- return _check_sum, _get_ret_vol_sr, _neg_sharpe
-
-
-def _optimize_max_sharpe_portfolio(
- init_guess: NDArray[float64],
- bounds: tuple[tuple[float, float], ...],
- minimize_method: LiteralMinimizeMethods,
- _check_sum: Callable[[NDArray[float64]], float],
- _get_ret_vol_sr: Callable[[NDArray[float64]], NDArray[float64]],
- _neg_sharpe: Callable[[NDArray[float64]], float64],
-) -> tuple[NDArray[float64], NDArray[float64]]:
- """Optimize maximum Sharpe ratio portfolio.
-
- Args:
- init_guess: Initial guess for optimization.
- bounds: Optimization bounds.
- minimize_method: Minimization method.
- _check_sum: Check sum constraint function.
- _get_ret_vol_sr: Get return, volatility, Sharpe ratio function.
- _neg_sharpe: Negative Sharpe ratio function.
-
- Returns:
- Tuple of (optimal metrics, optimal weights).
- """
- constraints = cast("Any", [{"type": "eq", "fun": _check_sum}])
- opt_results = minimize(
- fun=_neg_sharpe,
- x0=init_guess,
- method=minimize_method,
- bounds=bounds,
- constraints=constraints,
- )
-
- optimal = _get_ret_vol_sr(opt_results.x)
-
- return optimal, opt_results.x
-
-
-
-[docs]
-def efficient_frontier(
- eframe: OpenFrame,
- num_ports: int = 5000,
- seed: int = 71,
- bounds: tuple[tuple[float, float], ...] | None = None,
- frontier_points: int = 200,
- minimize_method: LiteralMinimizeMethods = "SLSQP",
- *,
- tweak: bool = True,
-) -> tuple[DataFrame, DataFrame, NDArray[float64]]:
- """Identify an efficient frontier.
-
- Args:
- eframe: Portfolio data.
- num_ports: Number of possible portfolios to simulate. Defaults to 5000.
- seed: The seed for the random process. Defaults to 71.
- bounds: The range of minimum and maximum allowed allocations for each asset.
- frontier_points: Number of points along frontier to optimize. Defaults to 200.
- minimize_method: The method passed into the scipy.minimize function.
- Defaults to SLSQP.
- tweak: Cutting the frontier to exclude multiple points with almost the
- same risk.
- Defaults to True.
-
- Returns:
- The efficient frontier data, simulation data and optimal portfolio.
- """
- log_ret, copi = _prepare_returns_for_frontier(eframe)
-
- simulated = simulate_portfolios(simframe=copi, num_ports=num_ports, seed=seed)
-
- frontier_min, frontier_max = _calculate_frontier_bounds(
- simulated=simulated,
- log_ret=log_ret,
- periods_in_a_year=copi.periods_in_a_year,
- )
-
- if not bounds:
- bounds = tuple((0.0, 1.0) for _ in range(eframe.item_count))
- init_guess = array(eframe.weights)
-
- _check_sum, _get_ret_vol_sr, _neg_sharpe = _create_optimization_functions(
- log_ret=log_ret,
- periods_in_a_year=copi.periods_in_a_year,
- )
-
- optimal, opt_weights = _optimize_max_sharpe_portfolio(
- init_guess=init_guess,
- bounds=bounds,
- minimize_method=minimize_method,
- _check_sum=_check_sum,
- _get_ret_vol_sr=_get_ret_vol_sr,
- _neg_sharpe=_neg_sharpe,
- )
-
- frontier_y = linspace(start=frontier_min, stop=frontier_max, num=frontier_points)
- frontier_x, frontier_weights = _build_frontier_line(
- log_ret=log_ret,
- frontier_min=frontier_min,
- frontier_max=frontier_max,
- frontier_points=frontier_points,
- periods_in_a_year=copi.periods_in_a_year,
- init_guess=init_guess,
- bounds=bounds,
- minimize_method=minimize_method,
- )
-
- line_df = _build_frontier_dataframe(
- frontier_x=frontier_x,
- frontier_y=frontier_y,
- frontier_weights=frontier_weights,
- columns_lvl_zero=eframe.columns_lvl_zero,
- )
-
- if tweak:
- line_df = _apply_tweak(line_df)
-
- return line_df, simulated, append(optimal, opt_weights)
-
-
-
-
-[docs]
-def constrain_optimized_portfolios(
- data: OpenFrame,
- serie: OpenTimeSeries,
- portfolioname: str = "Current Portfolio",
- simulations: int = 10000,
- curve_points: int = 200,
- bounds: tuple[tuple[float, float], ...] | None = None,
- minimize_method: LiteralMinimizeMethods = "SLSQP",
-) -> tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]:
- """Constrain optimized portfolios to those that improve on the current one.
-
- Args:
- data: Portfolio data.
- serie: A timeseries representing the current portfolio.
- portfolioname: Name of the portfolio. Defaults to "Current Portfolio".
- simulations: Number of possible portfolios to simulate. Defaults to 10000.
- curve_points: Number of optimal portfolios on the efficient frontier.
- Defaults to 200.
- bounds: The range of minimum and maximum allowed allocations for each asset.
- minimize_method: The method passed into the scipy.minimize function.
- Defaults to SLSQP.
-
- Returns:
- The constrained optimal portfolio data.
-
- """
- lr_frame = data.from_deepcopy()
- mv_frame = data.from_deepcopy()
-
- if not bounds:
- bounds = tuple((0.0, 1.0) for _ in range(data.item_count))
-
- front_frame, _, _ = efficient_frontier(
- eframe=data,
- num_ports=simulations,
- frontier_points=curve_points,
- bounds=bounds,
- minimize_method=minimize_method,
- )
-
- condition_least_ret = front_frame.ret > serie.arithmetic_ret
- least_ret_frame = front_frame[condition_least_ret].sort_values(by="stdev")
- least_ret_port: Series[float] = least_ret_frame.iloc[0]
- least_ret_port_name = f"Minimize vol & target return of {portfolioname}"
- least_ret_weights: list[float] = [
- least_ret_port.loc[c] for c in lr_frame.columns_lvl_zero
- ]
- lr_frame.weights = least_ret_weights
- resleast = OpenTimeSeries.from_df(lr_frame.make_portfolio(least_ret_port_name))
-
- condition_most_vol = front_frame.stdev < serie.vol
- most_vol_frame = front_frame[condition_most_vol].sort_values(
- by="ret",
- ascending=False,
- )
- most_vol_port: Series[float] = most_vol_frame.iloc[0]
- most_vol_port_name = f"Maximize return & target risk of {portfolioname}"
- most_vol_weights: list[float] = [
- most_vol_port.loc[c] for c in mv_frame.columns_lvl_zero
- ]
- mv_frame.weights = most_vol_weights
- resmost = OpenTimeSeries.from_df(mv_frame.make_portfolio(most_vol_port_name))
-
- return lr_frame, resleast, mv_frame, resmost
-
-
-
-
-[docs]
-def prepare_plot_data(
- assets: OpenFrame,
- current: OpenTimeSeries,
- optimized: NDArray[float64],
-) -> DataFrame:
- """Prepare data to be used as point_frame in the sharpeplot function.
-
- Args:
- assets: Portfolio data with individual assets and a weighted portfolio.
- current: The current or initial portfolio based on given weights.
- optimized: Data optimized with the efficient_frontier method.
-
- Returns:
- The data prepared with mean returns, volatility and weights.
- """
- txt = "<br><br>Weights:<br>" + "<br>".join(
- [
- f"{wgt:.1%} {nm}"
- for wgt, nm in zip(
- cast("list[float]", assets.weights),
- assets.columns_lvl_zero,
- strict=True,
- )
- ],
- )
-
- opt_text_list = [
- f"{wgt:.1%} {nm}"
- for wgt, nm in zip(optimized[3:], assets.columns_lvl_zero, strict=True)
- ]
- opt_text = "<br><br>Weights:<br>" + "<br>".join(opt_text_list)
- plotframe = DataFrame(
- data=[
- assets.arithmetic_ret,
- assets.vol,
- Series(
- data=[""] * assets.item_count,
- index=assets.vol.index,
- ),
- ],
- index=["ret", "stdev", "text"],
- )
- plotframe.columns = plotframe.columns.get_level_values(0)
- plotframe["Max Sharpe Portfolio"] = Series(
- data=[optimized[0], optimized[1], opt_text],
- index=plotframe.index,
- dtype=object,
- )
- if current.label is not None:
- label = current.label
- plotframe[label] = Series(
- data=[current.arithmetic_ret, current.vol, txt],
- index=plotframe.index,
- dtype=object,
- )
-
- return plotframe
-
-
-
-def _determine_output_directory(directory: DirectoryPath | None) -> Path:
- """Determine output directory for plot file.
-
- Args:
- directory: Optional directory path.
-
- Returns:
- Path to output directory.
- """
- if directory:
- return Path(directory).resolve()
- if Path.home().joinpath("Documents").exists():
- return Path.home().joinpath("Documents")
- return Path(stack()[2].filename).parent
-
-
-def _add_simulated_portfolios_trace(
- figure: Figure,
- sim_frame: DataFrame,
- returns: list[float],
- risk: list[float],
-) -> None:
- """Add simulated portfolios trace to figure.
-
- Args:
- figure: Plotly figure.
- sim_frame: Simulated portfolios DataFrame.
- returns: List to extend with returns.
- risk: List to extend with risk values.
- """
- returns.extend(list(sim_frame.loc[:, "ret"]))
- risk.extend(list(sim_frame.loc[:, "stdev"]))
- figure.add_scatter(
- x=sim_frame.loc[:, "stdev"],
- y=sim_frame.loc[:, "ret"],
- hoverinfo="skip",
- marker={
- "size": 10,
- "opacity": 0.5,
- "color": sim_frame.loc[:, "sharpe"],
- "colorscale": "Jet",
- "reversescale": True,
- "colorbar": {"thickness": 20, "title": "Ratio<br>ret / vol"},
- },
- mode="markers",
- name="simulated portfolios",
- )
-
-
-def _add_efficient_frontier_trace(
- figure: Figure,
- line_frame: DataFrame,
- returns: list[float],
- risk: list[float],
-) -> None:
- """Add efficient frontier trace to figure.
-
- Args:
- figure: Plotly figure.
- line_frame: Efficient frontier DataFrame.
- returns: List to extend with returns.
- risk: List to extend with risk values.
- """
- returns.extend(list(line_frame.loc[:, "ret"]))
- risk.extend(list(line_frame.loc[:, "stdev"]))
- figure.add_scatter(
- x=line_frame.loc[:, "stdev"],
- y=line_frame.loc[:, "ret"],
- text=line_frame.loc[:, "text"],
- xhoverformat=".2%",
- yhoverformat=".2%",
- hovertemplate="Return %{y}<br>Vol %{x}%{text}",
- hoverlabel_align="right",
- line={"width": 2.5, "dash": "solid"},
- mode="lines",
- name="Efficient frontier",
- )
-
-
-def _add_point_frame_traces(
- figure: Figure,
- point_frame: DataFrame,
- point_frame_mode: LiteralLinePlotMode,
- fig: dict[str, Any],
- returns: list[float],
- risk: list[float],
-) -> None:
- """Add point frame traces to figure.
-
- Args:
- figure: Plotly figure.
- point_frame: Point frame DataFrame.
- point_frame_mode: Mode for point frame traces.
- fig: Plotly figure dictionary.
- returns: List to extend with returns.
- risk: List to extend with risk values.
- """
- layout_dict = cast(
- "dict[str, str | int | float | bool | list[str]]",
- fig["layout"],
- )
- base_colorway = cast("list[str]", layout_dict.get("colorway", []))
- if len(base_colorway) < len(point_frame.columns) and base_colorway:
- repeats = (len(point_frame.columns) + len(base_colorway) - 1) // len(
- base_colorway
- )
- colorway = (base_colorway * repeats)[: len(point_frame.columns)]
- else:
- colorway = base_colorway[: len(point_frame.columns)]
- for col, clr in zip(point_frame.columns, colorway, strict=True):
- returns.extend([cast("float", point_frame.loc["ret", col])])
- risk.extend([cast("float", point_frame.loc["stdev", col])])
- figure.add_scatter(
- x=[point_frame.loc["stdev", col]],
- y=[point_frame.loc["ret", col]],
- xhoverformat=".2%",
- yhoverformat=".2%",
- hovertext=[point_frame.loc["text", col]],
- hovertemplate="Return %{y}<br>Vol %{x}%{hovertext}",
- hoverlabel_align="right",
- marker={"size": 20, "color": clr},
- mode=point_frame_mode,
- name=col,
- text=col,
- textfont={"size": 14},
- textposition="bottom center",
- )
-
-
-def _configure_figure_layout(
- figure: Figure,
- titletext: str | None,
- logo: dict[str, Any],
- *,
- title: bool = True,
- add_logo: bool = True,
-) -> None:
- """Configure figure layout.
-
- Args:
- figure: Plotly figure.
- title: Whether to add title.
- titletext: Optional title text.
- add_logo: Whether to add logo.
- logo: Logo dictionary.
- """
- figure.update_layout(
- xaxis={"tickformat": ".1%"},
- xaxis_title="volatility",
- yaxis={
- "tickformat": ".1%",
- "scaleanchor": "x",
- "scaleratio": 1,
- },
- yaxis_title="annual return",
- showlegend=False,
- )
- if title:
- if titletext is None:
- titletext = "<b>Risk and Return</b><br>"
- figure.update_layout(title={"text": titletext, "font": {"size": 36}})
-
- if add_logo:
- figure.add_layout_image(logo)
-
-
-def _generate_sharpeplot_output(
- figure: Figure,
- plotfile: Path,
- filename: str,
- output_type: LiteralPlotlyOutput,
- include_plotlyjs: LiteralPlotlyJSlib,
- fig: dict[str, Any],
- *,
- auto_open: bool = True,
-) -> str:
- """Generate output for sharpeplot.
-
- Args:
- figure: Plotly figure.
- plotfile: Path to plot file.
- filename: Filename.
- output_type: Output type.
- include_plotlyjs: How to include plotly.js.
- fig: Plotly figure dictionary.
- auto_open: Whether to auto-open.
-
- Returns:
- Output string.
- """
- if output_type == "file":
- plot(
- figure_or_data=figure,
- filename=str(plotfile),
- auto_open=auto_open,
- auto_play=False,
- include_plotlyjs=include_plotlyjs,
- config=fig["config"],
- output_type=output_type,
- )
- return str(plotfile)
-
- div_id = filename.split(maxsplit=1, sep=".")[0]
- return cast(
- "str",
- to_html(
- fig=figure,
- config=fig["config"],
- auto_play=False,
- include_plotlyjs=include_plotlyjs,
- full_html=False,
- div_id=div_id,
- ),
- )
-
-
-
-[docs]
-def sharpeplot(
- sim_frame: DataFrame | None = None,
- line_frame: DataFrame | None = None,
- point_frame: DataFrame | None = None,
- point_frame_mode: LiteralLinePlotMode = "markers",
- filename: str | None = None,
- directory: DirectoryPath | None = None,
- titletext: str | None = None,
- output_type: LiteralPlotlyOutput = "file",
- include_plotlyjs: LiteralPlotlyJSlib = "cdn",
- *,
- title: bool = True,
- add_logo: bool = True,
- auto_open: bool = True,
-) -> tuple[Figure, str]:
- """Create scatter plot coloured by Sharpe Ratio.
-
- Args:
- sim_frame: Data from the simulate_portfolios method.
- line_frame: Data from the efficient_frontier method.
- point_frame: Data to highlight current and efficient portfolios.
- point_frame_mode: Which type of scatter to use. Defaults to markers.
- filename: Name of the Plotly html file.
- directory: Directory where Plotly html file is saved.
- titletext: Text for the plot title.
- output_type: Determines output type. Defaults to "file".
- include_plotlyjs: Determines how the plotly.js library is included
- in the output.
- Defaults to "cdn".
- title: Whether to add standard plot title. Defaults to True.
- add_logo: Whether to add Captor logo. Defaults to True.
- auto_open: Determines whether to open a browser window with the plot.
- Defaults to True.
-
- Returns:
- The scatter plot with simulated and optimized results.
- """
- if sim_frame is None and line_frame is None and point_frame is None:
- msg = "One of sim_frame, line_frame or point_frame must be provided."
- raise AtLeastOneFrameError(msg)
-
- returns: list[float] = []
- risk: list[float] = []
-
- dirpath = _determine_output_directory(directory)
- if not filename:
- filename = "sharpeplot.html"
- plotfile = dirpath.joinpath(filename)
-
- fig, logo = load_plotly_dict()
- figure = Figure(fig)
-
- if sim_frame is not None:
- _add_simulated_portfolios_trace(figure, sim_frame, returns, risk)
- if line_frame is not None:
- _add_efficient_frontier_trace(figure, line_frame, returns, risk)
- if point_frame is not None:
- _add_point_frame_traces(
- figure, point_frame, point_frame_mode, fig, returns, risk
- )
-
- _configure_figure_layout(figure, titletext, logo, title=title, add_logo=add_logo)
-
- string_output = _generate_sharpeplot_output(
- figure,
- plotfile,
- filename,
- output_type,
- include_plotlyjs,
- fig,
- auto_open=auto_open,
- )
-
- return figure, string_output
-
-
-"""Functions related to HTML reports."""
-
-from __future__ import annotations
-
-from inspect import stack
-from itertools import cycle
-from json import dumps as json_dumps
-from logging import getLogger
-from pathlib import Path
-from secrets import choice
-from string import ascii_letters
-from typing import TYPE_CHECKING, Any, cast
-from warnings import catch_warnings, simplefilter
-from webbrowser import open as webbrowser_open
-
-from pandas import DataFrame, Index, Series, Timestamp, concat, isna
-from plotly.graph_objs import Bar, Figure, Scatter # type: ignore[import-untyped]
-from plotly.utils import PlotlyJSONEncoder # type: ignore[import-untyped]
-
-from .html_utils import _get_base_css, _get_plotly_script
-from .load_plotly import load_plotly_dict
-from .owntypes import (
- CaptorLogoType,
- LiteralBizDayFreq,
- LiteralFrameProps,
- LiteralPlotlyJSlib,
- LiteralPlotlyOutput,
- ValueType,
-)
-
-if TYPE_CHECKING: # pragma: no cover
- from .frame import OpenFrame
-
-logger = getLogger(__name__)
-
-__all__ = ["report_html"]
-
-
-def calendar_period_returns(
- data: OpenFrame,
- freq: LiteralBizDayFreq = "BYE",
- *,
- relabel: bool = True,
-) -> DataFrame:
- """Generate a table of returns with appropriate table labels."""
- copied = data.from_deepcopy()
- copied.resample_to_business_period_ends(freq=freq)
- copied.value_to_ret()
- cldr = copied.tsdf.iloc[1:].copy()
- if relabel:
- if freq.upper() == "BYE":
- cldr.index = Index([d.year for d in cldr.index])
- elif freq.upper() == "BQE":
- cldr.index = Index(
- [Timestamp(d).to_period("Q").strftime("Q%q %Y") for d in cldr.index],
- )
- else:
- cldr.index = Index([d.strftime("%b %y") for d in cldr.index])
- return cldr
-
-
-def _dumps_plotly(obj: object) -> str:
- return json_dumps(obj, cls=PlotlyJSONEncoder)
-
-
-def _fmt_dates(idx: Index) -> list[str]:
- return [Timestamp(d).strftime("%Y-%m-%d") for d in idx]
-
-
-def _metrics_table_html(df: DataFrame) -> str:
- return df.to_html(index=False, escape=False, classes=["metrics"], border=0)
-
-
-def _get_report_properties_and_labels(
- yearfrac: float,
-) -> tuple[list[str], list[str], list[str]]:
- """Get properties and labels based on yearfrac."""
- if yearfrac > 1.0:
- properties = [
- "geo_ret",
- "vol",
- "ret_vol_ratio",
- "sortino_ratio",
- "worst_month",
- "first_indices",
- "last_indices",
- ]
- labels_init = [
- "Return (CAGR)",
- "Volatility",
- "Sharpe Ratio",
- "Sortino Ratio",
- "Worst Month",
- "Comparison Start",
- "Comparison End",
- "Jensen's Alpha",
- "Information Ratio",
- "Tracking Error (weekly)",
- "Capture Ratio (monthly)",
- "Index Beta (weekly)",
- ]
- labels_final = [
- "Return (CAGR)",
- "Year-to-Date",
- "Month-to-Date",
- "Volatility",
- "Sharpe Ratio",
- "Sortino Ratio",
- "Jensen's Alpha",
- "Information Ratio",
- "Tracking Error (weekly)",
- "Index Beta (weekly)",
- "Capture Ratio (monthly)",
- "Worst Month",
- "Comparison Start",
- "Comparison End",
- ]
- else:
- properties = [
- "value_ret",
- "vol",
- "ret_vol_ratio",
- "sortino_ratio",
- "worst",
- "first_indices",
- "last_indices",
- ]
- labels_init = [
- "Return (simple)",
- "Volatility",
- "Sharpe Ratio",
- "Sortino Ratio",
- "Worst Day",
- "Comparison Start",
- "Comparison End",
- "Jensen's Alpha",
- "Information Ratio",
- "Tracking Error (weekly)",
- "Index Beta (weekly)",
- ]
- labels_final = [
- "Return (simple)",
- "Year-to-Date",
- "Month-to-Date",
- "Volatility",
- "Sharpe Ratio",
- "Sortino Ratio",
- "Jensen's Alpha",
- "Information Ratio",
- "Tracking Error (weekly)",
- "Index Beta (weekly)",
- "Worst Day",
- "Comparison Start",
- "Comparison End",
- ]
- return properties, labels_init, labels_final
-
-
-def _create_line_traces(data: OpenFrame) -> list[Scatter]:
- """Create line traces for the plot."""
- x_line = _fmt_dates(data.tsdf.index)
- line_traces: list[Scatter] = []
- for item, lbl in enumerate(data.columns_lvl_zero):
- line_traces.append(
- Scatter(
- x=x_line,
- y=data.tsdf.iloc[:, item].tolist(),
- hovertemplate=f"{lbl}<br>%{{y:.2%}}<br>%{{x}}<extra></extra>",
- line={"width": 2.5, "dash": "solid"},
- mode="lines",
- name=lbl,
- showlegend=True,
- ),
- )
- return line_traces
-
-
-def _create_bar_traces(
- data: OpenFrame,
- bar_freq: LiteralBizDayFreq,
-) -> list[Bar]:
- """Create bar traces for the plot."""
- quarter_of_year = 0.25
- if data.yearfrac < quarter_of_year:
- tmp = data.from_deepcopy()
- bdf = tmp.value_to_ret().tsdf.iloc[1:]
- else:
- bdf = calendar_period_returns(data=data, freq=bar_freq)
-
- x_bar = [str(x) for x in bdf.index]
- bar_traces: list[Bar] = []
- for item in range(data.item_count):
- col_name = cast("tuple[str, ValueType]", bdf.iloc[:, item].name)
- bar_traces.append(
- Bar(
- x=x_bar,
- y=bdf.iloc[:, item].tolist(),
- hovertemplate=f"{col_name[0]}<br>%{{y:.2%}}<br>%{{x}}<extra></extra>",
- name=col_name[0],
- showlegend=False,
- ),
- )
- return bar_traces
-
-
-def _add_jensen_alpha(
- rpt_df: DataFrame,
- data: OpenFrame,
-) -> DataFrame:
- """Add Jensen's Alpha to the report dataframe."""
- alpha_frame = data.from_deepcopy()
- alpha_frame.to_cumret()
- with catch_warnings():
- simplefilter("ignore")
- alphas: list[str | float] = [
- alpha_frame.jensen_alpha(
- asset=(aname, ValueType.PRICE),
- market=(alpha_frame.columns_lvl_zero[-1], ValueType.PRICE),
- riskfree_rate=0.0,
- )
- for aname in alpha_frame.columns_lvl_zero[:-1]
- ]
- alphas.append("")
- ar = DataFrame(
- data=alphas,
- index=data.tsdf.columns,
- columns=["Jensen's Alpha"],
- ).T
- return concat([rpt_df, ar])
-
-
-def _add_information_ratio(
- rpt_df: DataFrame,
- data: OpenFrame,
-) -> DataFrame:
- """Add Information Ratio to the report dataframe."""
- ir = data.info_ratio_func()
- ir.name = "Information Ratio"
- ir.iloc[-1] = None
- ir_df = ir.to_frame().T
- return concat([rpt_df, ir_df])
-
-
-def _add_tracking_error(
- rpt_df: DataFrame,
- data: OpenFrame,
-) -> DataFrame:
- """Add Tracking Error to the report dataframe."""
- te_frame = data.from_deepcopy()
- te_frame.resample("7D")
- with catch_warnings():
- simplefilter("ignore")
- te: Series[float] | Series[str] = te_frame.tracking_error_func()
- if te.hasnans:
- te = Series(
- data=[""] * te_frame.item_count,
- index=te_frame.tsdf.columns,
- name="Tracking Error (weekly)",
- )
- else:
- te.iloc[-1] = None
- te.name = "Tracking Error (weekly)"
- te_df = te.to_frame().T
- return concat([rpt_df, te_df])
-
-
-def _add_capture_ratio(
- rpt_df: DataFrame,
- data: OpenFrame,
- formats: list[str],
-) -> tuple[DataFrame, list[str]]:
- """Add Capture Ratio to the report dataframe."""
- crm = data.from_deepcopy()
- crm.resample("ME")
- cru_save = Series(
- data=[""] * crm.item_count,
- index=crm.tsdf.columns,
- name="Capture Ratio (monthly)",
- )
- with catch_warnings():
- simplefilter("ignore")
- try:
- cru: Series[float] | Series[str] = crm.capture_ratio_func(ratio="both")
- except ZeroDivisionError as exc: # pragma: no cover
- msg = f"Capture ratio calculation error: {exc!s}" # pragma: no cover
- logger.warning(msg) # pragma: no cover
- cru = cru_save # pragma: no cover
- if cru.hasnans:
- cru = cru_save
- else:
- cru.iloc[-1] = None
- cru.name = "Capture Ratio (monthly)"
- cru_df = cru.to_frame().T
- return concat([rpt_df, cru_df]), formats
-
-
-def _add_beta(
- rpt_df: DataFrame,
- data: OpenFrame,
-) -> DataFrame:
- """Add Index Beta to the report dataframe."""
- beta_frame = data.from_deepcopy()
- beta_frame.resample("7D").value_nan_handle("drop")
- beta_frame.to_cumret()
- betas: list[str | float] = [
- beta_frame.beta(
- asset=(bname, ValueType.PRICE),
- market=(beta_frame.columns_lvl_zero[-1], ValueType.PRICE),
- )
- for bname in beta_frame.columns_lvl_zero[:-1]
- ]
- betas.append("")
- br = DataFrame(
- data=betas,
- index=data.tsdf.columns,
- columns=["Index Beta (weekly)"],
- ).T
- return concat([rpt_df, br])
-
-
-def _add_ytd_mtd(
- rpt_df: DataFrame,
- data: OpenFrame,
-) -> DataFrame:
- """Add Year-to-Date and Month-to-Date to the report dataframe."""
- this_year = data.last_idx.year
- this_month = data.last_idx.month
- ytd = data.value_ret_calendar_period(year=this_year).map("{:.2%}".format)
- ytd.name = "Year-to-Date"
- mtd = data.value_ret_calendar_period(year=this_year, month=this_month).map(
- "{:.2%}".format,
- )
- mtd.name = "Month-to-Date"
- ytd_df = ytd.to_frame().T
- mtd_df = mtd.to_frame().T
- return concat([rpt_df, ytd_df, mtd_df])
-
-
-def _get_output_directory(directory: Path | None) -> Path:
- """Determine the output directory."""
- if directory:
- return Path(directory).resolve()
- if Path.home().joinpath("Documents").exists():
- return Path.home() / "Documents"
- return Path(stack()[1].filename).parent
-
-
-def _get_plotly_layouts(
- layout_theme: dict[str, Any],
- colorway: list[str],
- item_count: int,
-) -> tuple[dict[str, Any], dict[str, Any]]:
- """Get line and bar layouts for plotly."""
- line_layout = dict(layout_theme)
- line_layout.update(
- {
- "colorway": colorway[:item_count] if colorway else None,
- "margin": {"l": 50, "r": 20, "t": 20, "b": 40},
- "xaxis": {"gridcolor": "#EEEEEE", "automargin": True, "tickangle": -45},
- "yaxis": {"tickformat": ".2%", "gridcolor": "#EEEEEE", "automargin": True},
- "showlegend": False,
- },
- )
-
- bar_layout = dict(layout_theme)
- bar_layout.update(
- {
- "barmode": "group",
- "margin": {"l": 50, "r": 20, "t": 10, "b": 80},
- "xaxis": {"gridcolor": "#EEEEEE", "automargin": True, "tickangle": -45},
- "yaxis": {"tickformat": ".2%", "gridcolor": "#EEEEEE", "automargin": True},
- "showlegend": False,
- },
- )
-
- return line_layout, bar_layout
-
-
-def _get_logo_html(logo: CaptorLogoType, *, add_logo: bool) -> str:
- """Get logo HTML."""
- if not add_logo:
- return ""
- try:
- src = cast("dict[str, Any]", logo).get("source", "")
- except (KeyError, AttributeError, TypeError):
- src = ""
- if src:
- return f'<img src="{src}" alt="Captor" style="height:68px;" />'
- return "CAPTOR"
-
-
-def _get_legend_html(line_traces: list[Scatter], colorway: list[str]) -> str:
- """Generate HTML for the legend at the bottom of the page."""
- legend_items = []
- color_cycle = cycle(colorway or ["#66725B"])
- for trace in line_traces:
- name = trace.name or ""
- color = next(color_cycle)
- legend_items.append(
- f'<div class="legend-item">'
- f'<div class="legend-color" style="background-color:{color};"></div>'
- f"<span>{name}</span>"
- f"</div>",
- )
- if legend_items:
- return f'<div class="legend-container">{"".join(legend_items)}</div>'
- return ""
-
-
-def _get_css() -> str:
- """Get CSS styles for the HTML report."""
- base_css = _get_base_css()
- return (
- base_css
- + """
- .header{display:grid;grid-template-columns:140px 1fr 140px;gap:12px;
- align-items:start;}
- h1{margin:0;text-align:center;font-size:45px;font-weight:800;}
- .layout{display:grid;grid-template-columns:1.2fr .9fr;
- grid-template-areas:"charts table";gap:22px;align-items:start;margin-top:12px;}
- .charts{grid-area:charts;display:grid;grid-template-rows:auto auto;gap:18px;}
- .table{grid-area:table;}
- .plot{width:100%;height:380px;}
- .plot.bar{height:300px;}
- @media (max-width:980px){
- .page{padding:24px;padding-bottom:24px;}
- .header{grid-template-columns:120px 1fr;}
- h1{font-size:36px;}
- .layout{grid-template-columns:1fr;grid-template-areas:"table" "charts";gap:16px;}
- .plot{height:380px;}
- .plot.bar{height:300px;}
- table.metrics{table-layout:fixed;width:auto;}
- table.metrics thead th{min-width:120px;width:120px;white-space:nowrap;}
- table.metrics thead th:first-child{width:180px;}
- table.metrics tbody td{min-width:120px;width:120px;}
- table.metrics tbody td:first-child{width:180px;}
- }
- table.metrics{width:100%;border-collapse:separate;border-spacing:0;font-size:12px;
- border-radius:4px;overflow:hidden;table-layout:fixed;}
- table.metrics thead th{background:var(--header);color:white;padding:8px 10px;
- font-weight:700;text-align:center;word-wrap:break-word;word-break:break-word;}
- table.metrics thead th:first-child{background:var(--header2);text-align:left;
- width:180px;}
- table.metrics tbody td{padding:7px 10px;border-bottom:1px solid white;
- border-right:1px solid white;text-align:center;background:var(--paper);}
- table.metrics tbody td:first-child{text-align:left;font-weight:600;color:white;
- background:var(--header);width:180px;}
- table.metrics tbody td:last-child{background:var(--cell2);}
- .legend-container{margin-top:24px;padding-top:20px;padding-bottom:16px;
- display:flex;justify-content:center;flex-wrap:wrap;gap:24px;flex-shrink:0;}
- .legend-item{display:flex;align-items:center;gap:8px;font-size:14px;}
- .legend-color{width:20px;height:3px;border-radius:2px;}
- @media (min-width:981px){
- html,body{overflow-y:auto;}
- }
- """
- )
-
-
-def _write_html_file(
- plotfile: Path,
- html: str,
- *,
- auto_open: bool,
-) -> str:
- """Write HTML file and optionally open it."""
- plotfile.parent.mkdir(parents=True, exist_ok=True)
- plotfile.write_text(html, encoding="utf-8")
- if auto_open:
- try:
- webbrowser_open(plotfile.as_uri())
- except OSError as exc:
- logger.warning("Failed to open browser: %s", exc)
- return str(plotfile)
-
-
-def _generate_html(
- title: str | None,
- css: str,
- plotly_script: str,
- logo_html: str,
- table_html: str,
- line_payload: dict[str, Any],
- bar_payload: dict[str, Any],
- legend_html: str,
-) -> str:
- """Generate the HTML string."""
- return f"""<!doctype html>
-<html lang="sv">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>{title or ""}</title>
-<style>{css}</style>
-{plotly_script}
-</head>
-<body>
-<div class="page">
- <div class="header">
- <div>{logo_html}</div>
- <div><h1>{title or ""}</h1></div>
- <div></div>
- </div>
- <div class="layout">
- <div class="charts">
- <div id="lineplot" class="plot"></div>
- <div id="barplot" class="plot bar"></div>
- </div>
- <div class="table">{table_html}</div>
- </div>
- {legend_html}
-</div>
-<script>
-const line = {_dumps_plotly(line_payload)};
-const bar = {_dumps_plotly(bar_payload)};
-Plotly.newPlot("lineplot", line.data, line.layout, line.config);
-Plotly.newPlot("barplot", bar.data, bar.layout, bar.config);
-window.addEventListener("resize", () => {{
- Plotly.Plots.resize("lineplot");
- Plotly.Plots.resize("barplot");
-}});
-</script>
-</body>
-</html>
-"""
-
-
-
-[docs]
-def report_html(
- data: OpenFrame,
- bar_freq: LiteralBizDayFreq = "BYE",
- filename: str | None = None,
- title: str | None = None,
- directory: Path | None = None,
- output_type: LiteralPlotlyOutput = "file",
- include_plotlyjs: LiteralPlotlyJSlib = "cdn",
- *,
- auto_open: bool = False,
- add_logo: bool = True,
- vertical_legend: bool = True,
-) -> tuple[Figure, str]:
- """Generate a responsive HTML report page with line and bar plots and a table."""
- copied = data.from_deepcopy()
- copied.trunc_frame().value_nan_handle().to_cumret()
-
- properties, labels_init, labels_final = _get_report_properties_and_labels(
- copied.yearfrac,
- )
-
- line_traces = _create_line_traces(copied)
- bar_traces = _create_bar_traces(copied, bar_freq)
-
- rpt_df = copied.all_properties(
- properties=cast("list[LiteralFrameProps]", properties),
- )
- rpt_df = _add_jensen_alpha(rpt_df, copied)
- rpt_df = _add_information_ratio(rpt_df, copied)
- rpt_df = _add_tracking_error(rpt_df, copied)
-
- if copied.yearfrac > 1.0:
- rpt_df, _ = _add_capture_ratio(rpt_df, copied, [])
-
- rpt_df = _add_beta(rpt_df, copied)
- rpt_df.index = Index(labels_init)
- rpt_df = _add_ytd_mtd(rpt_df, copied)
- rpt_df = rpt_df.reindex(labels_final)
-
- format_map = {
- "Return (CAGR)": "{:.2%}",
- "Return (simple)": "{:.2%}",
- "Year-to-Date": "{:.2%}",
- "Month-to-Date": "{:.2%}",
- "Volatility": "{:.2%}",
- "Sharpe Ratio": "{:.2f}",
- "Sortino Ratio": "{:.2f}",
- "Jensen's Alpha": "{:.2%}",
- "Information Ratio": "{:.2f}",
- "Tracking Error (weekly)": "{:.2%}",
- "Index Beta (weekly)": "{:.2f}",
- "Capture Ratio (monthly)": "{:.2f}",
- "Worst Month": "{:.2%}",
- "Worst Day": "{:.2%}",
- "Comparison Start": "{:%Y-%m-%d}",
- "Comparison End": "{:%Y-%m-%d}",
- }
- formats = [format_map.get(label, "{:.2f}") for label in labels_final]
-
- for item, f in zip(rpt_df.index, formats, strict=False):
- rpt_df.loc[item] = rpt_df.loc[item].apply(
- lambda x, fmt=f: (
- ""
- if (
- x is None
- or (not isinstance(x, str) and isna(x))
- or (isinstance(x, str) and x.lower() in ("nan", "nan%", ""))
- )
- else (
- str(x)
- if isinstance(x, str)
- else (
- Timestamp(x).strftime("%Y-%m-%d")
- if "%Y-%m-%d" in fmt and not isinstance(x, str)
- else fmt.format(x)
- )
- )
- ),
- )
-
- rpt_df.index = Index([f"<b>{x}</b>" for x in rpt_df.index])
- rpt_df = rpt_df.reset_index()
-
- colmns = ["", *copied.columns_lvl_zero]
- rpt_df.columns = colmns
- table_html = _metrics_table_html(rpt_df)
-
- dirpath = _get_output_directory(directory=directory)
-
- if not filename:
- filename = "".join(choice(ascii_letters) for _ in range(6)) + ".html"
-
- plotfile = dirpath / filename
-
- fig_theme, logo = load_plotly_dict()
- layout_theme = cast("dict[str, Any]", fig_theme.get("layout", {}))
- colorway: list[str] = cast("dict[str, list[str]]", layout_theme).get(
- "colorway", []
- )
-
- line_layout, bar_layout = _get_plotly_layouts(
- layout_theme=layout_theme,
- colorway=colorway,
- item_count=copied.item_count,
- )
-
- config = cast("dict[str, Any]", fig_theme.get("config", {})) or {}
- config = {**config, "responsive": True, "displayModeBar": False}
-
- plotly_script = _get_plotly_script(include_plotlyjs=include_plotlyjs)
- logo_html = _get_logo_html(logo=logo, add_logo=add_logo)
- css = _get_css()
-
- line_payload = {
- "data": [t.to_plotly_json() for t in line_traces],
- "layout": line_layout,
- "config": config,
- }
- bar_payload = {
- "data": [t.to_plotly_json() for t in bar_traces],
- "layout": bar_layout,
- "config": config,
- }
-
- if not vertical_legend:
- logger.debug("Horizontal legend layout requested.")
- legend_html = _get_legend_html(line_traces=line_traces, colorway=colorway)
-
- html = _generate_html(
- title=title,
- css=css,
- plotly_script=plotly_script,
- logo_html=logo_html,
- table_html=table_html,
- line_payload=line_payload,
- bar_payload=bar_payload,
- legend_html=legend_html,
- )
-
- if output_type == "file":
- output = _write_html_file(plotfile=plotfile, html=html, auto_open=auto_open)
- else:
- output = html
-
- fig_return = Figure(data=line_traces)
- return fig_return, output
-
-
-"""The OpenTimeSeries class."""
-
-from __future__ import annotations
-
-import datetime as dt
-from copy import deepcopy
-from logging import getLogger
-from typing import TYPE_CHECKING, Any, Self, TypeVar, cast
-
-if TYPE_CHECKING: # pragma: no cover
- from numpy.typing import NDArray
- from pandas import Timestamp
-
-from numpy import (
- append,
- array,
- asarray,
- cumprod,
- diff,
- float64,
- insert,
- isnan,
- log,
- sqrt,
- square,
-)
-from pandas import (
- DataFrame,
- DatetimeIndex,
- Index,
- MultiIndex,
- Series,
- date_range,
-)
-from pydantic import field_validator, model_validator
-from scipy.stats import chi2, norm
-
-from ._common_model import (
- _calculate_time_factor,
- _CommonModel,
- _demeaned_returns_for_autocorr,
-)
-from .datefixer import _do_resample_to_business_period_ends, date_fix
-from .owntypes import (
- Countries,
- CountriesType,
- Currency,
- CurrencyStringType,
- DateAlignmentError,
- DateListType,
- DaysInYearType,
- IncorrectArgumentComboError,
- LiteralBizDayFreq,
- LiteralPandasReindexMethod,
- LiteralSeriesProps,
- MarketsNotStringNorListStrError,
- OpenTimeSeriesPropertiesList,
- ResampleDataLossError,
- ValueListType,
- ValueType,
-)
-
-logger = getLogger(__name__)
-
-__all__ = ["OpenTimeSeries", "timeseries_chain"]
-
-TypeOpenTimeSeries = TypeVar("TypeOpenTimeSeries", bound="OpenTimeSeries")
-
-
-
-[docs]
-class OpenTimeSeries(_CommonModel[float]):
- """OpenTimeSeries objects are at the core of the openseries package.
-
- The intended use is to allow analyses of financial timeseries.
- It is only intended for daily or less frequent data samples.
-
- Args:
- timeseries_id: Database identifier of the timeseries.
- instrument_id: Database identifier of the instrument associated with
- the timeseries.
- name: String identifier of the timeseries and/or instrument.
- valuetype: Identifies if the series is a series of values or returns.
- dates: Dates of the individual timeseries items.
- These dates will not be altered by methods.
- values: The value or return values of the timeseries items.
- These values will not be altered by methods.
- local_ccy: Boolean flag indicating if timeseries is in local currency.
- tsdf: Pandas object holding dates and values that can be altered via
- methods.
- currency: ISO 4217 currency code of the timeseries.
- domestic: ISO 4217 currency code of the user's home currency.
- Defaults to "SEK".
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
- Optional.
- isin: ISO 6166 identifier code of the associated instrument. Optional.
- label: Placeholder for a name of the timeseries. Optional.
- """
-
- timeseries_id: str
- instrument_id: str
- name: str
- valuetype: ValueType
- dates: DateListType
- values: ValueListType
- local_ccy: bool
- tsdf: DataFrame
- currency: CurrencyStringType
- domestic: CurrencyStringType = "SEK"
- countries: CountriesType = "SE"
- isin: str | None = None
- label: str | None = None
-
- @field_validator("domestic", mode="before")
- @classmethod
- def _validate_domestic(cls, value: CurrencyStringType) -> CurrencyStringType:
- """Pydantic validator to ensure domestic field is validated."""
- Currency(ccy=value)
- return value
-
- @field_validator("countries", mode="before")
- @classmethod
- def _validate_countries(cls, value: CountriesType) -> CountriesType:
- """Pydantic validator to ensure countries field is validated."""
- Countries(countryinput=value)
- return value
-
- @field_validator("markets", mode="before")
- @classmethod
- def _validate_markets(
- cls,
- value: list[str] | str | None,
- ) -> list[str] | str | None:
- """Pydantic validator to ensure markets field is validated.
-
- Raises:
- MarketsNotStringNorListStrError: If ``markets`` is neither a string
- nor a non-empty list of strings.
- """
- msg = (
- "'markets' must be a string or list of strings, "
- f"got {type(value).__name__!r}"
- )
- if value is None or isinstance(value, str):
- return value
- if isinstance(value, list):
- if all(isinstance(item, str) for item in value) and len(value) != 0:
- return value
- item_msg = "All items in 'markets' must be strings."
- raise MarketsNotStringNorListStrError(item_msg)
- raise MarketsNotStringNorListStrError(msg)
-
- @model_validator(mode="after")
- def _dates_and_values_validate(self: Self) -> Self:
- """Pydantic validator to ensure dates and values are validated.
-
- Raises:
- ValueError: If dates are not unique or if numbers of dates and values
- do not match the shape of ``tsdf``.
- """
- values_list_length = len(self.values)
- dates_list_length = len(self.dates)
- dates_set_length = len(set(self.dates))
- if dates_list_length != dates_set_length:
- msg = "Dates are not unique"
- raise ValueError(msg)
- if (
- (dates_list_length != values_list_length)
- or (len(self.tsdf.index) != self.tsdf.shape[0])
- or (self.tsdf.shape[1] != 1)
- ):
- msg = "Number of dates and values passed do not match"
- raise ValueError(msg)
- return self
-
- def _coerce_result(
- self: Self,
- result: Series[float],
- name: str,
- ) -> float:
- _ = name
- return float(asarray(a=result, dtype=float64).squeeze())
-
-
-[docs]
- @classmethod
- def from_arrays(
- cls,
- name: str,
- dates: DateListType,
- values: ValueListType,
- valuetype: ValueType = ValueType.PRICE,
- timeseries_id: str = "",
- instrument_id: str = "",
- isin: str | None = None,
- baseccy: CurrencyStringType = "SEK",
- *,
- local_ccy: bool = True,
- ) -> Self:
- """Create series from a list of dates and a list of values.
-
- Args:
- name: String identifier of the timeseries and/or instrument.
- dates: List of date strings as ISO 8601 YYYY-MM-DD.
- values: Array of float values.
- valuetype: Identifies if the series is a series of values or returns.
- Defaults to ValueType.PRICE.
- timeseries_id: Database identifier of the timeseries. Optional.
- instrument_id: Database identifier of the instrument associated
- with the timeseries. Optional.
- isin: ISO 6166 identifier code of the associated instrument. Optional.
- baseccy: ISO 4217 currency code of the timeseries. Defaults to "SEK".
- local_ccy: Boolean flag indicating if timeseries is in local currency.
- Defaults to True.
-
- Returns:
- An OpenTimeSeries object.
- """
- return cls(
- name=name,
- label=name,
- dates=dates,
- values=values,
- valuetype=valuetype,
- timeseries_id=timeseries_id,
- instrument_id=instrument_id,
- isin=isin,
- currency=baseccy,
- local_ccy=local_ccy,
- tsdf=DataFrame(
- data=values,
- index=[deyt.date() for deyt in DatetimeIndex(dates)],
- columns=[[name], [valuetype]],
- dtype="float64",
- ),
- )
-
-
-
-[docs]
- @classmethod
- def from_df(
- cls,
- dframe: Series | DataFrame | object,
- column_nmbr: int = 0,
- valuetype: ValueType = ValueType.PRICE,
- baseccy: CurrencyStringType = "SEK",
- *,
- local_ccy: bool = True,
- ) -> Self:
- """Create series from a Pandas DataFrame or Series.
-
- Args:
- dframe: Pandas DataFrame or Series.
- column_nmbr: Using iloc[:, column_nmbr] to pick column. Defaults to 0.
- valuetype: Identifies if the series is a series of values or returns.
- Defaults to ValueType.PRICE.
- baseccy: ISO 4217 currency code of the timeseries. Defaults to "SEK".
- local_ccy: Boolean flag indicating if timeseries is in local currency.
- Defaults to True.
-
- Returns:
- An OpenTimeSeries object.
-
- Raises:
- TypeError: If ``dframe`` is not a ``pandas.Series`` or a
- ``pandas.DataFrame``.
- """
- msg = "Argument dframe must be pandas Series or DataFrame."
- values: list[float]
- pandas_obj: Series | DataFrame
- if isinstance(dframe, Series):
- pandas_obj = dframe
- if isinstance(dframe.name, tuple):
- label, _ = dframe.name
- else:
- label = dframe.name
- values = dframe.to_numpy().tolist()
- elif isinstance(dframe, DataFrame):
- pandas_obj = dframe
- values = dframe.iloc[:, column_nmbr].to_list()
- if isinstance(dframe.columns, MultiIndex):
- if _check_if_none(
- dframe.columns.get_level_values(0).to_numpy()[column_nmbr],
- ):
- label = "Series"
- msg = f"Label missing. Adding: {label}"
- logger.warning(msg)
- else:
- label = dframe.columns.get_level_values(0).to_numpy()[column_nmbr]
- if _check_if_none(
- dframe.columns.get_level_values(1).to_numpy()[column_nmbr],
- ):
- valuetype = ValueType.PRICE
- msg = f"valuetype missing. Adding: {valuetype.value}"
- logger.warning(msg)
- else:
- valuetype = dframe.columns.get_level_values(1).to_numpy()[
- column_nmbr
- ]
- else:
- label = dframe.columns.to_numpy()[column_nmbr]
- else:
- raise TypeError(msg)
-
- dates = [date_fix(d).strftime("%Y-%m-%d") for d in pandas_obj.index]
-
- return cls(
- timeseries_id="",
- instrument_id="",
- currency=baseccy,
- dates=dates,
- name=label,
- label=label,
- valuetype=valuetype,
- values=values,
- local_ccy=local_ccy,
- tsdf=DataFrame(
- data=values,
- index=[deyt.date() for deyt in DatetimeIndex(dates)],
- columns=[[label], [valuetype]],
- dtype="float64",
- ),
- )
-
-
-
-[docs]
- @classmethod
- def from_fixed_rate(
- cls,
- rate: float,
- d_range: DatetimeIndex | None = None,
- days: int | None = None,
- end_dt: dt.date | None = None,
- label: str = "Series",
- valuetype: ValueType = ValueType.PRICE,
- baseccy: CurrencyStringType = "SEK",
- *,
- local_ccy: bool = True,
- ) -> Self:
- """Create series from values accruing with a given fixed rate return.
-
- Providing a date_range of type Pandas DatetimeIndex takes priority over
- providing a combination of days and an end date.
-
- Args:
- rate: The accrual rate.
- d_range: A given range of dates. Optional.
- days: Number of days to generate when date_range not provided. Must be
- combined with end_dt. Optional.
- end_dt: End date of date range to generate when date_range not provided.
- Must be combined with days. Optional.
- label: Placeholder for a name of the timeseries.
- valuetype: Identifies if the series is a series of values or returns.
- Defaults to ValueType.PRICE.
- baseccy: The currency of the timeseries. Defaults to "SEK".
- local_ccy: Boolean flag indicating if timeseries is in local currency.
- Defaults to True.
-
- Returns:
- An OpenTimeSeries object.
-
- Raises:
- IncorrectArgumentComboError: If ``d_range`` is not provided and the
- combination of ``days`` and ``end_dt`` is incomplete.
- """
- if d_range is None:
- if days is not None and end_dt is not None:
- d_range = DatetimeIndex(
- [d.date() for d in date_range(periods=days, end=end_dt, freq="D")],
- )
- else:
- msg = "If d_range is not provided both days and end_dt must be."
- raise IncorrectArgumentComboError(msg)
- deltas = array([i.days for i in d_range[1:] - d_range[:-1]])
- arr: list[float] = list(cumprod(insert(1 + deltas * rate / 365, 0, 1.0)))
- dates = [d.strftime("%Y-%m-%d") for d in d_range]
-
- return cls(
- timeseries_id="",
- instrument_id="",
- currency=baseccy,
- dates=dates,
- name=label,
- label=label,
- valuetype=valuetype,
- values=arr,
- local_ccy=local_ccy,
- tsdf=DataFrame(
- data=arr,
- index=[d.date() for d in DatetimeIndex(dates)],
- columns=[[label], [valuetype]],
- dtype="float64",
- ),
- )
-
-
-
-[docs]
- def from_deepcopy(self: Self) -> Self:
- """Create copy of OpenTimeSeries object.
-
- Returns:
- An OpenTimeSeries object.
- """
- return deepcopy(self)
-
-
-
-[docs]
- def pandas_df(self: Self) -> Self:
- """Populate .tsdf Pandas DataFrame from the .dates and .values lists.
-
- Returns:
- An OpenTimeSeries object.
- """
- dframe = DataFrame(
- data=self.values,
- index=[d.date() for d in DatetimeIndex(self.dates)],
- columns=[[self.label], [self.valuetype]],
- dtype="float64",
- )
- self.tsdf = dframe
-
- return self
-
-
-
-[docs]
- def all_properties(
- self: Self,
- properties: list[LiteralSeriesProps] | None = None,
- ) -> DataFrame:
- """Calculate chosen properties.
-
- Args:
- properties: The properties to calculate. Defaults to calculating all
- available. Optional.
-
- Returns:
- Properties of the OpenTimeSeries.
- """
- if not properties:
- properties = cast(
- "list[LiteralSeriesProps]",
- OpenTimeSeriesPropertiesList.allowed_strings,
- )
-
- props = OpenTimeSeriesPropertiesList(*properties)
-
- def _prop_value(name: str) -> float | int | dt.date | Series[float]:
- attr = getattr(self, name)
- return cast(
- "float | int | dt.date | Series[float]",
- attr() if callable(attr) else attr,
- )
-
- pdf = DataFrame.from_dict(
- {x: _prop_value(x) for x in props},
- orient="index",
- )
- pdf.columns = self.tsdf.columns
- return pdf
-
-
-
-[docs]
- def value_to_ret(self: Self) -> Self:
- """Convert series of values into series of returns.
-
- Returns:
- The returns of the values in the series.
- """
- returns = self.tsdf.ffill().pct_change()
- returns.iloc[0] = 0
- self.valuetype = ValueType.RTRN
- arrays = cast("Any", [[self.label], [self.valuetype]])
- returns.columns = MultiIndex.from_arrays(arrays)
- self.tsdf = returns.copy()
- return self
-
-
-
-[docs]
- def value_to_diff(self: Self, periods: int = 1) -> Self:
- """Convert series of values to series of their period differences.
-
- Args:
- periods: The number of periods between observations over which difference
- is calculated. Defaults to 1.
-
- Returns:
- An OpenTimeSeries object.
- """
- self.tsdf = self.tsdf.diff(periods=periods)
- self.tsdf.iloc[0] = 0
- self.valuetype = ValueType.RTRN
- self.tsdf.columns = MultiIndex.from_arrays(
- [
- [self.label],
- [self.valuetype],
- ],
- )
- return self
-
-
-
-[docs]
- def to_cumret(self: Self) -> Self:
- """Convert series of returns into cumulative series of values.
-
- Returns:
- An OpenTimeSeries object.
- """
- if self.valuetype == ValueType.PRICE:
- self.value_to_ret()
-
- self.tsdf = self.tsdf.add(1.0)
- self.tsdf = self.tsdf.cumprod(axis=0) / self.tsdf.iloc[0]
-
- self.valuetype = ValueType.PRICE
- self.tsdf.columns = MultiIndex.from_arrays(
- [
- [self.label],
- [self.valuetype],
- ],
- )
- return self
-
-
-
-[docs]
- def from_1d_rate_to_cumret(
- self: Self,
- days_in_year: int = 365,
- divider: float = 1.0,
- ) -> Self:
- """Convert series of 1-day rates into series of cumulative values.
-
- Args:
- days_in_year: Calendar days per year used as divisor. Defaults to 365.
- divider: Convenience divider for when the 1-day rate is not scaled
- correctly. Defaults to 1.0.
-
- Returns:
- An OpenTimeSeries object.
- """
- arr: NDArray[float64] = array(self.values) / divider
-
- deltas = array([i.days for i in self.tsdf.index[1:] - self.tsdf.index[:-1]])
- arr = cast(
- "NDArray[float64]",
- cumprod(
- a=insert(
- arr=1.0 + deltas * arr[:-1] / days_in_year, obj=0, values=1.0
- ),
- ),
- )
-
- self.dates = [d.strftime("%Y-%m-%d") for d in self.tsdf.index]
- self.values = list(arr)
- self.valuetype = ValueType.PRICE
- self.tsdf = DataFrame(
- data=self.values,
- index=[d.date() for d in DatetimeIndex(self.dates)],
- columns=[[self.label], [self.valuetype]],
- dtype="float64",
- )
-
- return self
-
-
-
-[docs]
- def resample(
- self: Self,
- freq: LiteralBizDayFreq | str = "BME",
- ) -> Self:
- """Resamples the timeseries frequency.
-
- Args:
- freq: The date offset string that sets the resampled frequency.
- Defaults to "BME".
-
- Returns:
- An OpenTimeSeries object.
- """
- self.tsdf.index = DatetimeIndex(self.tsdf.index)
- if self.valuetype == ValueType.RTRN:
- self.tsdf = self.tsdf.resample(freq).sum()
- else:
- self.tsdf = self.tsdf.resample(freq).last()
- self.tsdf.index = Index(DatetimeIndex(self.tsdf.index).date)
- return self
-
-
-
-[docs]
- def resample_to_business_period_ends(
- self: Self,
- freq: LiteralBizDayFreq = "BME",
- method: LiteralPandasReindexMethod = "nearest",
- ) -> Self:
- """Resamples timeseries frequency to the business calendar month end dates.
-
- Stubs left in place. Stubs will be aligned to the shortest stub.
-
- Args:
- freq: The date offset string that sets the resampled frequency.
- Defaults to BME.
- method: Controls the method used to align values across columns.
- Defaults to nearest.
-
- Returns:
- An OpenTimeSeries object.
-
- Raises:
- ResampleDataLossError: If called on a return series (``valuetype`` is
- ``ValueType.RTRN``), since summation across sparser frequency would
- be required to avoid data loss.
- """
- if self.valuetype == ValueType.RTRN:
- msg = (
- "Do not run resample_to_business_period_ends on return series. "
- "The operation will pick the last data point in the sparser series. "
- "It will not sum returns and therefore data will be lost."
- )
- raise ResampleDataLossError(msg)
-
- dates = _do_resample_to_business_period_ends(
- data=self.tsdf,
- freq=freq,
- countries=self.countries,
- markets=self.markets,
- )
- self.tsdf = self.tsdf.reindex([deyt.date() for deyt in dates], method=method)
- return self
-
-
-
-[docs]
- def ewma_vol_func(
- self: Self,
- lmbda: float = 0.94,
- day_chunk: int = 11,
- dlta_degr_freedms: int = 0,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> Series[float]:
- """Exponentially Weighted Moving Average Model for Volatility.
-
- Reference: https://www.investopedia.com/articles/07/ewma.asp.
-
- Args:
- lmbda: Scaling factor to determine weighting. Defaults to 0.94.
- day_chunk: Sampling the data which is assumed to be daily.
- Defaults to 11.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 0.
- months_from_last: Number of months offset as positive integer.
- Overrides use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Series EWMA volatility.
- """
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
- time_factor = _calculate_time_factor(
- data=self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ].iloc[:, 0],
- earlier=earlier,
- later=later,
- periods_in_a_year_fixed=periods_in_a_year_fixed,
- )
-
- data = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ].copy()
-
- data.loc[:, (self.label, ValueType.RTRN)] = log(
- data.loc[:, self.tsdf.columns.to_numpy()[0]],
- ).diff()
-
- rawdata = [
- data[(self.label, ValueType.RTRN)]
- .iloc[1:day_chunk]
- .std(ddof=dlta_degr_freedms)
- * sqrt(time_factor),
- ]
-
- for item in data[(self.label, ValueType.RTRN)].iloc[1:]:
- prev = rawdata[-1]
- rawdata.append(
- sqrt(
- square(item) * time_factor * (1 - lmbda) + square(prev) * lmbda,
- ),
- )
-
- return Series(
- data=rawdata,
- index=data.index,
- name=(self.label, ValueType.EWMA_VOL),
- dtype="float64",
- )
-
-
-
-[docs]
- def ewma_var_func(
- self: Self,
- lmbda: float = 0.94,
- day_chunk: int = 11,
- level: float = 0.95,
- dlta_degr_freedms: int = 0,
- months_from_last: int | None = None,
- from_date: dt.date | None = None,
- to_date: dt.date | None = None,
- periods_in_a_year_fixed: DaysInYearType | None = None,
- ) -> Series[float]:
- """Exponentially Weighted Moving Average Model for Value At Risk (VaR).
-
- Reference: https://www.investopedia.com/articles/07/ewma.asp.
-
- Args:
- lmbda: Scaling factor to determine weighting. Defaults to 0.94.
- day_chunk: Sampling the data which is assumed to be daily.
- Defaults to 11.
- level: The sought VaR level. Defaults to 0.95.
- dlta_degr_freedms: Variance bias factor taking the value 0 or 1.
- Defaults to 0.
- months_from_last: Number of months offset as positive integer.
- Overrides use of from_date and to_date. Optional.
- from_date: Specific from date. Optional.
- to_date: Specific to date. Optional.
- periods_in_a_year_fixed: Allows locking the periods-in-a-year to simplify
- test cases and comparisons. Optional.
-
- Returns:
- Series EWMA VaR.
- """
- earlier, later = self.calc_range(
- months_offset=months_from_last,
- from_dt=from_date,
- to_dt=to_date,
- )
- time_factor = _calculate_time_factor(
- data=self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ].iloc[:, 0],
- earlier=earlier,
- later=later,
- periods_in_a_year_fixed=periods_in_a_year_fixed,
- )
-
- data = self.tsdf.loc[
- cast("Timestamp", earlier) : cast("Timestamp", later)
- ].copy()
-
- data.loc[:, (self.label, ValueType.RTRN)] = log(
- data.loc[:, self.tsdf.columns.to_numpy()[0]],
- ).diff()
-
- rawdata = [
- data[(self.label, ValueType.RTRN)]
- .iloc[1:day_chunk]
- .std(ddof=dlta_degr_freedms)
- * sqrt(time_factor),
- ]
-
- for item in data[(self.label, ValueType.RTRN)].iloc[1:]:
- prev = rawdata[-1]
- rawdata.append(
- sqrt(
- square(item) * time_factor * (1 - lmbda) + square(prev) * lmbda,
- ),
- )
-
- return Series(
- data=array(rawdata) * norm.ppf(1 - level),
- index=data.index,
- name=(self.label, ValueType.EWMA_VAR),
- dtype="float64",
- )
-
-
-
-[docs]
- def running_adjustment(
- self: Self,
- adjustment: float,
- days_in_year: int = 365,
- ) -> Self:
- """Add or subtract a fee from the timeseries return.
-
- Args:
- adjustment: Fee to add or subtract.
- days_in_year: The calculation divisor and assumed number of days in a
- calendar year. Defaults to 365.
-
- Returns:
- An OpenTimeSeries object.
- """
- if self.valuetype == ValueType.RTRN:
- ra_df = self.tsdf.copy()
- initial_value = 1.0
- returns_input = True
- else:
- initial_value = cast("float", self.tsdf.iloc[0, 0])
- ra_df = self.tsdf.ffill().pct_change()
- returns_input = False
- ra_df = ra_df.dropna()
-
- dates_index = DatetimeIndex(ra_df.index)
- dates_list = [self.first_idx] + [d.date() for d in dates_index]
-
- dates_np = array(
- [dt.datetime.combine(d, dt.time()) for d in dates_list],
- dtype="datetime64[D]",
- )
- date_diffs = cast(
- "NDArray[float64]",
- diff(dates_np).astype("timedelta64[D]").astype(float64),
- )
-
- returns_array = cast(
- "NDArray[float64]",
- ra_df.iloc[:, 0].to_numpy(),
- )
-
- adjustment_factors = (
- 1.0 + returns_array + adjustment * date_diffs / days_in_year
- )
-
- values_array = cumprod(insert(adjustment_factors, 0, initial_value))
- values = list(values_array)
-
- self.tsdf = DataFrame(data=values, index=dates_list)
- self.valuetype = ValueType.PRICE
- self.tsdf.columns = MultiIndex.from_arrays(
- [
- [self.label],
- [self.valuetype],
- ],
- )
- self.tsdf.index = Index(DatetimeIndex(self.tsdf.index).date)
- if returns_input:
- self.value_to_ret()
- return self
-
-
-
-[docs]
- def set_new_label(
- self: Self,
- lvl_zero: str | None = None,
- lvl_one: ValueType | None = None,
- *,
- delete_lvl_one: bool = False,
- ) -> Self:
- """Set the column labels of the .tsdf Pandas Dataframe.
-
- Args:
- lvl_zero: New level zero label. Optional.
- lvl_one: New level one label. Optional.
- delete_lvl_one: If True the level one label is deleted. Defaults to False.
-
- Returns:
- An OpenTimeSeries object.
- """
- if lvl_zero is None and lvl_one is None:
- self.tsdf.columns = MultiIndex.from_arrays(
- [[self.label], [self.valuetype]],
- )
- elif lvl_zero is not None and lvl_one is None:
- self.tsdf.columns = MultiIndex.from_arrays([[lvl_zero], [self.valuetype]])
- self.label = lvl_zero
- elif lvl_zero is None and lvl_one is not None:
- self.tsdf.columns = MultiIndex.from_arrays([[self.label], [lvl_one]])
- self.valuetype = lvl_one
- else:
- self.tsdf.columns = MultiIndex.from_arrays([[lvl_zero], [lvl_one]])
- self.label, self.valuetype = lvl_zero, cast("ValueType", lvl_one)
- if delete_lvl_one:
- self.tsdf.columns = self.tsdf.columns.get_level_values(0)
- return self
-
-
- def _returns_series(self: Self, *, squared: bool = False) -> Series[float]:
- """Return demeaned return series for autocorrelation analysis."""
- data: Series[float] = self.tsdf.iloc[:, 0]
- return _demeaned_returns_for_autocorr(
- series=data, valuetype=self.valuetype, squared=squared
- )
-
-
-[docs]
- def acf(
- self: Self,
- lags: int | list[int],
- *,
- squared: bool = False,
- ) -> Series[float]:
- """Calculate autocorrelation function for specified lags.
-
- Args:
- lags: If int, compute ACF from lag 0 to this value (inclusive).
- If list, compute ACF at lag 0 plus each lag in the list.
- squared: If True, compute ACF of squared returns. Defaults to False.
-
- Returns:
- Series of autocorrelations indexed by lag.
- """
- rets = self._returns_series(squared=squared)
- if isinstance(lags, int):
- lag_list = list(range(lags + 1))
- else:
- lag_list = sorted({0} | set(lags))
- values: list[float] = []
- for lag in lag_list:
- if lag == 0:
- values.append(1.0)
- else:
- values.append(float(rets.autocorr(lag=lag)))
- return Series(
- data=values,
- index=lag_list,
- name="ACF",
- dtype="float64",
- )
-
-
-
-[docs]
- def partial_autocorr(self: Self, lag: int = 1, *, squared: bool = False) -> float:
- """Calculate partial autocorrelation at a given lag.
-
- Args:
- lag: The lag at which to compute partial autocorrelation. Defaults to 1.
- squared: If True, compute partial autocorrelation of squared returns.
- Defaults to False.
-
- Returns:
- Partial autocorrelation at the specified lag.
- """
- pacf_series = self.pacf(lags=lag, squared=squared)
- return float(pacf_series.loc[lag])
-
-
-
-[docs]
- def pacf(
- self: Self,
- lags: int | list[int],
- *,
- squared: bool = False,
- ) -> Series[float]:
- """Calculate partial autocorrelation function for specified lags.
-
- Args:
- lags: If int, compute PACF from lag 0 to this value (inclusive).
- If list, compute PACF at lag 0 plus each lag in the list.
- squared: If True, compute PACF of squared returns. Defaults to False.
-
- Returns:
- Series of partial autocorrelations indexed by lag.
- """
- if isinstance(lags, int):
- lag_list = list(range(lags + 1))
- else:
- lag_list = sorted({0} | set(lags))
- max_lag = max(lag_list) if lag_list else 0
- acf_vals = self.acf(lags=max_lag, squared=squared)
- acf_arr = array([acf_vals.loc[k] for k in range(max_lag + 1)])
- pacf_values: list[float] = [1.0]
- phi: list[list[float]] = []
- for k in range(1, max_lag + 1):
- if k == 1:
- phi_kk = acf_arr[1]
- else:
- numer = acf_arr[k]
- denom = 1.0
- for j in range(k - 1):
- numer -= phi[k - 2][j] * acf_arr[k - 1 - j]
- denom -= phi[k - 2][j] * acf_arr[j + 1]
- phi_kk = numer / denom
- phi_row = [0.0] * k
- for j in range(k - 1):
- phi_row[j] = phi[k - 2][j] - phi_kk * phi[k - 2][k - 2 - j]
- phi_row[k - 1] = phi_kk
- phi.append(phi_row)
- pacf_values.append(phi_kk)
- result = {lag: pacf_values[lag] for lag in lag_list}
- return Series(
- data=[result[lag] for lag in lag_list],
- index=lag_list,
- name="PACF",
- dtype="float64",
- )
-
-
-
-[docs]
- def ljung_box(
- self: Self,
- lags: int | list[int],
- *,
- squared: bool = False,
- ) -> tuple[float, float, list[int]]:
- """Compute Ljung-Box test for autocorrelation.
-
- Args:
- lags: If int, use lags 1 through this value. If list, use the given
- lags (lag 0 excluded from test).
- squared: If True, test autocorrelation of squared returns.
- Defaults to False.
-
- Returns:
- Tuple of (statistic, pvalue, lags) where statistic is the Ljung-Box
- Q statistic, pvalue is the chi-squared p-value, and lags is the
- list of lags used.
- """
- rets = self._returns_series(squared=squared)
- n = len(rets)
- if isinstance(lags, int):
- lag_list = list(range(1, lags + 1))
- else:
- lag_list = sorted({k for k in lags if k > 0})
- if not lag_list:
- return 0.0, 1.0, []
- r_k_sq_sum = 0.0
- for k in lag_list:
- if k < n:
- r_k = float(rets.autocorr(lag=k))
- r_k_sq_sum += r_k**2 / (n - k)
- q_stat = n * (n + 2) * r_k_sq_sum
- df = len(lag_list)
- pval = float(1.0 - chi2.cdf(q_stat, df))
- return q_stat, pval, lag_list
-
-
-
-
-
-[docs]
-def timeseries_chain(
- front: TypeOpenTimeSeries,
- back: TypeOpenTimeSeries,
- old_fee: float = 0.0,
-) -> TypeOpenTimeSeries:
- """Chain two timeseries together.
-
- The function assumes that the two series have at least one date in common.
-
- Args:
- front: Earlier series to chain with.
- back: Later series to chain with.
- old_fee: Fee to apply to earlier series. Defaults to 0.0.
-
- Returns:
- An OpenTimeSeries object or a subclass thereof.
- """
- old = front.from_deepcopy()
- old.running_adjustment(old_fee)
- new = back.from_deepcopy()
- idx = 0
- first = new.tsdf.index[idx]
-
- if old.last_idx < first:
- msg = "Timeseries dates must overlap to allow them to be chained."
- raise DateAlignmentError(msg)
-
- while first not in old.tsdf.index:
- idx += 1
- first = new.tsdf.index[idx]
- if first > old.tsdf.index[-1]:
- msg = "Failed to find a matching date between series"
- raise DateAlignmentError(msg)
-
- dates: list[str] = [x.strftime("%Y-%m-%d") for x in old.tsdf.index if x < first]
-
- old_values = Series(old.tsdf.iloc[: len(dates), 0])
- old_values = old_values.mul(
- Series(new.tsdf.iloc[:, 0]).loc[first]
- / Series(old.tsdf.iloc[:, 0]).loc[first],
- )
- values = append(old_values, new.tsdf.iloc[:, 0])
-
- dates.extend([x.strftime("%Y-%m-%d") for x in new.tsdf.index])
-
- return back.__class__(
- timeseries_id=new.timeseries_id,
- instrument_id=new.instrument_id,
- currency=new.currency,
- dates=dates,
- name=new.name,
- label=new.name,
- valuetype=new.valuetype,
- values=list(values),
- local_ccy=new.local_ccy,
- tsdf=DataFrame(
- data=values,
- index=[d.date() for d in DatetimeIndex(dates)],
- columns=[[new.label], [new.valuetype]],
- dtype="float64",
- ),
- )
-
-
-
-def _check_if_none(item: object) -> bool:
- """Check if a variable is None or equivalent.
-
- Args:
- item: Variable to be checked.
-
- Returns:
- Answer to whether the variable is None or equivalent.
- """
- if item is None:
- return True
-
- try:
- return cast("bool", isnan(cast("float", item)))
- except (TypeError, ValueError):
- return len(str(item)) == 0
-
-"""The ReturnSimulation class."""
-
-from __future__ import annotations
-
-from functools import cached_property
-from typing import TYPE_CHECKING, Self, TypedDict, cast
-
-try:
- from typing import Unpack
-except ImportError: # pragma: no cover
- from typing_extensions import Unpack
-
-if TYPE_CHECKING:
- import datetime as dt # pragma: no cover
-
-from numpy import multiply, sqrt
-from numpy.random import PCG64, Generator, SeedSequence
-from pandas import (
- DataFrame,
- Index,
- MultiIndex,
- concat,
-)
-from pydantic import (
- BaseModel,
- ConfigDict,
- NonNegativeFloat,
- PositiveFloat,
- PositiveInt,
-)
-
-from .datefixer import generate_calendar_date_range
-from .owntypes import (
- CountriesType,
- DaysInYearType,
- ValueType,
-)
-
-__all__ = ["ReturnSimulation"]
-
-
-class _JumpParams(TypedDict, total=False):
- """TypedDict for jump diffusion parameters."""
-
- jumps_lamda: NonNegativeFloat
- jumps_sigma: NonNegativeFloat
- jumps_mu: float
-
-
-def _validate_ar1_coef(ar1_coef: float) -> None:
- """Validate ar1_coef is in (-1, 1) for stationarity."""
- if not -1.0 < ar1_coef < 1.0:
- msg = f"ar1_coef must be in (-1, 1) for stationarity, got {ar1_coef}"
- raise ValueError(msg)
-
-
-def _apply_ar1_filter(returns: DataFrame, ar1_coef: float) -> DataFrame:
- """Apply AR(1) filter to returns to introduce lag-1 autocorrelation.
-
- r_t = ar1_coef * r_{t-1} + sqrt(1 - ar1_coef**2) * innovation_t
- Preserves mean and variance of the base process.
-
- Args:
- returns: DataFrame of shape (number_of_sims, trading_days).
- ar1_coef: Lag-1 autocorrelation coefficient in (-1, 1).
-
- Returns:
- Filtered returns.
- """
- if ar1_coef == 0.0:
- return returns
- arr = returns.to_numpy(copy=True)
- scale = sqrt(1.0 - ar1_coef * ar1_coef)
- for t in range(1, arr.shape[1]):
- arr[:, t] = ar1_coef * arr[:, t - 1] + scale * arr[:, t]
- return DataFrame(data=arr, dtype="float64")
-
-
-def _random_generator(seed: int | None) -> Generator:
- """Make a Numpy Random Generator object.
-
- Args:
- seed: Random seed.
-
- Returns:
- Numpy random process generator.
- """
- ss = SeedSequence(entropy=seed)
- bg = PCG64(seed=cast("int | None", ss))
- return Generator(bit_generator=bg)
-
-
-def _create_base_simulation(
- cls: type[ReturnSimulation],
- returns: DataFrame,
- number_of_sims: PositiveInt,
- trading_days: PositiveInt,
- trading_days_in_year: DaysInYearType,
- mean_annual_return: float,
- mean_annual_vol: PositiveFloat,
- seed: int | None = None,
- **kwargs: Unpack[_JumpParams],
-) -> ReturnSimulation:
- """Common logic for creating simulations.
-
- Args:
- cls: The ReturnSimulation class.
- returns: The calculated returns data.
- number_of_sims: Number of simulations to generate.
- trading_days: Number of trading days to simulate.
- trading_days_in_year: Number of trading days used to annualize.
- mean_annual_return: Mean annual return.
- mean_annual_vol: Mean annual volatility.
- seed: Seed for random process initiation.
- **kwargs: Additional keyword arguments for jump parameters.
-
- Returns:
- A ReturnSimulation instance.
- """
- return cls(
- number_of_sims=number_of_sims,
- trading_days=trading_days,
- trading_days_in_year=trading_days_in_year,
- mean_annual_return=mean_annual_return,
- mean_annual_vol=mean_annual_vol,
- dframe=returns,
- seed=seed,
- **kwargs,
- )
-
-
-
-[docs]
-class ReturnSimulation(BaseModel):
- """The class ReturnSimulation allows for simulating financial timeseries.
-
- Args:
- number_of_sims: Number of simulations to generate.
- trading_days: Total number of days to simulate.
- trading_days_in_year: Number of trading days used to annualize.
- mean_annual_return: Mean annual return of the distribution.
- mean_annual_vol: Mean annual standard deviation of the distribution.
- dframe: Pandas DataFrame object holding the resulting values.
- jumps_lamda: This is the probability of a jump happening at each point in time.
- Defaults to 0.0.
- jumps_sigma: This is the volatility of the jump size. Defaults to 0.0.
- jumps_mu: This is the average jump size. Defaults to 0.0.
- seed: Seed for random process initiation.
-
- """
-
- number_of_sims: PositiveInt
- trading_days: PositiveInt
- trading_days_in_year: DaysInYearType
- mean_annual_return: float
- mean_annual_vol: PositiveFloat
- dframe: DataFrame
- jumps_lamda: NonNegativeFloat = 0.0
- jumps_sigma: NonNegativeFloat = 0.0
- jumps_mu: float = 0.0
- seed: int | None = None
-
- model_config = ConfigDict(
- arbitrary_types_allowed=True,
- validate_assignment=True,
- revalidate_instances="always",
- )
-
-
-[docs]
- @cached_property
- def results(self: Self) -> DataFrame:
- """Simulation data.
-
- Returns:
- Simulation data.
- """
- return self.dframe.add(1.0).cumprod(axis="columns").T
-
-
- @property
- def realized_mean_return(self: Self) -> float:
- """Annualized arithmetic mean of returns.
-
- Returns:
- Annualized arithmetic mean of returns.
- """
- return cast(
- "float",
- (
- self.results.ffill().pct_change().mean() * self.trading_days_in_year
- ).iloc[0],
- )
-
- @property
- def realized_vol(self: Self) -> float:
- """Annualized volatility.
-
- Returns:
- Annualized volatility.
- """
- return cast(
- "float",
- (
- self.results.ffill().pct_change().std()
- * sqrt(self.trading_days_in_year)
- ).iloc[0],
- )
-
-
-[docs]
- @classmethod
- def from_normal(
- cls: type[ReturnSimulation],
- number_of_sims: PositiveInt,
- mean_annual_return: float,
- mean_annual_vol: PositiveFloat,
- trading_days: PositiveInt,
- trading_days_in_year: DaysInYearType = 252,
- seed: int | None = None,
- randomizer: Generator | None = None,
- ar1_coef: float = 0.0,
- ) -> ReturnSimulation:
- """Create a Normal distribution simulation.
-
- Args:
- number_of_sims: Number of simulations to generate.
- trading_days: Number of trading days to simulate.
- mean_annual_return: Mean return.
- mean_annual_vol: Mean standard deviation.
- trading_days_in_year: Number of trading days used to annualize.
- Defaults to 252.
- seed: Seed for random process initiation.
- randomizer: Random process generator.
- ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
- autocorrelation. Defaults to 0.0 (i.i.d. returns).
-
- Returns:
- Normal distribution simulation.
- """
- _validate_ar1_coef(ar1_coef)
- if not randomizer:
- randomizer = _random_generator(seed=seed)
-
- returns_df = DataFrame(
- data=randomizer.normal(
- loc=mean_annual_return / trading_days_in_year,
- scale=mean_annual_vol / sqrt(trading_days_in_year),
- size=(number_of_sims, trading_days),
- ),
- dtype="float64",
- )
- returns = _apply_ar1_filter(returns_df, ar1_coef)
-
- return _create_base_simulation(
- cls=cls,
- returns=returns,
- number_of_sims=number_of_sims,
- trading_days=trading_days,
- trading_days_in_year=trading_days_in_year,
- mean_annual_return=mean_annual_return,
- mean_annual_vol=mean_annual_vol,
- seed=seed,
- )
-
-
-
-[docs]
- @classmethod
- def from_lognormal(
- cls: type[ReturnSimulation],
- number_of_sims: PositiveInt,
- mean_annual_return: float,
- mean_annual_vol: PositiveFloat,
- trading_days: PositiveInt,
- trading_days_in_year: DaysInYearType = 252,
- seed: int | None = None,
- randomizer: Generator | None = None,
- ar1_coef: float = 0.0,
- ) -> ReturnSimulation:
- """Create a Lognormal distribution simulation.
-
- Args:
- number_of_sims: Number of simulations to generate.
- trading_days: Number of trading days to simulate.
- mean_annual_return: Mean return.
- mean_annual_vol: Mean standard deviation.
- trading_days_in_year: Number of trading days used to annualize.
- Defaults to 252.
- seed: Seed for random process initiation.
- randomizer: Random process generator.
- ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
- autocorrelation. Defaults to 0.0 (i.i.d. returns).
-
- Returns:
- Lognormal distribution simulation.
- """
- _validate_ar1_coef(ar1_coef)
- if not randomizer:
- randomizer = _random_generator(seed=seed)
-
- returns_df = DataFrame(
- data=(
- randomizer.lognormal(
- mean=mean_annual_return / trading_days_in_year,
- sigma=mean_annual_vol / sqrt(trading_days_in_year),
- size=(number_of_sims, trading_days),
- )
- - 1
- ),
- dtype="float64",
- )
- returns = _apply_ar1_filter(returns_df, ar1_coef)
-
- return _create_base_simulation(
- cls=cls,
- returns=returns,
- number_of_sims=number_of_sims,
- trading_days=trading_days,
- trading_days_in_year=trading_days_in_year,
- mean_annual_return=mean_annual_return,
- mean_annual_vol=mean_annual_vol,
- seed=seed,
- )
-
-
-
-[docs]
- @classmethod
- def from_gbm(
- cls: type[ReturnSimulation],
- number_of_sims: PositiveInt,
- mean_annual_return: float,
- mean_annual_vol: PositiveFloat,
- trading_days: PositiveInt,
- trading_days_in_year: DaysInYearType = 252,
- seed: int | None = None,
- randomizer: Generator | None = None,
- ar1_coef: float = 0.0,
- ) -> ReturnSimulation:
- """Create a Geometric Brownian Motion simulation.
-
- Args:
- number_of_sims: Number of simulations to generate.
- trading_days: Number of trading days to simulate.
- mean_annual_return: Mean return.
- mean_annual_vol: Mean standard deviation.
- trading_days_in_year: Number of trading days used to annualize.
- Defaults to 252.
- seed: Seed for random process initiation.
- randomizer: Random process generator.
- ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
- autocorrelation. Defaults to 0.0 (i.i.d. returns).
-
- Returns:
- Geometric Brownian Motion simulation.
- """
- _validate_ar1_coef(ar1_coef)
- if not randomizer:
- randomizer = _random_generator(seed=seed)
-
- drift = (mean_annual_return - 0.5 * mean_annual_vol**2.0) * (
- 1.0 / trading_days_in_year
- )
-
- normal_mean = 0.0
- wiener = randomizer.normal(
- loc=normal_mean,
- scale=sqrt(1.0 / trading_days_in_year) * mean_annual_vol,
- size=(number_of_sims, trading_days),
- )
-
- returns_df = DataFrame(data=drift + wiener, dtype="float64")
- returns = _apply_ar1_filter(returns_df, ar1_coef)
-
- return _create_base_simulation(
- cls=cls,
- returns=returns,
- number_of_sims=number_of_sims,
- trading_days=trading_days,
- trading_days_in_year=trading_days_in_year,
- mean_annual_return=mean_annual_return,
- mean_annual_vol=mean_annual_vol,
- seed=seed,
- )
-
-
-
-[docs]
- @classmethod
- def from_merton_jump_gbm(
- cls: type[ReturnSimulation],
- number_of_sims: PositiveInt,
- trading_days: PositiveInt,
- mean_annual_return: float,
- mean_annual_vol: PositiveFloat,
- jumps_lamda: NonNegativeFloat,
- jumps_sigma: NonNegativeFloat = 0.0,
- jumps_mu: float = 0.0,
- trading_days_in_year: DaysInYearType = 252,
- seed: int | None = None,
- randomizer: Generator | None = None,
- ar1_coef: float = 0.0,
- ) -> ReturnSimulation:
- """Create a Merton Jump-Diffusion model simulation.
-
- Args:
- number_of_sims: Number of simulations to generate.
- trading_days: Number of trading days to simulate.
- mean_annual_return: Mean return.
- mean_annual_vol: Mean standard deviation.
- jumps_lamda: This is the probability of a jump happening at each point
- in time.
- jumps_sigma: This is the volatility of the jump size. Defaults to 0.0.
- jumps_mu: This is the average jump size. Defaults to 0.0.
- trading_days_in_year: Number of trading days used to annualize.
- Defaults to 252.
- seed: Seed for random process initiation.
- randomizer: Random process generator.
- ar1_coef: Lag-1 autoregressive coefficient in (-1, 1) to induce
- autocorrelation. Defaults to 0.0 (i.i.d. returns).
-
- Returns:
- Merton Jump-Diffusion model simulation.
- """
- _validate_ar1_coef(ar1_coef)
- if not randomizer:
- randomizer = _random_generator(seed=seed)
-
- normal_mean = 0.0
- wiener = randomizer.normal(
- loc=normal_mean,
- scale=sqrt(1.0 / trading_days_in_year) * mean_annual_vol,
- size=(number_of_sims, trading_days),
- )
-
- poisson_jumps = multiply(
- randomizer.poisson(
- lam=jumps_lamda * (1.0 / trading_days_in_year),
- size=(number_of_sims, trading_days),
- ),
- randomizer.normal(
- loc=jumps_mu,
- scale=jumps_sigma,
- size=(number_of_sims, trading_days),
- ),
- )
-
- drift = (
- mean_annual_return
- - 0.5 * mean_annual_vol**2.0
- - jumps_lamda * (jumps_mu + jumps_sigma**2.0)
- ) * (1.0 / trading_days_in_year)
-
- raw_returns = poisson_jumps + drift + wiener
- raw_returns[:, 0] = 0.0
-
- returns_df = DataFrame(data=raw_returns, dtype="float64")
- returns = _apply_ar1_filter(returns_df, ar1_coef)
-
- return _create_base_simulation(
- cls=cls,
- returns=returns,
- number_of_sims=number_of_sims,
- trading_days=trading_days,
- trading_days_in_year=trading_days_in_year,
- mean_annual_return=mean_annual_return,
- mean_annual_vol=mean_annual_vol,
- seed=seed,
- jumps_lamda=jumps_lamda,
- jumps_sigma=jumps_sigma,
- jumps_mu=jumps_mu,
- )
-
-
-
-[docs]
- def to_dataframe(
- self: Self,
- name: str,
- start: dt.date | None = None,
- end: dt.date | None = None,
- countries: CountriesType = "SE",
- markets: list[str] | str | None = None,
- ) -> DataFrame:
- """Create a pandas.DataFrame from simulation(s).
-
- Args:
- name: Name label of the serie(s).
- start: Date when the simulation starts.
- end: Date when the simulation ends.
- countries: (List of) country code(s) according to ISO 3166-1 alpha-2.
- Defaults to "SE".
- markets: (List of) markets code(s) supported by exchange_calendars.
-
- Returns:
- The simulation(s) data.
- """
- d_range = generate_calendar_date_range(
- trading_days=self.trading_days,
- start=start,
- end=end,
- countries=countries,
- markets=markets,
- )
-
- if self.number_of_sims == 1:
- sdf = self.dframe.iloc[0].T.to_frame()
- sdf.index = Index(d_range)
- sdf.columns = MultiIndex.from_arrays(
- [
- [name],
- [ValueType.RTRN],
- ],
- )
- return sdf
-
- df_list = [
- DataFrame(
- data=self.dframe.iloc[item].values,
- index=Index(d_range),
- columns=MultiIndex.from_arrays(
- [
- [f"{name}_{item}"],
- [ValueType.RTRN],
- ],
- ),
- )
- for item in range(self.number_of_sims)
- ]
- return concat(df_list, axis="columns", sort=True)
-
-
-' - + '' - + _("Hide Search Matches") - + "
", - ), - ); - }, - - /** - * helper function to hide the search marks again - */ - hideSearchWords: () => { - document - .querySelectorAll("#searchbox .highlight-link") - .forEach((el) => el.remove()); - document - .querySelectorAll("span.highlighted") - .forEach((el) => el.classList.remove("highlighted")); - localStorage.removeItem("sphinx_highlight_terms"); - }, - - initEscapeListener: () => { - // only install a listener if it is really needed - if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; - - document.addEventListener("keydown", (event) => { - // bail for input elements - if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) - return; - // bail with special keys - if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) - return; - if ( - DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS - && event.key === "Escape" - ) { - SphinxHighlight.hideSearchWords(); - event.preventDefault(); - } - }); - }, -}; - -_ready(() => { - /* Do not call highlightSearchWords() when we are on the search page. - * It will highlight words from the *previous* search query. - */ - if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); - SphinxHighlight.initEscapeListener(); -}); diff --git a/docs/build/html/api/datefixer.html b/docs/build/html/api/datefixer.html deleted file mode 100644 index 93262dd8..00000000 --- a/docs/build/html/api/datefixer.html +++ /dev/null @@ -1,307 +0,0 @@ - - - - - - - - -Parse different date formats into datetime.date.
-fixerdate (DateType) – The data item to parse.
-Parsed date.
-TypeError – If the provided fixerdate type is not supported.
dt.date
-Offset dates according to a given calendar.
-raw_date (DateType) – The date to offset from.
months_offset (int) – Number of months as integer. Defaults to 12.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
adjust (bool) – Determines if offset should adjust for business days. -Defaults to False.
following (bool) – Determines if days should be offset forward (following) or backward. -Defaults to True.
Offset date.
-dt.date
-Generate a list of business day calendar dates.
-trading_days (int) – Number of days to generate. Must be greater than zero.
start (dt.date | None) – Date when the range starts.
end (dt.date | None) – Date when the range ends.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
List of business day calendar dates.
-list[dt.date]
-Bump date backwards to find the previous business day.
-today (dt.date | None) – Manual input of the day from where the previous business day is found.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
The previous business day.
-dt.date
-Generate a business calendar.
-startyear (int) – First year in date range generated.
endyear (int) – Last year in date range generated.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
Generate a business calendar.
-CountriesNotStringNorListStrError – If countries is not a supported
- ISO 3166-1 alpha-2 string or a list of such strings.
busdaycalendar
-Bump date by business days.
-It first adjusts to a valid business day and then bumps with given -number of business days from there.
-ddate (dt.date) – A starting date that does not have to be a business day.
days (int) – The number of business days to offset from the business day -that is given. -If days is set as anything other than an integer its value is set to zero.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
The new offset business day.
-dt.date
-The datefixer module provides utilities for handling business days, holidays, and date calculations commonly needed in financial analysis.
-Bases: _CommonModel[Series]
OpenFrame objects hold OpenTimeSeries in the list constituents.
-The intended use is to allow comparisons across these timeseries.
-OpenFrame objects hold OpenTimeSeries in the list constituents.
-The intended use is to allow comparisons across these timeseries.
- -Create copy of the OpenFrame object.
- -Merge index of Pandas Dataframes of the constituent OpenTimeSeries.
- -Calculate chosen timeseries properties.
-properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'autocorr', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown', 'max_drawdown_date', 'max_drawdown_cal_year', 'first_indices', 'last_indices', 'lengths_of_items', 'span_of_days_all']] | None) – The properties to calculate. Defaults to calculating all -available. Optional.
self (Self)
Properties of the constituent OpenTimeSeries.
-Number of observations of all constituents.
-Number of observations of all constituents.
-Number of constituents.
-Number of constituents.
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-The first dates in the timeseries of all constituents.
-The first dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-Number of days from the first date to the last for all items in the frame.
-Number of days from the first date to the last for all -items in the frame.
-Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert series of returns into cumulative series of values.
- -Resample the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenFrame object.
-Exponentially Weighted Moving Average Volatilities and Correlation.
-Exponentially Weighted Moving Average (EWMA) for Volatilities and -Correlation.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
first_column (int) – Column of first timeseries. Defaults to 0.
second_column (int) – Column of second timeseries. Defaults to 1.
corr_scale (float) – Correlation scale factor. Defaults to 2.0.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series volatilities and correlation.
-DataFrame
-Correlation matrix.
-This property returns the correlation matrix of the time series -in the frame.
-Correlation matrix of the time series in the frame.
-To add an OpenTimeSeries object.
- -To delete an OpenTimeSeries object.
- -Truncate DataFrame such that all timeseries have the same time span.
-start_cut (dt.date | None) – New first date. Optional.
end_cut (dt.date | None) – New last date. Optional.
where (LiteralTrunc) – Determines where dataframe is truncated also when start_cut -or end_cut is None. Defaults to both.
self (Self)
An OpenFrame object.
-Self
-Calculate cumulative relative return between two series.
-None
-Tracking Error.
-Calculates Tracking Error which is the standard deviation of the -difference between the fund and its index returns.
-Reference: https://www.investopedia.com/terms/t/trackingerror.asp.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Tracking Errors.
-Series[float]
-Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided -by the Tracking Error. And the Tracking Error is the standard deviation of -the difference between the fund and its index returns. -The ratio is calculated using the annualized arithmetic mean of returns.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Information Ratios.
-Series[float]
-Capture Ratio.
-The Up (Down) Capture Ratio is calculated by dividing the CAGR -of the asset during periods that the benchmark returns are positive (negative) -by the CAGR of the benchmark during the same periods. -CaptureRatio.BOTH is the Up ratio divided by the Down ratio. -Source: ‘Capture Ratios: A Popular Method of Measuring Portfolio Performance -in Practice’, Don R. Cox and Delbert C. Goff, Journal of Economics and -Finance Education (Vol 2 Winter 2013).
-Reference: https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf.
-ratio (LiteralCaptureRatio) – The ratio to calculate.
base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Capture Ratios.
-Series[float]
-Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-Beta as Co-variance of x & y divided by Variance of x.
-Ordinary Least Squares fit.
-Performs a linear regression and adds a new column with a fitted line -using Ordinary Least Squares fit.
-y_column (tuple[str, ValueType] | int) – The column level values of the dependent variable y.
x_column (tuple[str, ValueType] | int) – The column level values of the exogenous variable x.
fitted_series (bool) – If True the fit is added as a new column in the .tsdf -Pandas.DataFrame. Defaults to True.
self (Self)
A dictionary with the coefficient, intercept and rsquared outputs.
-Jensen’s alpha.
-The Jensen’s measure, or Jensen’s alpha, is a risk-adjusted performance -measure that represents the average return on a portfolio or investment, -above or below that predicted by the capital asset pricing model (CAPM), -given the portfolio’s or investment’s beta and the average market return. -This metric is also commonly referred to as simply alpha.
-Reference: https://www.investopedia.com/terms/j/jensensmeasure.asp.
-asset (tuple[str, ValueType] | int) – The column of the asset.
market (tuple[str, ValueType] | int) – The column of the market against which Jensen’s alpha is measured.
riskfree_rate (float) – The return of the zero volatility riskfree asset. -Defaults to 0.0.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Jensen’s alpha.
-Calculate a basket timeseries based on the supplied weights.
- -Calculate rolling Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided by -the Tracking Error. And the Tracking Error is the standard deviation of the -difference between the fund and its index returns.
-long_column (int) – Column of timeseries that is the numerator in the ratio. -Defaults to 0.
short_column (int) – Column of timeseries that is the denominator in the ratio. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
periods_in_a_year_fixed (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])] | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Rolling Information Ratios.
-Calculate rolling Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-asset_column (int) – Column of timeseries that is the asset. Defaults to 0.
market_column (int) – Column of timeseries that is the market. Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Rolling Betas.
-Calculate rolling Correlation.
-Calculates correlation between two series. The period with -at least the given number of observations is the first period calculated.
-first_column (int) – The position as integer of the first timeseries to compare. -Defaults to 0.
second_column (int) – The position as integer of the second timeseries to compare. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
self (Self)
Rolling Correlations.
-Perform a multi-factor linear regression.
-This function treats one specified column in the DataFrame as the dependent -variable (y) and uses all remaining columns as independent variables (X). -It utilizes a scikit-learn LinearRegression model and returns a DataFrame -with summary output and an OpenTimeSeries of predicted values.
-A DataFrame with the R-squared, the intercept and the regression -coefficients
An OpenTimeSeries of predicted values
A tuple containing
-KeyError – If the column tuple is not found in the OpenFrame.tsdf.columns.
ValueError – If not all series are returnseries (ValueType.RTRN).
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Create a rebalanced portfolio from the OpenFrame constituents.
-name (str) – Name of the portfolio.
items (list[str] | None) – List of items to include in the portfolio. If None, uses all items. -Optional.
bal_weights (list[float] | None) – List of weights for rebalancing. If None, uses frame weights. -Optional.
frequency (int) – Rebalancing frequency. Defaults to 1.
cash_index (OpenTimeSeries | None) – Cash index series for cash component. Optional.
equal_weights (bool) – If True, use equal weights for all items. Defaults to False.
drop_extras (bool) – If True, only return TWR series; if False, return all details. -Defaults to True.
self (Self)
OpenFrame containing the rebalanced portfolio.
-OpenFrame
-The OpenFrame class manages collections of OpenTimeSeries objects and provides functionality for:
-Multi-asset analysis and comparison
Portfolio construction and optimization
Correlation and regression analysis
Risk attribution and factor analysis
Batch processing of multiple time series
Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-Number of constituents.
-Number of constituents.
-The first dates in the timeseries of all constituents.
-The first dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-Number of observations of all constituents.
-Number of observations of all constituents.
-Number of days from the first date to the last for all items in the frame.
-Number of days from the first date to the last for all -items in the frame.
-The first date in the timeseries.
-The first date in the timeseries.
-The last date in the timeseries.
-The last date in the timeseries.
-Number of observations.
-Number of observations.
-Number of days from the first date to the last.
-Number of days from the first date to the last.
-Date when the maximum drawdown occurred.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Date when the maximum drawdown occurred
-The average number of observations per year.
-The average number of observations per year.
-Length of series in years assuming 365.25 days per year.
-Length of the timeseries in years assuming 365.25 days per year.
-self (Self)
properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'autocorr', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown', 'max_drawdown_date', 'max_drawdown_cal_year', 'first_indices', 'last_indices', 'lengths_of_items', 'span_of_days_all']] | None)
Annualized arithmetic mean of returns.
-Reference: https://www.investopedia.com/terms/a/arithmeticmean.asp.
-Annualized arithmetic mean of returns. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Compounded Annual Growth Rate (CAGR).
-Reference: https://www.investopedia.com/terms/c/cagr.asp.
-Compounded Annual Growth Rate (CAGR). -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Simple return.
-Simple return. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Annualized volatility.
-Based on Pandas .std() which is the equivalent of stdev.s([…]) in MS Excel.
-Reference: https://www.investopedia.com/terms/v/volatility.asp.
-Annualized volatility. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside Deviation.
-Standard deviation of returns that are below a Minimum Accepted Return -of zero. It is used to calculate the Sortino Ratio.
-Reference: https://www.investopedia.com/terms/d/downside-deviation.asp.
-Downside deviation. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Ratio of annualized arithmetic mean of returns and annualized volatility.
-Ratio of the annualized arithmetic mean of returns and annualized -volatility. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Sortino ratio.
-Reference: https://www.investopedia.com/terms/s/sortinoratio.asp.
-Sortino ratio calculated as the annualized arithmetic mean of returns -/ downside deviation. The ratio implies that the riskfree asset has zero -volatility, and a minimum acceptable return of zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Kappa-3 ratio.
-The Kappa-3 ratio is a generalized downside-risk ratio defined as -annualized arithmetic return divided by the cubic-root of the -lower partial moment of order 3 (with respect to a minimum acceptable -return, MAR). It penalizes larger downside outcomes more heavily than -the Sortino ratio (which uses order 2).
-Kappa-3 ratio calculation with the riskfree rate and. -Minimum Acceptable Return (MAR) both set to zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Omega ratio.
-Reference: https://en.wikipedia.org/wiki/Omega_ratio.
-Omega ratio calculation. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside 95% Value At Risk (VaR).
-The equivalent of percentile.inc([…], 1-level) over returns in MS Excel. -https://www.investopedia.com/terms/v/var.asp.
-Downside 95% Value At Risk (VaR). -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside 95% Conditional Value At Risk “CVaR”.
-Reference: https://www.investopedia.com/terms/c/conditional_value_at_risk.asp.
-Downside 95% Conditional Value At Risk “CVaR”. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Most negative percentage change.
-Most negative percentage change. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Most negative month.
-Most negative month. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Maximum drawdown without any limit on date range.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Maximum drawdown without any limit on date range. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Maximum drawdown in a single calendar year.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Maximum drawdown in a single calendar year. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-The share of percentage changes that are greater than zero.
-The share of percentage changes that are greater than zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Implied annualized volatility from Downside 95% Value at Risk.
-Assumes that returns are normally distributed.
-Implied annualized volatility from the Downside 95% VaR using the -assumption that returns are normally distributed. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Autocorrelation at lag 1.
-Shorthand for autocorr_func(lag=1). Returns the lag-1 autocorrelation
-of demeaned returns. For price series, returns are computed via
-pct_change; for return series, raw values are used after demeaning.
Autocorrelation at lag 1. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Skew of the return distribution.
-Reference: https://www.investopedia.com/terms/s/skewness.asp.
-Skew of the return distribution. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Kurtosis of the return distribution.
-Reference: https://www.investopedia.com/terms/k/kurtosis.asp.
-Kurtosis of the return distribution. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Z-score.
-Reference: https://www.investopedia.com/terms/z/zscore.asp.
-Z-score as (last return - mean return) / standard deviation of returns. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Merge index of Pandas Dataframes of the constituent OpenTimeSeries.
- -Truncate DataFrame such that all timeseries have the same time span.
-start_cut (dt.date | None) – New first date. Optional.
end_cut (dt.date | None) – New last date. Optional.
where (LiteralTrunc) – Determines where dataframe is truncated also when start_cut -or end_cut is None. Defaults to both.
self (Self)
An OpenFrame object.
-Self
-To add an OpenTimeSeries object.
- -Calculate cumulative relative return between two series.
-None
-Calculate a basket timeseries based on the supplied weights.
- -Create a rebalanced portfolio from the OpenFrame constituents.
-name (str) – Name of the portfolio.
items (list[str] | None) – List of items to include in the portfolio. If None, uses all items. -Optional.
bal_weights (list[float] | None) – List of weights for rebalancing. If None, uses frame weights. -Optional.
frequency (int) – Rebalancing frequency. Defaults to 1.
cash_index (OpenTimeSeries | None) – Cash index series for cash component. Optional.
equal_weights (bool) – If True, use equal weights for all items. Defaults to False.
drop_extras (bool) – If True, only return TWR series; if False, return all details. -Defaults to True.
self (Self)
OpenFrame containing the rebalanced portfolio.
-OpenFrame
-Ordinary Least Squares fit.
-Performs a linear regression and adds a new column with a fitted line -using Ordinary Least Squares fit.
-y_column (tuple[str, ValueType] | int) – The column level values of the dependent variable y.
x_column (tuple[str, ValueType] | int) – The column level values of the exogenous variable x.
fitted_series (bool) – If True the fit is added as a new column in the .tsdf -Pandas.DataFrame. Defaults to True.
self (Self)
A dictionary with the coefficient, intercept and rsquared outputs.
-Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-Beta as Co-variance of x & y divided by Variance of x.
-Jensen’s alpha.
-The Jensen’s measure, or Jensen’s alpha, is a risk-adjusted performance -measure that represents the average return on a portfolio or investment, -above or below that predicted by the capital asset pricing model (CAPM), -given the portfolio’s or investment’s beta and the average market return. -This metric is also commonly referred to as simply alpha.
-Reference: https://www.investopedia.com/terms/j/jensensmeasure.asp.
-asset (tuple[str, ValueType] | int) – The column of the asset.
market (tuple[str, ValueType] | int) – The column of the market against which Jensen’s alpha is measured.
riskfree_rate (float) – The return of the zero volatility riskfree asset. -Defaults to 0.0.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Jensen’s alpha.
-Tracking Error.
-Calculates Tracking Error which is the standard deviation of the -difference between the fund and its index returns.
-Reference: https://www.investopedia.com/terms/t/trackingerror.asp.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Tracking Errors.
-Series[float]
-Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided -by the Tracking Error. And the Tracking Error is the standard deviation of -the difference between the fund and its index returns. -The ratio is calculated using the annualized arithmetic mean of returns.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Information Ratios.
-Series[float]
-Capture Ratio.
-The Up (Down) Capture Ratio is calculated by dividing the CAGR -of the asset during periods that the benchmark returns are positive (negative) -by the CAGR of the benchmark during the same periods. -CaptureRatio.BOTH is the Up ratio divided by the Down ratio. -Source: ‘Capture Ratios: A Popular Method of Measuring Portfolio Performance -in Practice’, Don R. Cox and Delbert C. Goff, Journal of Economics and -Finance Education (Vol 2 Winter 2013).
-Reference: https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf.
-ratio (LiteralCaptureRatio) – The ratio to calculate.
base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Capture Ratios.
-Series[float]
-Perform a multi-factor linear regression.
-This function treats one specified column in the DataFrame as the dependent -variable (y) and uses all remaining columns as independent variables (X). -It utilizes a scikit-learn LinearRegression model and returns a DataFrame -with summary output and an OpenTimeSeries of predicted values.
-A DataFrame with the R-squared, the intercept and the regression -coefficients
An OpenTimeSeries of predicted values
A tuple containing
-KeyError – If the column tuple is not found in the OpenFrame.tsdf.columns.
ValueError – If not all series are returnseries (ValueType.RTRN).
Calculate rolling Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided by -the Tracking Error. And the Tracking Error is the standard deviation of the -difference between the fund and its index returns.
-long_column (int) – Column of timeseries that is the numerator in the ratio. -Defaults to 0.
short_column (int) – Column of timeseries that is the denominator in the ratio. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
periods_in_a_year_fixed (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])] | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Rolling Information Ratios.
-Calculate rolling Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-asset_column (int) – Column of timeseries that is the asset. Defaults to 0.
market_column (int) – Column of timeseries that is the market. Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Rolling Betas.
-Calculate rolling Correlation.
-Calculates correlation between two series. The period with -at least the given number of observations is the first period calculated.
-first_column (int) – The position as integer of the first timeseries to compare. -Defaults to 0.
second_column (int) – The position as integer of the second timeseries to compare. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
self (Self)
Rolling Correlations.
-Calculate rolling returns.
- -Calculate rolling annualized volatilities.
-column (int) – Column position to calculate.
observations (int) – Number of observations in the overlapping window.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
dlta_degr_freedms (int) – Variance bias factor (0 or 1).
self (Self)
DataFrame with rolling annualized volatilities.
-DataFrame
-Calculate rolling annualized downside Value At Risk (VaR).
-DataFrame with rolling annualized downside VaR.
-DataFrame
-Calculate rolling annualized downside CVaR.
- -Correlation matrix.
-This property returns the correlation matrix of the time series -in the frame.
-Correlation matrix of the time series in the frame.
-Exponentially Weighted Moving Average Volatilities and Correlation.
-Exponentially Weighted Moving Average (EWMA) for Volatilities and -Correlation.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
first_column (int) – Column of first timeseries. Defaults to 0.
second_column (int) – Column of second timeseries. Defaults to 1.
corr_scale (float) – Correlation scale factor. Defaults to 2.0.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series volatilities and correlation.
-DataFrame
-Align the index of .tsdf with local calendar business days.
countries (CountriesType | None) – Country code(s) (ISO 3166-1 alpha-2).
markets (list[str] | str | None) – Market code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Missing holidays that should be added.
method (LiteralPandasReindexMethod) – Method for reindexing when aligning to business days.
self (Self)
The modified object.
-Self
-Resample the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenFrame object.
-Handle missing values in a value series.
-method (LiteralNanMethod) – Method used to handle NaN. Either "fill" (last known) or
-"drop".
self (Self)
The modified object.
-Self
-Handle missing values in a return series.
-method (LiteralNanMethod) – Method used to handle NaN. Either "fill" (zero) or
-"drop".
self (Self)
The modified object.
-Self
-Convert series of returns into cumulative series of values.
- -Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert value series to log-weighted series.
-Equivalent to LN(value[t] / value[t=0]) in Excel.
Convert timeseries into a drawdown series.
- -Calculate simple return for a specific calendar period.
- -Autocorrelation analysis: autocorr (property) and autocorr_func return
-lag-N autocorrelation per column. For ACF, PACF, and Ljung-Box tests, use the
-constituent OpenTimeSeries objects.
Calculate autocorrelation at a given lag.
-Computes the autocorrelation of demeaned returns at the specified lag.
-For price series (ValueType.PRICE), returns are derived via pct_change;
-for return series (ValueType.RTRN), raw values are demeaned. Use
-squared=True for squared-return autocorrelation (e.g. volatility
-clustering). Returns nan when the series has too few observations.
Autocorrelation at the specified lag. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Create a user-defined date range aligned to index.
-A tuple (earlier, later) representing the start and end date of the
-chosen date range aligned to existing index values.
DateAlignmentError – If the implied range is outside series bounds.
-Detect outliers using z-score analysis.
-Identifies data points where the absolute z-score exceeds the threshold. -For OpenTimeSeries, returns a Series with dates and outlier values. For -OpenFrame, returns a DataFrame with dates and outlier values for each -column.
-threshold (float) – Z-score threshold; values with |z| > threshold are
-outliers.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (date | None) – Specific from date.
to_date (date | None) – Specific to date.
self (Self)
Series of outliers. For OpenFrame: DataFrame of -outliers. Empty if none found.
-For OpenTimeSeries
-Annualized arithmetic mean of returns.
-Reference: https://www.investopedia.com/terms/a/arithmeticmean.asp.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Annualized arithmetic mean of returns. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Compounded Annual Growth Rate (CAGR).
-Reference: https://www.investopedia.com/terms/c/cagr.asp.
CAGR. Float for OpenTimeSeries, Series[float] for OpenFrame.
InitialValueZeroError – If initial value is zero or there are negative - values.
-SeriesOrFloat_co
-Calculate simple return.
-Simple return. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
InitialValueZeroError – If initial value is zero.
-SeriesOrFloat_co
-Annualized volatility.
-Based on pandas.Series.std() (Excel STDEV.S equivalent).
-Reference: https://www.investopedia.com/terms/v/volatility.asp.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Annualized volatility. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Lower partial moment and downside deviation (order=2).
-If order is 2 calculates standard deviation of returns below MAR=0.
-For general order p, returns (LPM_p)^(1/p).
min_accepted_return (float) – Annualized Minimum Accepted Return (MAR).
order (Literal[2, 3]) – Order of partial moment (2 or 3).
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Downside deviation if order is 2; otherwise rooted lower partial
-moment. Float for OpenTimeSeries, Series[float] for OpenFrame.
ValueError – If order is not 2 or 3.
SeriesOrFloat_co
-Ratio between arithmetic mean of returns and annualized volatility.
-If riskfree_rate provided, computes the Sharpe ratio as
-(arithmetic return - risk-free) / volatility. Assumes zero volatility
-for the risk-free asset. Reference:
-https://www.investopedia.com/terms/s/sharperatio.asp.
riskfree_rate (float) – Return of the zero volatility asset.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Ratio value. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Sortino ratio or Kappa-3 ratio.
-Sortino: (return - riskfree_rate) / downside deviation using arithmetic
-mean of returns. Kappa-3 when order=3 penalizes larger downside more
-than Sortino.
riskfree_rate (float) – Return of the zero volatility asset.
min_accepted_return (float) – Annualized Minimum Accepted Return (MAR).
order (Literal[2, 3]) – Order of partial moment (2 for Sortino, 3 for Kappa-3).
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Ratio value. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Omega Ratio.
-Compares returns above MAR to the total downside risk below MAR.
-Reference: https://en.wikipedia.org/wiki/Omega_ratio.
Omega ratio. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Downside Value At Risk (VaR).
-Equivalent to PERCENTILE.INC(returns, 1-level) in Excel. Reference:
-https://www.investopedia.com/terms/v/var.asp.
level (float) – The sought VaR level.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation used by DataFrame.quantile.
self (Self)
Downside VaR. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Downside Conditional Value At Risk (CVaR).
-Reference: https://www.investopedia.com/terms/c/conditional_value_at_risk.asp.
Downside CVaR. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Most negative percentage change over a rolling window.
-Most negative percentage change. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Maximum drawdown without any limit on date range.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
Maximum drawdown. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Share of percentage changes greater than zero.
-Share of positive returns. Float for OpenTimeSeries, Series[float]
-for OpenFrame.
SeriesOrFloat_co
-Implied annualized volatility from downside VaR.
-Assumes normally distributed returns.
-level (float) – The sought VaR level.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation type used by DataFrame.quantile.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
drift_adjust (bool) – Adjustment to remove the bias implied by the average -return.
self (Self)
Implied annualized volatility. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Skew of the return distribution.
-Reference: https://www.investopedia.com/terms/s/skewness.asp.
Skewness. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Kurtosis of the return distribution.
-Reference: https://www.investopedia.com/terms/k/kurtosis.asp.
Kurtosis. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Z-score of the last return.
-Computed as (last return - mean return) / std dev of returns.
-Reference: https://www.investopedia.com/terms/z/zscore.asp.
Z-score. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Target weight from VaR.
-Computes a position weight multiplier from the ratio between a VaR implied -volatility and a given target volatility. Multiplier = 1.0 → target met.
-target_vol (float) – Target volatility.
level (float) – The sought VaR level.
min_leverage_local (float) – Minimum adjustment factor.
max_leverage_local (float) – Maximum adjustment factor.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation type used by DataFrame.quantile.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
drift_adjust (bool) – Adjustment to remove the bias implied by the average -return.
self (Self)
Weight multiplier (or implied volatility if used downstream). Float for
-OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-The plotting methods generate fully responsive HTML output that automatically adapts to different screen sizes and device orientations. Plots are optimized for both desktop and mobile viewing with separate title containers and responsive CSS styling.
-Create a Plotly Scatter Figure.
-mode (LiteralLinePlotMode) – The type of scatter to use.
title (str | None) – A title above the plot.
tick_fmt (str | None) – Tick format for the y-axis, e.g. '%' or '.1%'.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
show_last (bool) – If True, highlight the last point in red with a label.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Create a Plotly Bar Figure.
-mode (LiteralBarPlotMode) – The type of bar to use.
title (str | None) – A title above the plot.
tick_fmt (str | None) – Tick format for the y-axis, e.g. '%' or '.1%'.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Create a Plotly Histogram Figure.
-plot_type (LiteralPlotlyHistogramPlotType) – Type of plot, "bars" or "lines".
histnorm (LiteralPlotlyHistogramHistNorm) – Normalization mode.
barmode (LiteralPlotlyHistogramBarMode) – How bar traces are displayed relative to one another.
xbins_size (float | None) – Width of each bin along the x-axis in data units.
opacity (float) – Trace opacity between 0 and 1.
bargap (float) – Gap between bars of adjacent location coordinates.
bargroupgap (float) – Gap between bar groups at the same location coordinate.
curve_type (LiteralPlotlyHistogramCurveType) – Type of distribution curve to overlay on the histogram.
title (str | None) – A title above the plot.
x_fmt (str | None) – Tick format for the x-axis.
y_fmt (str | None) – Tick format for the y-axis.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
cumulative (bool) – Whether to compute a cumulative histogram.
show_rug (bool) – Whether to draw a rug plot alongside the distribution.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Dump timeseries data into a JSON file.
- -Save .tsdf DataFrame to an Excel spreadsheet file.
The Excel file path.
-NameError – If filename does not end with .xlsx.
FileExistsError – If the file exists and overwrite is False.
Bases: _CommonModel[Series]
OpenFrame objects hold OpenTimeSeries in the list constituents.
-The intended use is to allow comparisons across these timeseries.
-OpenFrame objects hold OpenTimeSeries in the list constituents.
-The intended use is to allow comparisons across these timeseries.
- -Methods
-
|
-OpenFrame objects hold OpenTimeSeries in the list constituents. |
-
|
-To add an OpenTimeSeries object. |
-
|
-Align the index of |
-
|
-Calculate chosen timeseries properties. |
-
|
-Annualized arithmetic mean of returns. |
-
|
-Calculate autocorrelation at a given lag. |
-
|
-Market Beta. |
-
|
-Create a user-defined date range aligned to index. |
-
|
-Capture Ratio. |
-
|
-- |
|
-Returns a copy of the model. |
-
|
-Downside Conditional Value At Risk (CVaR). |
-
|
-To delete an OpenTimeSeries object. |
-
|
-- |
|
-Exponentially Weighted Moving Average Volatilities and Correlation. |
-
|
-Create copy of the OpenFrame object. |
-
|
-- |
|
-Compounded Annual Growth Rate (CAGR). |
-
|
-Information Ratio. |
-
|
-Jensen's alpha. |
-
|
-- |
|
-Kurtosis of the return distribution. |
-
|
-Lower partial moment and downside deviation (order=2). |
-
|
-Calculate a basket timeseries based on the supplied weights. |
-
|
-Maximum drawdown without any limit on date range. |
-
|
-Merge index of Pandas Dataframes of the constituent OpenTimeSeries. |
-
|
-Creates a new instance of the Model class with validated data. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Generates a JSON schema for a model class. |
-
|
-Compute the class name for parametrizations of generic classes. |
-
|
-Override this method to perform additional initialization after __init__ and model_construct. |
-
|
-Try to rebuild the pydantic-core schema for the model. |
-
|
-Validate a pydantic model instance. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Validate the given object with string data against the Pydantic model. |
-
|
-Perform a multi-factor linear regression. |
-
|
-Omega Ratio. |
-
|
-Ordinary Least Squares fit. |
-
|
-Detect outliers using z-score analysis. |
-
|
-- |
|
-- |
|
-- |
|
-Create a Plotly Bar Figure. |
-
|
-Create a Plotly Histogram Figure. |
-
|
-Create a Plotly Scatter Figure. |
-
|
-Share of percentage changes greater than zero. |
-
|
-Create a rebalanced portfolio from the OpenFrame constituents. |
-
|
-Calculate cumulative relative return between two series. |
-
|
-Resample the timeseries frequency. |
-
|
-Resamples timeseries frequency to the business calendar month end dates. |
-
|
-Ratio between arithmetic mean of returns and annualized volatility. |
-
|
-Handle missing values in a return series. |
-
|
-Calculate rolling Market Beta. |
-
|
-Calculate rolling Correlation. |
-
|
-Calculate rolling annualized downside CVaR. |
-
|
-Calculate rolling Information Ratio. |
-
|
-Calculate rolling returns. |
-
|
-Calculate rolling annualized downside Value At Risk (VaR). |
-
|
-Calculate rolling annualized volatilities. |
-
|
-- |
|
-- |
|
-Skew of the return distribution. |
-
|
-Sortino ratio or Kappa-3 ratio. |
-
|
-Target weight from VaR. |
-
|
-Convert series of returns into cumulative series of values. |
-
|
-Convert timeseries into a drawdown series. |
-
|
-Dump timeseries data into a JSON file. |
-
|
-Save |
-
|
-Tracking Error. |
-
|
-Truncate DataFrame such that all timeseries have the same time span. |
-
|
-- |
|
-- |
|
-Handle missing values in a value series. |
-
|
-Calculate simple return for a specific calendar period. |
-
|
-Calculate simple return. |
-
|
-Convert series of values to series of their period differences. |
-
|
-Convert value series to log-weighted series. |
-
|
-Convert series of values into series of returns. |
-
|
-Downside Value At Risk (VaR). |
-
|
-Implied annualized volatility from downside VaR. |
-
|
-Annualized volatility. |
-
|
-Most negative percentage change over a rolling window. |
-
|
-Z-score of the last return. |
-
Attributes
-
|
-Annualized arithmetic mean of returns. |
-
|
-Autocorrelation at lag 1. |
-
|
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame. |
-
|
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame. |
-
|
-Correlation matrix. |
-
|
-Downside 95% Conditional Value At Risk "CVaR". |
-
|
-Downside Deviation. |
-
|
-The first date in the timeseries. |
-
|
-The first dates in the timeseries of all constituents. |
-
|
-Compounded Annual Growth Rate (CAGR). |
-
|
-Number of constituents. |
-
|
-Kappa-3 ratio. |
-
|
-Kurtosis of the return distribution. |
-
|
-The last date in the timeseries. |
-
|
-The last dates in the timeseries of all constituents. |
-
|
-Number of observations. |
-
|
-Number of observations of all constituents. |
-
|
-Maximum drawdown without any limit on date range. |
-
|
-Maximum drawdown in a single calendar year. |
-
|
-Date when the maximum drawdown occurred. |
-
|
-- |
|
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. |
-
|
-Get extra fields set during validation. |
-
|
-- |
|
-Returns the set of fields that have been explicitly set on this model instance. |
-
|
-Omega ratio. |
-
|
-The average number of observations per year. |
-
|
-The share of percentage changes that are greater than zero. |
-
|
-Ratio of annualized arithmetic mean of returns and annualized volatility. |
-
|
-Skew of the return distribution. |
-
|
-Sortino ratio. |
-
|
-Number of days from the first date to the last. |
-
|
-Number of days from the first date to the last for all items in the frame. |
-
|
-Simple return. |
-
|
-Downside 95% Value At Risk (VaR). |
-
|
-Annualized volatility. |
-
|
-Implied annualized volatility from Downside 95% Value at Risk. |
-
|
-Most negative percentage change. |
-
|
-Most negative month. |
-
|
-Length of series in years assuming 365.25 days per year. |
-
|
-Z-score. |
-
|
-- |
|
-- |
|
-- |
|
-- |
OpenFrame objects hold OpenTimeSeries in the list constituents.
-The intended use is to allow comparisons across these timeseries.
- -Create copy of the OpenFrame object.
- -Merge index of Pandas Dataframes of the constituent OpenTimeSeries.
- -Calculate chosen timeseries properties.
-properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'autocorr', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown', 'max_drawdown_date', 'max_drawdown_cal_year', 'first_indices', 'last_indices', 'lengths_of_items', 'span_of_days_all']] | None) – The properties to calculate. Defaults to calculating all -available. Optional.
self (Self)
Properties of the constituent OpenTimeSeries.
-Number of observations of all constituents.
-Number of observations of all constituents.
-Number of constituents.
-Number of constituents.
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 0 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-Level 1 values of the MultiIndex columns in the .tsdf DataFrame.
-The first dates in the timeseries of all constituents.
-The first dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-The last dates in the timeseries of all constituents.
-Number of days from the first date to the last for all items in the frame.
-Number of days from the first date to the last for all -items in the frame.
-Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert series of returns into cumulative series of values.
- -Resample the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenFrame object.
-Exponentially Weighted Moving Average Volatilities and Correlation.
-Exponentially Weighted Moving Average (EWMA) for Volatilities and -Correlation.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
first_column (int) – Column of first timeseries. Defaults to 0.
second_column (int) – Column of second timeseries. Defaults to 1.
corr_scale (float) – Correlation scale factor. Defaults to 2.0.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series volatilities and correlation.
-DataFrame
-Correlation matrix.
-This property returns the correlation matrix of the time series -in the frame.
-Correlation matrix of the time series in the frame.
-To add an OpenTimeSeries object.
- -To delete an OpenTimeSeries object.
- -Truncate DataFrame such that all timeseries have the same time span.
-start_cut (dt.date | None) – New first date. Optional.
end_cut (dt.date | None) – New last date. Optional.
where (LiteralTrunc) – Determines where dataframe is truncated also when start_cut -or end_cut is None. Defaults to both.
self (Self)
An OpenFrame object.
-Self
-Calculate cumulative relative return between two series.
-None
-Tracking Error.
-Calculates Tracking Error which is the standard deviation of the -difference between the fund and its index returns.
-Reference: https://www.investopedia.com/terms/t/trackingerror.asp.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Tracking Errors.
-Series[float]
-Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided -by the Tracking Error. And the Tracking Error is the standard deviation of -the difference between the fund and its index returns. -The ratio is calculated using the annualized arithmetic mean of returns.
-base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Information Ratios.
-Series[float]
-Capture Ratio.
-The Up (Down) Capture Ratio is calculated by dividing the CAGR -of the asset during periods that the benchmark returns are positive (negative) -by the CAGR of the benchmark during the same periods. -CaptureRatio.BOTH is the Up ratio divided by the Down ratio. -Source: ‘Capture Ratios: A Popular Method of Measuring Portfolio Performance -in Practice’, Don R. Cox and Delbert C. Goff, Journal of Economics and -Finance Education (Vol 2 Winter 2013).
-Reference: https://www.economics-finance.org/jefe/volume12-2/11ArticleCox.pdf.
-ratio (LiteralCaptureRatio) – The ratio to calculate.
base_column (tuple[str, ValueType] | int) – Column of timeseries that is the denominator in the ratio. -Defaults to -1.
months_from_last (int | None) – Number of months offset as positive integer. Overrides -use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Capture Ratios.
-Series[float]
-Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-Beta as Co-variance of x & y divided by Variance of x.
-Ordinary Least Squares fit.
-Performs a linear regression and adds a new column with a fitted line -using Ordinary Least Squares fit.
-y_column (tuple[str, ValueType] | int) – The column level values of the dependent variable y.
x_column (tuple[str, ValueType] | int) – The column level values of the exogenous variable x.
fitted_series (bool) – If True the fit is added as a new column in the .tsdf -Pandas.DataFrame. Defaults to True.
self (Self)
A dictionary with the coefficient, intercept and rsquared outputs.
-Jensen’s alpha.
-The Jensen’s measure, or Jensen’s alpha, is a risk-adjusted performance -measure that represents the average return on a portfolio or investment, -above or below that predicted by the capital asset pricing model (CAPM), -given the portfolio’s or investment’s beta and the average market return. -This metric is also commonly referred to as simply alpha.
-Reference: https://www.investopedia.com/terms/j/jensensmeasure.asp.
-asset (tuple[str, ValueType] | int) – The column of the asset.
market (tuple[str, ValueType] | int) – The column of the market against which Jensen’s alpha is measured.
riskfree_rate (float) – The return of the zero volatility riskfree asset. -Defaults to 0.0.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Jensen’s alpha.
-Calculate a basket timeseries based on the supplied weights.
- -Calculate rolling Information Ratio.
-The Information Ratio equals ( fund return less index return ) divided by -the Tracking Error. And the Tracking Error is the standard deviation of the -difference between the fund and its index returns.
-long_column (int) – Column of timeseries that is the numerator in the ratio. -Defaults to 0.
short_column (int) – Column of timeseries that is the denominator in the ratio. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
periods_in_a_year_fixed (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])] | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Rolling Information Ratios.
-Calculate rolling Market Beta.
-Calculates Beta as Co-variance of asset & market divided by Variance -of the market.
-Reference: https://www.investopedia.com/terms/b/beta.asp.
-asset_column (int) – Column of timeseries that is the asset. Defaults to 0.
market_column (int) – Column of timeseries that is the market. Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 1.
self (Self)
Rolling Betas.
-Calculate rolling Correlation.
-Calculates correlation between two series. The period with -at least the given number of observations is the first period calculated.
-first_column (int) – The position as integer of the first timeseries to compare. -Defaults to 0.
second_column (int) – The position as integer of the second timeseries to compare. -Defaults to 1.
observations (int) – The length of the rolling window to use is set as number of -observations. Defaults to 21.
self (Self)
Rolling Correlations.
-Perform a multi-factor linear regression.
-This function treats one specified column in the DataFrame as the dependent -variable (y) and uses all remaining columns as independent variables (X). -It utilizes a scikit-learn LinearRegression model and returns a DataFrame -with summary output and an OpenTimeSeries of predicted values.
-A DataFrame with the R-squared, the intercept and the regression -coefficients
An OpenTimeSeries of predicted values
A tuple containing
-KeyError – If the column tuple is not found in the OpenFrame.tsdf.columns.
ValueError – If not all series are returnseries (ValueType.RTRN).
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Create a rebalanced portfolio from the OpenFrame constituents.
-name (str) – Name of the portfolio.
items (list[str] | None) – List of items to include in the portfolio. If None, uses all items. -Optional.
bal_weights (list[float] | None) – List of weights for rebalancing. If None, uses frame weights. -Optional.
frequency (int) – Rebalancing frequency. Defaults to 1.
cash_index (OpenTimeSeries | None) – Cash index series for cash component. Optional.
equal_weights (bool) – If True, use equal weights for all items. Defaults to False.
drop_extras (bool) – If True, only return TWR series; if False, return all details. -Defaults to True.
self (Self)
OpenFrame containing the rebalanced portfolio.
-OpenFrame
-Bases: _CommonModel[float]
OpenTimeSeries objects are at the core of the openseries package.
-The intended use is to allow analyses of financial timeseries. -It is only intended for daily or less frequent data samples.
-timeseries_id (str) – Database identifier of the timeseries.
instrument_id (str) – Database identifier of the instrument associated with -the timeseries.
name (str) – String identifier of the timeseries and/or instrument.
valuetype (ValueType) – Identifies if the series is a series of values or returns.
dates (Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]) – Dates of the individual timeseries items. -These dates will not be altered by methods.
values (Annotated[list[float], MinLen(min_length=1)]) – The value or return values of the timeseries items. -These values will not be altered by methods.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency.
tsdf (DataFrame) – Pandas object holding dates and values that can be altered via -methods.
currency (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries.
domestic (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the user’s home currency. -Defaults to “SEK”.
countries (Annotated[set[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]], MinLen(min_length=1)] | Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars. -Optional.
isin (str | None) – ISO 6166 identifier code of the associated instrument. Optional.
label (str | None) – Placeholder for a name of the timeseries. Optional.
Create a new model by parsing and validating input data from keyword arguments.
-Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.
-self is explicitly positional-only to allow self as a field name.
-data (Any)
-None
-Methods
-
|
-Create a new model by parsing and validating input data from keyword arguments. |
-
|
-Calculate autocorrelation function for specified lags. |
-
|
-Align the index of |
-
|
-Calculate chosen properties. |
-
|
-Annualized arithmetic mean of returns. |
-
|
-Calculate autocorrelation at a given lag. |
-
|
-Create a user-defined date range aligned to index. |
-
|
-- |
|
-Returns a copy of the model. |
-
|
-Downside Conditional Value At Risk (CVaR). |
-
|
-- |
|
-Exponentially Weighted Moving Average Model for Value At Risk (VaR). |
-
|
-Exponentially Weighted Moving Average Model for Volatility. |
-
|
-Convert series of 1-day rates into series of cumulative values. |
-
|
-Create series from a list of dates and a list of values. |
-
|
-Create copy of OpenTimeSeries object. |
-
|
-Create series from a Pandas DataFrame or Series. |
-
|
-Create series from values accruing with a given fixed rate return. |
-
|
-- |
|
-Compounded Annual Growth Rate (CAGR). |
-
|
-- |
|
-Kurtosis of the return distribution. |
-
|
-Compute Ljung-Box test for autocorrelation. |
-
|
-Lower partial moment and downside deviation (order=2). |
-
|
-Maximum drawdown without any limit on date range. |
-
|
-Creates a new instance of the Model class with validated data. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Generates a JSON schema for a model class. |
-
|
-Compute the class name for parametrizations of generic classes. |
-
|
-Override this method to perform additional initialization after __init__ and model_construct. |
-
|
-Try to rebuild the pydantic-core schema for the model. |
-
|
-Validate a pydantic model instance. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Validate the given object with string data against the Pydantic model. |
-
|
-Omega Ratio. |
-
|
-Detect outliers using z-score analysis. |
-
|
-Calculate partial autocorrelation function for specified lags. |
-
|
-Populate .tsdf Pandas DataFrame from the .dates and .values lists. |
-
|
-- |
|
-- |
|
-- |
|
-Calculate partial autocorrelation at a given lag. |
-
|
-Create a Plotly Bar Figure. |
-
|
-Create a Plotly Histogram Figure. |
-
|
-Create a Plotly Scatter Figure. |
-
|
-Share of percentage changes greater than zero. |
-
|
-Resamples the timeseries frequency. |
-
|
-Resamples timeseries frequency to the business calendar month end dates. |
-
|
-Ratio between arithmetic mean of returns and annualized volatility. |
-
|
-Handle missing values in a return series. |
-
|
-Calculate rolling annualized downside CVaR. |
-
|
-Calculate rolling returns. |
-
|
-Calculate rolling annualized downside Value At Risk (VaR). |
-
|
-Calculate rolling annualized volatilities. |
-
|
-Add or subtract a fee from the timeseries return. |
-
|
-- |
|
-- |
|
-Set the column labels of the .tsdf Pandas Dataframe. |
-
|
-Skew of the return distribution. |
-
|
-Sortino ratio or Kappa-3 ratio. |
-
|
-Target weight from VaR. |
-
|
-Convert series of returns into cumulative series of values. |
-
|
-Convert timeseries into a drawdown series. |
-
|
-Dump timeseries data into a JSON file. |
-
|
-Save |
-
|
-- |
|
-- |
|
-Handle missing values in a value series. |
-
|
-Calculate simple return for a specific calendar period. |
-
|
-Calculate simple return. |
-
|
-Convert series of values to series of their period differences. |
-
|
-Convert value series to log-weighted series. |
-
|
-Convert series of values into series of returns. |
-
|
-Downside Value At Risk (VaR). |
-
|
-Implied annualized volatility from downside VaR. |
-
|
-Annualized volatility. |
-
|
-Most negative percentage change over a rolling window. |
-
|
-Z-score of the last return. |
-
Attributes
-
|
-Annualized arithmetic mean of returns. |
-
|
-Autocorrelation at lag 1. |
-
|
-Downside 95% Conditional Value At Risk "CVaR". |
-
|
-Downside Deviation. |
-
|
-The first date in the timeseries. |
-
|
-Compounded Annual Growth Rate (CAGR). |
-
|
-Kappa-3 ratio. |
-
|
-Kurtosis of the return distribution. |
-
|
-The last date in the timeseries. |
-
|
-Number of observations. |
-
|
-Maximum drawdown without any limit on date range. |
-
|
-Maximum drawdown in a single calendar year. |
-
|
-Date when the maximum drawdown occurred. |
-
|
-- |
|
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. |
-
|
-Get extra fields set during validation. |
-
|
-- |
|
-Returns the set of fields that have been explicitly set on this model instance. |
-
|
-Omega ratio. |
-
|
-The average number of observations per year. |
-
|
-The share of percentage changes that are greater than zero. |
-
|
-Ratio of annualized arithmetic mean of returns and annualized volatility. |
-
|
-Skew of the return distribution. |
-
|
-Sortino ratio. |
-
|
-Number of days from the first date to the last. |
-
|
-Simple return. |
-
|
-Downside 95% Value At Risk (VaR). |
-
|
-Annualized volatility. |
-
|
-Implied annualized volatility from Downside 95% Value at Risk. |
-
|
-Most negative percentage change. |
-
|
-Most negative month. |
-
|
-Length of series in years assuming 365.25 days per year. |
-
|
-Z-score. |
-
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
Create series from a list of dates and a list of values.
-name (str) – String identifier of the timeseries and/or instrument.
dates (Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]) – List of date strings as ISO 8601 YYYY-MM-DD.
values (Annotated[list[float], MinLen(min_length=1)]) – Array of float values.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
timeseries_id (str) – Database identifier of the timeseries. Optional.
instrument_id (str) – Database identifier of the instrument associated -with the timeseries. Optional.
isin (str | None) – ISO 6166 identifier code of the associated instrument. Optional.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-Create series from a Pandas DataFrame or Series.
-dframe (Series | DataFrame | object) – Pandas DataFrame or Series.
column_nmbr (int) – Using iloc[:, column_nmbr] to pick column. Defaults to 0.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-TypeError – If dframe is not a pandas.Series or a
- pandas.DataFrame.
Create series from values accruing with a given fixed rate return.
-Providing a date_range of type Pandas DatetimeIndex takes priority over -providing a combination of days and an end date.
-rate (float) – The accrual rate.
d_range (DatetimeIndex | None) – A given range of dates. Optional.
days (int | None) – Number of days to generate when date_range not provided. Must be -combined with end_dt. Optional.
end_dt (date | None) – End date of date range to generate when date_range not provided. -Must be combined with days. Optional.
label (str) – Placeholder for a name of the timeseries.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – The currency of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-IncorrectArgumentComboError – If d_range is not provided and the
- combination of days and end_dt is incomplete.
Create copy of OpenTimeSeries object.
- -Populate .tsdf Pandas DataFrame from the .dates and .values lists.
- -Calculate chosen properties.
-properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown_cal_year', 'max_drawdown', 'max_drawdown_date', 'first_idx', 'last_idx', 'length', 'span_of_days', 'yearfrac', 'periods_in_a_year', 'autocorr', 'partial_autocorr']] | None) – The properties to calculate. Defaults to calculating all -available. Optional.
self (Self)
Properties of the OpenTimeSeries.
-Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert series of returns into cumulative series of values.
- -Convert series of 1-day rates into series of cumulative values.
- -Resamples the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenTimeSeries object.
-ResampleDataLossError – If called on a return series (valuetype is
- ValueType.RTRN), since summation across sparser frequency would
- be required to avoid data loss.
Exponentially Weighted Moving Average Model for Volatility.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. -Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
months_from_last (int | None) – Number of months offset as positive integer. -Overrides use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series EWMA volatility.
-Series[float]
-Exponentially Weighted Moving Average Model for Value At Risk (VaR).
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. -Defaults to 11.
level (float) – The sought VaR level. Defaults to 0.95.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
months_from_last (int | None) – Number of months offset as positive integer. -Overrides use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series EWMA VaR.
-Series[float]
-Add or subtract a fee from the timeseries return.
- -Set the column labels of the .tsdf Pandas Dataframe.
- -Calculate autocorrelation function for specified lags.
-Series of autocorrelations indexed by lag.
-Series[float]
-Calculate partial autocorrelation at a given lag.
- -Calculate partial autocorrelation function for specified lags.
-Series of partial autocorrelations indexed by lag.
-Series[float]
-Compute Ljung-Box test for autocorrelation.
-Tuple of (statistic, pvalue, lags) where statistic is the Ljung-Box -Q statistic, pvalue is the chi-squared p-value, and lags is the -list of lags used.
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Bases: BaseModel
The class ReturnSimulation allows for simulating financial timeseries.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Total number of days to simulate.
trading_days_in_year (Annotated[int, Strict(strict=True), Ge(ge=1), Le(le=366)]) – Number of trading days used to annualize.
mean_annual_return (float) – Mean annual return of the distribution.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean annual standard deviation of the distribution.
dframe (DataFrame) – Pandas DataFrame object holding the resulting values.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point in time. -Defaults to 0.0.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
seed (int | None) – Seed for random process initiation.
Create a new model by parsing and validating input data from keyword arguments.
-Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be -validated to form a valid model.
-self is explicitly positional-only to allow self as a field name.
-data (Any)
-None
-Methods
-
|
-Create a new model by parsing and validating input data from keyword arguments. |
-
|
-- |
|
-Returns a copy of the model. |
-
|
-- |
|
-Create a Geometric Brownian Motion simulation. |
-
|
-Create a Lognormal distribution simulation. |
-
|
-Create a Merton Jump-Diffusion model simulation. |
-
|
-Create a Normal distribution simulation. |
-
|
-- |
|
-- |
|
-Creates a new instance of the Model class with validated data. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Generates a JSON schema for a model class. |
-
|
-Compute the class name for parametrizations of generic classes. |
-
|
-Override this method to perform additional initialization after __init__ and model_construct. |
-
|
-Try to rebuild the pydantic-core schema for the model. |
-
|
-Validate a pydantic model instance. |
-
|
-!!! abstract "Usage Documentation" |
-
|
-Validate the given object with string data against the Pydantic model. |
-
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-Create a pandas.DataFrame from simulation(s). |
-
|
-- |
|
-- |
Attributes
-
|
-- |
|
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict]. |
-
|
-Get extra fields set during validation. |
-
|
-- |
|
-Returns the set of fields that have been explicitly set on this model instance. |
-
|
-Annualized arithmetic mean of returns. |
-
|
-Annualized volatility. |
-
|
-Simulation data. |
-
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Annualized arithmetic mean of returns.
-Annualized arithmetic mean of returns.
-Annualized volatility.
-Annualized volatility.
-Create a Normal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Normal distribution simulation.
-ReturnSimulation
-Create a Lognormal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Lognormal distribution simulation.
-ReturnSimulation
-Create a Geometric Brownian Motion simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Geometric Brownian Motion simulation.
-ReturnSimulation
-Create a Merton Jump-Diffusion model simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point -in time.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Merton Jump-Diffusion model simulation.
-ReturnSimulation
-Create a pandas.DataFrame from simulation(s).
-name (str) – Name label of the serie(s).
start (dt.date | None) – Date when the simulation starts.
end (dt.date | None) – Date when the simulation ends.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
self (Self)
The simulation(s) data.
-DataFrame
-Bases: StrEnum
Enum types of OpenTimeSeries to identify the output.
-Methods
-
|
-Encode the string using the codec registered for encoding. |
-
|
-Return a copy with all occurrences of substring old replaced by new. |
-
|
-Return a list of the substrings in the string, using sep as the separator string. |
-
|
-Return a list of the substrings in the string, using sep as the separator string. |
-
|
-Concatenate any number of strings. |
-
|
-Return a capitalized version of the string. |
-
|
-Return a version of the string suitable for caseless comparisons. |
-
|
-Return a version of the string where each word is titlecased. |
-
|
-Return a centered string of length width. |
-
|
-Return the number of non-overlapping occurrences of substring sub in string S[start:end]. |
-
|
-Return a copy where all tab characters are expanded using spaces. |
-
|
-Return the lowest index in S where substring sub is found, such that sub is contained within S[start:end]. |
-
|
-Partition the string into three parts using the given separator. |
-
|
-Return the lowest index in S where substring sub is found, such that sub is contained within S[start:end]. |
-
|
-Return a left-justified string of length width. |
-
|
-Return a copy of the string converted to lowercase. |
-
|
-Return a copy of the string with leading whitespace removed. |
-
|
-Return the highest index in S where substring sub is found, such that sub is contained within S[start:end]. |
-
|
-Return the highest index in S where substring sub is found, such that sub is contained within S[start:end]. |
-
|
-Return a right-justified string of length width. |
-
|
-Return a copy of the string with trailing whitespace removed. |
-
|
-Partition the string into three parts using the given separator. |
-
|
-Return a list of the lines in the string, breaking at line boundaries. |
-
|
-Return a copy of the string with leading and trailing whitespace removed. |
-
|
-Convert uppercase characters to lowercase and lowercase characters to uppercase. |
-
|
-Replace each character in the string using the given translation table. |
-
|
-Return a copy of the string converted to uppercase. |
-
|
-Return True if the string starts with the specified prefix, False otherwise. |
-
|
-Return True if the string ends with the specified suffix, False otherwise. |
-
|
-Return a str with the given prefix string removed if present. |
-
|
-Return a str with the given suffix string removed if present. |
-
|
-Return True if all characters in the string are ASCII, False otherwise. |
-
|
-Return True if the string is a lowercase string, False otherwise. |
-
|
-Return True if the string is an uppercase string, False otherwise. |
-
|
-Return True if the string is a title-cased string, False otherwise. |
-
|
-Return True if the string is a whitespace string, False otherwise. |
-
|
-Return True if the string is a decimal string, False otherwise. |
-
|
-Return True if the string is a digit string, False otherwise. |
-
|
-Return True if the string is a numeric string, False otherwise. |
-
|
-Return True if the string is an alphabetic string, False otherwise. |
-
|
-Return True if the string is an alpha-numeric string, False otherwise. |
-
|
-Return True if the string is a valid Python identifier, False otherwise. |
-
|
-Return True if all characters in the string are printable, False otherwise. |
-
|
-Pad a numeric string with zeros on the left, to fill a field of the given width. |
-
|
-Return a formatted version of the string, using substitutions from args and kwargs. |
-
|
-Return a formatted version of the string, using substitutions from mapping. |
-
|
-Return a translation table usable for str.translate(). |
-
|
-- |
Attributes
-
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
|
-- |
Constrain optimized portfolios to those that improve on the current one.
-data (OpenFrame) – Portfolio data.
serie (OpenTimeSeries) – A timeseries representing the current portfolio.
portfolioname (str) – Name of the portfolio. Defaults to “Current Portfolio”.
simulations (int) – Number of possible portfolios to simulate. Defaults to 10000.
curve_points (int) – Number of optimal portfolios on the efficient frontier. -Defaults to 200.
bounds (tuple[tuple[float, float], ...] | None) – The range of minimum and maximum allowed allocations for each asset.
minimize_method (LiteralMinimizeMethods) – The method passed into the scipy.minimize function. -Defaults to SLSQP.
The constrained optimal portfolio data.
-tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
-Offset dates according to a given calendar.
-raw_date (DateType) – The date to offset from.
months_offset (int) – Number of months as integer. Defaults to 12.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
adjust (bool) – Determines if offset should adjust for business days. -Defaults to False.
following (bool) – Determines if days should be offset forward (following) or backward. -Defaults to True.
Offset date.
-dt.date
-Identify an efficient frontier.
-eframe (OpenFrame) – Portfolio data.
num_ports (int) – Number of possible portfolios to simulate. Defaults to 5000.
seed (int) – The seed for the random process. Defaults to 71.
bounds (tuple[tuple[float, float], ...] | None) – The range of minimum and maximum allowed allocations for each asset.
frontier_points (int) – Number of points along frontier to optimize. Defaults to 200.
minimize_method (LiteralMinimizeMethods) – The method passed into the scipy.minimize function. -Defaults to SLSQP.
tweak (bool) – Cutting the frontier to exclude multiple points with almost the -same risk. -Defaults to True.
The efficient frontier data, simulation data and optimal portfolio.
-tuple[DataFrame, DataFrame, NDArray[float64]]
-Generate a list of business day calendar dates.
-trading_days (int) – Number of days to generate. Must be greater than zero.
start (dt.date | None) – Date when the range starts.
end (dt.date | None) – Date when the range ends.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
List of business day calendar dates.
-list[dt.date]
-Bump date backwards to find the previous business day.
-today (dt.date | None) – Manual input of the day from where the previous business day is found.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
The previous business day.
-dt.date
-Generate a business calendar.
-startyear (int) – First year in date range generated.
endyear (int) – Last year in date range generated.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
Generate a business calendar.
-CountriesNotStringNorListStrError – If countries is not a supported
- ISO 3166-1 alpha-2 string or a list of such strings.
busdaycalendar
-Load Plotly defaults.
-responsive (bool) – Flag whether to load as responsive. Defaults to True.
-A tuple (config_and_layout, logo) -where config_and_layout is the Plotly config and layout template dict, -and logo is the Captor logo dict (may be empty if the remote logo is -unavailable).
-tuple[PlotlyLayoutType, CaptorLogoType]
-Bump date by business days.
-It first adjusts to a valid business day and then bumps with given -number of business days from there.
-ddate (dt.date) – A starting date that does not have to be a business day.
days (int) – The number of business days to offset from the business day -that is given. -If days is set as anything other than an integer its value is set to zero.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Argument where missing holidays can be added.
The new offset business day.
-dt.date
-Prepare data to be used as point_frame in the sharpeplot function.
-assets (OpenFrame) – Portfolio data with individual assets and a weighted portfolio.
current (OpenTimeSeries) – The current or initial portfolio based on given weights.
optimized (NDArray[float64]) – Data optimized with the efficient_frontier method.
The data prepared with mean returns, volatility and weights.
-DataFrame
-Generate a responsive HTML report page with line and bar plots and a table.
-Create scatter plot coloured by Sharpe Ratio.
-sim_frame (DataFrame | None) – Data from the simulate_portfolios method.
line_frame (DataFrame | None) – Data from the efficient_frontier method.
point_frame (DataFrame | None) – Data to highlight current and efficient portfolios.
point_frame_mode (LiteralLinePlotMode) – Which type of scatter to use. Defaults to markers.
filename (str | None) – Name of the Plotly html file.
directory (DirectoryPath | None) – Directory where Plotly html file is saved.
titletext (str | None) – Text for the plot title.
output_type (LiteralPlotlyOutput) – Determines output type. Defaults to “file”.
include_plotlyjs (LiteralPlotlyJSlib) – Determines how the plotly.js library is included -in the output. -Defaults to “cdn”.
title (bool) – Whether to add standard plot title. Defaults to True.
add_logo (bool) – Whether to add Captor logo. Defaults to True.
auto_open (bool) – Determines whether to open a browser window with the plot. -Defaults to True.
The scatter plot with simulated and optimized results.
-Chain two timeseries together.
-The function assumes that the two series have at least one date in common.
-front (TypeOpenTimeSeries) – Earlier series to chain with.
back (TypeOpenTimeSeries) – Later series to chain with.
old_fee (float) – Fee to apply to earlier series. Defaults to 0.0.
An OpenTimeSeries object or a subclass thereof.
-TypeOpenTimeSeries
-The openseries package provides two main classes for financial time series analysis:
-OpenTimeSeries: For single financial time series analysis
OpenFrame: For multi-asset portfolio analysis and comparison
Both classes inherit from the private _CommonModel class, which in turn inherits from Pydantic’s BaseModel. This inheritance structure provides:
Data validation through Pydantic’s validation system
Common functionality shared between both classes (risk metrics, plotting, data handling)
Type safety and consistent API design
The _CommonModel class contains all the shared methods and properties that both OpenTimeSeries and OpenFrame use, including risk calculations, plotting capabilities, and data manipulation functions.
|
-OpenTimeSeries objects are at the core of the openseries package. |
-
|
-OpenFrame objects hold OpenTimeSeries in the list constituents. |
-
|
-Chain two timeseries together. |
-
|
-Generate a responsive HTML report page with line and bar plots and a table. |
-
|
-Identify an efficient frontier. |
-
|
-Generate random weights for simulated portfolios. |
-
|
-Constrain optimized portfolios to those that improve on the current one. |
-
|
-Prepare data to be used as point_frame in the sharpeplot function. |
-
|
-Create scatter plot coloured by Sharpe Ratio. |
-
|
-Parse different date formats into datetime.date. |
-
|
-Offset dates according to a given calendar. |
-
|
-Generate a list of business day calendar dates. |
-
|
-Bump date backwards to find the previous business day. |
-
|
-Generate a business calendar. |
-
|
-Bump date by business days. |
-
|
-The class ReturnSimulation allows for simulating financial timeseries. |
-
|
-Enum types of OpenTimeSeries to identify the output. |
-
|
-Load Plotly defaults. |
-
|
-Export a Plotly figure to a mobile-responsive HTML file or inline div. |
-
The portfoliotools module provides functions for portfolio optimization, simulation, and analysis.
-Identify an efficient frontier.
-eframe (OpenFrame) – Portfolio data.
num_ports (int) – Number of possible portfolios to simulate. Defaults to 5000.
seed (int) – The seed for the random process. Defaults to 71.
bounds (tuple[tuple[float, float], ...] | None) – The range of minimum and maximum allowed allocations for each asset.
frontier_points (int) – Number of points along frontier to optimize. Defaults to 200.
minimize_method (LiteralMinimizeMethods) – The method passed into the scipy.minimize function. -Defaults to SLSQP.
tweak (bool) – Cutting the frontier to exclude multiple points with almost the -same risk. -Defaults to True.
The efficient frontier data, simulation data and optimal portfolio.
-tuple[DataFrame, DataFrame, NDArray[float64]]
-Generate random weights for simulated portfolios.
- -Constrain optimized portfolios to those that improve on the current one.
-data (OpenFrame) – Portfolio data.
serie (OpenTimeSeries) – A timeseries representing the current portfolio.
portfolioname (str) – Name of the portfolio. Defaults to “Current Portfolio”.
simulations (int) – Number of possible portfolios to simulate. Defaults to 10000.
curve_points (int) – Number of optimal portfolios on the efficient frontier. -Defaults to 200.
bounds (tuple[tuple[float, float], ...] | None) – The range of minimum and maximum allowed allocations for each asset.
minimize_method (LiteralMinimizeMethods) – The method passed into the scipy.minimize function. -Defaults to SLSQP.
The constrained optimal portfolio data.
-tuple[OpenFrame, OpenTimeSeries, OpenFrame, OpenTimeSeries]
-Prepare data to be used as point_frame in the sharpeplot function.
-assets (OpenFrame) – Portfolio data with individual assets and a weighted portfolio.
current (OpenTimeSeries) – The current or initial portfolio based on given weights.
optimized (NDArray[float64]) – Data optimized with the efficient_frontier method.
The data prepared with mean returns, volatility and weights.
-DataFrame
-Create scatter plot coloured by Sharpe Ratio.
-sim_frame (DataFrame | None) – Data from the simulate_portfolios method.
line_frame (DataFrame | None) – Data from the efficient_frontier method.
point_frame (DataFrame | None) – Data to highlight current and efficient portfolios.
point_frame_mode (LiteralLinePlotMode) – Which type of scatter to use. Defaults to markers.
filename (str | None) – Name of the Plotly html file.
directory (DirectoryPath | None) – Directory where Plotly html file is saved.
titletext (str | None) – Text for the plot title.
output_type (LiteralPlotlyOutput) – Determines output type. Defaults to “file”.
include_plotlyjs (LiteralPlotlyJSlib) – Determines how the plotly.js library is included -in the output. -Defaults to “cdn”.
title (bool) – Whether to add standard plot title. Defaults to True.
add_logo (bool) – Whether to add Captor logo. Defaults to True.
auto_open (bool) – Determines whether to open a browser window with the plot. -Defaults to True.
The scatter plot with simulated and optimized results.
-Generate a responsive HTML report page with line and bar plots and a table.
-The report_html function creates comprehensive HTML reports for financial analysis, comparing multiple assets and providing detailed performance metrics, charts, and risk analysis.
The generated report includes:
-Interactive line charts showing cumulative returns over time for all assets
Bar charts displaying period returns (annual, quarterly, or monthly depending on data length)
Performance metrics table including: -- Return metrics (CAGR or simple return, Year-to-Date, Month-to-Date) -- Risk metrics (Volatility, Sharpe Ratio, Sortino Ratio) -- Relative performance metrics (Jensen’s Alpha, Information Ratio, Tracking Error, Index Beta) -- Capture Ratio (for periods longer than one year) -- Worst period returns -- Comparison period dates
Important Notes:
-The last asset in the OpenFrame is used as the benchmark for relative performance metrics
-(Jensen’s Alpha, Information Ratio, Tracking Error, Index Beta, and Capture Ratio)
For periods shorter than one year, the report uses simple returns instead of CAGR
For periods shorter than a quarter, bar charts show daily returns instead of period returns
Capture Ratio is only included for periods longer than one year
The report features responsive design with separate layouts optimized for desktop and mobile devices:
-Desktop layout: Charts and tables are displayed side-by-side in a 2x2 grid layout. The table -is rendered as an interactive Plotly table integrated with the charts.
Mobile layout: Content is stacked vertically for better viewing on smaller screens. The table -is rendered as a standard HTML table below the charts for better mobile compatibility.
The HTML output automatically adapts to screen size and device capabilities using CSS media queries -and JavaScript detection. The layout switches at a breakpoint of 960px width or when touch capabilities -are detected.
-The function returns a tuple containing:
-Plotly Figure: The desktop version of the figure object (can be used for further customization -or interactive display)
String output: The type depends on the output_type parameter:
When output_type="file" (default): Returns the file path string to the saved HTML file.
-The file contains a complete HTML document (with DOCTYPE, html, head, and body tags)
-that can be opened directly in a web browser. If auto_open=True, the file will
-automatically open in the default web browser.
When output_type="div": Returns a string containing the responsive HTML div section
-(includes both desktop and mobile layouts with CSS and JavaScript) that can be embedded
-in an existing HTML page. The filename parameter is optional when using this mode and
-is only used to generate unique div IDs for the embedded content.
Bases: _CommonModel[float]
OpenTimeSeries objects are at the core of the openseries package.
-The intended use is to allow analyses of financial timeseries. -It is only intended for daily or less frequent data samples.
-timeseries_id (str) – Database identifier of the timeseries.
instrument_id (str) – Database identifier of the instrument associated with -the timeseries.
name (str) – String identifier of the timeseries and/or instrument.
valuetype (ValueType) – Identifies if the series is a series of values or returns.
dates (Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]) – Dates of the individual timeseries items. -These dates will not be altered by methods.
values (Annotated[list[float], MinLen(min_length=1)]) – The value or return values of the timeseries items. -These values will not be altered by methods.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency.
tsdf (DataFrame) – Pandas object holding dates and values that can be altered via -methods.
currency (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries.
domestic (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the user’s home currency. -Defaults to “SEK”.
countries (Annotated[set[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]], MinLen(min_length=1)] | Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars. -Optional.
isin (str | None) – ISO 6166 identifier code of the associated instrument. Optional.
label (str | None) – Placeholder for a name of the timeseries. Optional.
Create series from a list of dates and a list of values.
-name (str) – String identifier of the timeseries and/or instrument.
dates (Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]) – List of date strings as ISO 8601 YYYY-MM-DD.
values (Annotated[list[float], MinLen(min_length=1)]) – Array of float values.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
timeseries_id (str) – Database identifier of the timeseries. Optional.
instrument_id (str) – Database identifier of the instrument associated -with the timeseries. Optional.
isin (str | None) – ISO 6166 identifier code of the associated instrument. Optional.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-Create series from a Pandas DataFrame or Series.
-dframe (Series | DataFrame | object) – Pandas DataFrame or Series.
column_nmbr (int) – Using iloc[:, column_nmbr] to pick column. Defaults to 0.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-TypeError – If dframe is not a pandas.Series or a
- pandas.DataFrame.
Create series from values accruing with a given fixed rate return.
-Providing a date_range of type Pandas DatetimeIndex takes priority over -providing a combination of days and an end date.
-rate (float) – The accrual rate.
d_range (DatetimeIndex | None) – A given range of dates. Optional.
days (int | None) – Number of days to generate when date_range not provided. Must be -combined with end_dt. Optional.
end_dt (date | None) – End date of date range to generate when date_range not provided. -Must be combined with days. Optional.
label (str) – Placeholder for a name of the timeseries.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – The currency of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-IncorrectArgumentComboError – If d_range is not provided and the
- combination of days and end_dt is incomplete.
Create copy of OpenTimeSeries object.
- -Populate .tsdf Pandas DataFrame from the .dates and .values lists.
- -Calculate chosen properties.
-properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown_cal_year', 'max_drawdown', 'max_drawdown_date', 'first_idx', 'last_idx', 'length', 'span_of_days', 'yearfrac', 'periods_in_a_year', 'autocorr', 'partial_autocorr']] | None) – The properties to calculate. Defaults to calculating all -available. Optional.
self (Self)
Properties of the OpenTimeSeries.
-Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert series of returns into cumulative series of values.
- -Convert series of 1-day rates into series of cumulative values.
- -Resamples the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenTimeSeries object.
-ResampleDataLossError – If called on a return series (valuetype is
- ValueType.RTRN), since summation across sparser frequency would
- be required to avoid data loss.
Exponentially Weighted Moving Average Model for Volatility.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. -Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
months_from_last (int | None) – Number of months offset as positive integer. -Overrides use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series EWMA volatility.
-Series[float]
-Exponentially Weighted Moving Average Model for Value At Risk (VaR).
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. -Defaults to 11.
level (float) – The sought VaR level. Defaults to 0.95.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
months_from_last (int | None) – Number of months offset as positive integer. -Overrides use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series EWMA VaR.
-Series[float]
-Add or subtract a fee from the timeseries return.
- -Set the column labels of the .tsdf Pandas Dataframe.
- -Calculate autocorrelation function for specified lags.
-Series of autocorrelations indexed by lag.
-Series[float]
-Calculate partial autocorrelation at a given lag.
- -Calculate partial autocorrelation function for specified lags.
-Series of partial autocorrelations indexed by lag.
-Series[float]
-Compute Ljung-Box test for autocorrelation.
-Tuple of (statistic, pvalue, lags) where statistic is the Ljung-Box -Q statistic, pvalue is the chi-squared p-value, and lags is the -list of lags used.
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-The OpenTimeSeries class is the core component for analyzing individual financial time series. It provides comprehensive functionality for:
-Loading data from various sources (arrays, DataFrames, fixed rates)
Calculating financial metrics and risk measures
Performing time series transformations
Creating visualizations
Exporting results
Create series from a list of dates and a list of values.
-name (str) – String identifier of the timeseries and/or instrument.
dates (Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]) – List of date strings as ISO 8601 YYYY-MM-DD.
values (Annotated[list[float], MinLen(min_length=1)]) – Array of float values.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
timeseries_id (str) – Database identifier of the timeseries. Optional.
instrument_id (str) – Database identifier of the instrument associated -with the timeseries. Optional.
isin (str | None) – ISO 6166 identifier code of the associated instrument. Optional.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-Create series from a Pandas DataFrame or Series.
-dframe (Series | DataFrame | object) – Pandas DataFrame or Series.
column_nmbr (int) – Using iloc[:, column_nmbr] to pick column. Defaults to 0.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – ISO 4217 currency code of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-TypeError – If dframe is not a pandas.Series or a
- pandas.DataFrame.
Create series from values accruing with a given fixed rate return.
-Providing a date_range of type Pandas DatetimeIndex takes priority over -providing a combination of days and an end date.
-rate (float) – The accrual rate.
d_range (DatetimeIndex | None) – A given range of dates. Optional.
days (int | None) – Number of days to generate when date_range not provided. Must be -combined with end_dt. Optional.
end_dt (date | None) – End date of date range to generate when date_range not provided. -Must be combined with days. Optional.
label (str) – Placeholder for a name of the timeseries.
valuetype (ValueType) – Identifies if the series is a series of values or returns. -Defaults to ValueType.PRICE.
baseccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]) – The currency of the timeseries. Defaults to “SEK”.
local_ccy (bool) – Boolean flag indicating if timeseries is in local currency. -Defaults to True.
An OpenTimeSeries object.
-IncorrectArgumentComboError – If d_range is not provided and the
- combination of days and end_dt is incomplete.
The first date in the timeseries.
-The first date in the timeseries.
-The last date in the timeseries.
-The last date in the timeseries.
-Number of observations.
-Number of observations.
-Number of days from the first date to the last.
-Number of days from the first date to the last.
-Date when the maximum drawdown occurred.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Date when the maximum drawdown occurred
-The average number of observations per year.
-The average number of observations per year.
-Length of series in years assuming 365.25 days per year.
-Length of the timeseries in years assuming 365.25 days per year.
-self (Self)
properties (list[Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown_cal_year', 'max_drawdown', 'max_drawdown_date', 'first_idx', 'last_idx', 'length', 'span_of_days', 'yearfrac', 'periods_in_a_year', 'autocorr', 'partial_autocorr']] | None)
Annualized arithmetic mean of returns.
-Reference: https://www.investopedia.com/terms/a/arithmeticmean.asp.
-Annualized arithmetic mean of returns. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Compounded Annual Growth Rate (CAGR).
-Reference: https://www.investopedia.com/terms/c/cagr.asp.
-Compounded Annual Growth Rate (CAGR). -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Simple return.
-Simple return. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Annualized volatility.
-Based on Pandas .std() which is the equivalent of stdev.s([…]) in MS Excel.
-Reference: https://www.investopedia.com/terms/v/volatility.asp.
-Annualized volatility. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside Deviation.
-Standard deviation of returns that are below a Minimum Accepted Return -of zero. It is used to calculate the Sortino Ratio.
-Reference: https://www.investopedia.com/terms/d/downside-deviation.asp.
-Downside deviation. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Ratio of annualized arithmetic mean of returns and annualized volatility.
-Ratio of the annualized arithmetic mean of returns and annualized -volatility. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Sortino ratio.
-Reference: https://www.investopedia.com/terms/s/sortinoratio.asp.
-Sortino ratio calculated as the annualized arithmetic mean of returns -/ downside deviation. The ratio implies that the riskfree asset has zero -volatility, and a minimum acceptable return of zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Kappa-3 ratio.
-The Kappa-3 ratio is a generalized downside-risk ratio defined as -annualized arithmetic return divided by the cubic-root of the -lower partial moment of order 3 (with respect to a minimum acceptable -return, MAR). It penalizes larger downside outcomes more heavily than -the Sortino ratio (which uses order 2).
-Kappa-3 ratio calculation with the riskfree rate and. -Minimum Acceptable Return (MAR) both set to zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Omega ratio.
-Reference: https://en.wikipedia.org/wiki/Omega_ratio.
-Omega ratio calculation. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside 95% Value At Risk (VaR).
-The equivalent of percentile.inc([…], 1-level) over returns in MS Excel. -https://www.investopedia.com/terms/v/var.asp.
-Downside 95% Value At Risk (VaR). -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Downside 95% Conditional Value At Risk “CVaR”.
-Reference: https://www.investopedia.com/terms/c/conditional_value_at_risk.asp.
-Downside 95% Conditional Value At Risk “CVaR”. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Most negative percentage change.
-Most negative percentage change. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Most negative month.
-Most negative month. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Maximum drawdown without any limit on date range.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Maximum drawdown without any limit on date range. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Maximum drawdown in a single calendar year.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
-Maximum drawdown in a single calendar year. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-The share of percentage changes that are greater than zero.
-The share of percentage changes that are greater than zero. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Implied annualized volatility from Downside 95% Value at Risk.
-Assumes that returns are normally distributed.
-Implied annualized volatility from the Downside 95% VaR using the -assumption that returns are normally distributed. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Autocorrelation at lag 1.
-Shorthand for autocorr_func(lag=1). Returns the lag-1 autocorrelation
-of demeaned returns. For price series, returns are computed via
-pct_change; for return series, raw values are used after demeaning.
Autocorrelation at lag 1. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Skew of the return distribution.
-Reference: https://www.investopedia.com/terms/s/skewness.asp.
-Skew of the return distribution. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Kurtosis of the return distribution.
-Reference: https://www.investopedia.com/terms/k/kurtosis.asp.
-Kurtosis of the return distribution. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Z-score.
-Reference: https://www.investopedia.com/terms/z/zscore.asp.
-Z-score as (last return - mean return) / standard deviation of returns. -Returns float for OpenTimeSeries, Series[float] for OpenFrame.
-Populate .tsdf Pandas DataFrame from the .dates and .values lists.
- -Set the column labels of the .tsdf Pandas Dataframe.
- -Add or subtract a fee from the timeseries return.
- -Convert series of 1-day rates into series of cumulative values.
- -Align the index of .tsdf with local calendar business days.
countries (CountriesType | None) – Country code(s) (ISO 3166-1 alpha-2).
markets (list[str] | str | None) – Market code(s) supported by exchange_calendars.
custom_holidays (list[str] | str | None) – Missing holidays that should be added.
method (LiteralPandasReindexMethod) – Method for reindexing when aligning to business days.
self (Self)
The modified object.
-Self
-Resamples the timeseries frequency.
- -Resamples timeseries frequency to the business calendar month end dates.
-Stubs left in place. Stubs will be aligned to the shortest stub.
-An OpenTimeSeries object.
-ResampleDataLossError – If called on a return series (valuetype is
- ValueType.RTRN), since summation across sparser frequency would
- be required to avoid data loss.
Handle missing values in a value series.
-method (LiteralNanMethod) – Method used to handle NaN. Either "fill" (last known) or
-"drop".
self (Self)
The modified object.
-Self
-Handle missing values in a return series.
-method (LiteralNanMethod) – Method used to handle NaN. Either "fill" (zero) or
-"drop".
self (Self)
The modified object.
-Self
-Convert series of returns into cumulative series of values.
- -Convert series of values into series of returns.
- -Convert series of values to series of their period differences.
- -Convert value series to log-weighted series.
-Equivalent to LN(value[t] / value[t=0]) in Excel.
Autocorrelation analysis: autocorr (property) and autocorr_func provide
-lag-N autocorrelation; acf, pacf, partial_autocorr, and ljung_box
-support full autocorrelation diagnostics. Available on both OpenTimeSeries and
-OpenFrame (except acf, pacf, partial_autocorr, ljung_box which are
-OpenTimeSeries-only).
Calculate autocorrelation at a given lag.
-Computes the autocorrelation of demeaned returns at the specified lag.
-For price series (ValueType.PRICE), returns are derived via pct_change;
-for return series (ValueType.RTRN), raw values are demeaned. Use
-squared=True for squared-return autocorrelation (e.g. volatility
-clustering). Returns nan when the series has too few observations.
Autocorrelation at the specified lag. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Calculate autocorrelation function for specified lags.
-Series of autocorrelations indexed by lag.
-Series[float]
-Calculate partial autocorrelation at a given lag.
- -Calculate partial autocorrelation function for specified lags.
-Series of partial autocorrelations indexed by lag.
-Series[float]
-Compute Ljung-Box test for autocorrelation.
-Tuple of (statistic, pvalue, lags) where statistic is the Ljung-Box -Q statistic, pvalue is the chi-squared p-value, and lags is the -list of lags used.
-Exponentially Weighted Moving Average Model for Volatility.
-Reference: https://www.investopedia.com/articles/07/ewma.asp.
-lmbda (float) – Scaling factor to determine weighting. Defaults to 0.94.
day_chunk (int) – Sampling the data which is assumed to be daily. -Defaults to 11.
dlta_degr_freedms (int) – Variance bias factor taking the value 0 or 1. -Defaults to 0.
months_from_last (int | None) – Number of months offset as positive integer. -Overrides use of from_date and to_date. Optional.
from_date (dt.date | None) – Specific from date. Optional.
to_date (dt.date | None) – Specific to date. Optional.
periods_in_a_year_fixed (DaysInYearType | None) – Allows locking the periods-in-a-year to simplify -test cases and comparisons. Optional.
self (Self)
Series EWMA volatility.
-Series[float]
-Calculate simple return for a specific calendar period.
- -Calculate rolling returns.
- -Calculate rolling annualized volatilities.
-column (int) – Column position to calculate.
observations (int) – Number of observations in the overlapping window.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
dlta_degr_freedms (int) – Variance bias factor (0 or 1).
self (Self)
DataFrame with rolling annualized volatilities.
-DataFrame
-Calculate rolling annualized downside Value At Risk (VaR).
-DataFrame with rolling annualized downside VaR.
-DataFrame
-Calculate rolling annualized downside CVaR.
- -Create a user-defined date range aligned to index.
-A tuple (earlier, later) representing the start and end date of the
-chosen date range aligned to existing index values.
DateAlignmentError – If the implied range is outside series bounds.
-Detect outliers using z-score analysis.
-Identifies data points where the absolute z-score exceeds the threshold. -For OpenTimeSeries, returns a Series with dates and outlier values. For -OpenFrame, returns a DataFrame with dates and outlier values for each -column.
-threshold (float) – Z-score threshold; values with |z| > threshold are
-outliers.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (date | None) – Specific from date.
to_date (date | None) – Specific to date.
self (Self)
Series of outliers. For OpenFrame: DataFrame of -outliers. Empty if none found.
-For OpenTimeSeries
-Annualized arithmetic mean of returns.
-Reference: https://www.investopedia.com/terms/a/arithmeticmean.asp.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Annualized arithmetic mean of returns. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Compounded Annual Growth Rate (CAGR).
-Reference: https://www.investopedia.com/terms/c/cagr.asp.
CAGR. Float for OpenTimeSeries, Series[float] for OpenFrame.
InitialValueZeroError – If initial value is zero or there are negative - values.
-SeriesOrFloat_co
-Calculate simple return.
-Simple return. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
InitialValueZeroError – If initial value is zero.
-SeriesOrFloat_co
-Annualized volatility.
-Based on pandas.Series.std() (Excel STDEV.S equivalent).
-Reference: https://www.investopedia.com/terms/v/volatility.asp.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Annualized volatility. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Lower partial moment and downside deviation (order=2).
-If order is 2 calculates standard deviation of returns below MAR=0.
-For general order p, returns (LPM_p)^(1/p).
min_accepted_return (float) – Annualized Minimum Accepted Return (MAR).
order (Literal[2, 3]) – Order of partial moment (2 or 3).
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Downside deviation if order is 2; otherwise rooted lower partial
-moment. Float for OpenTimeSeries, Series[float] for OpenFrame.
ValueError – If order is not 2 or 3.
SeriesOrFloat_co
-Ratio between arithmetic mean of returns and annualized volatility.
-If riskfree_rate provided, computes the Sharpe ratio as
-(arithmetic return - risk-free) / volatility. Assumes zero volatility
-for the risk-free asset. Reference:
-https://www.investopedia.com/terms/s/sharperatio.asp.
riskfree_rate (float) – Return of the zero volatility asset.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Ratio value. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Sortino ratio or Kappa-3 ratio.
-Sortino: (return - riskfree_rate) / downside deviation using arithmetic
-mean of returns. Kappa-3 when order=3 penalizes larger downside more
-than Sortino.
riskfree_rate (float) – Return of the zero volatility asset.
min_accepted_return (float) – Annualized Minimum Accepted Return (MAR).
order (Literal[2, 3]) – Order of partial moment (2 for Sortino, 3 for Kappa-3).
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
self (Self)
Ratio value. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Omega Ratio.
-Compares returns above MAR to the total downside risk below MAR.
-Reference: https://en.wikipedia.org/wiki/Omega_ratio.
Omega ratio. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Downside Value At Risk (VaR).
-Equivalent to PERCENTILE.INC(returns, 1-level) in Excel. Reference:
-https://www.investopedia.com/terms/v/var.asp.
level (float) – The sought VaR level.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation used by DataFrame.quantile.
self (Self)
Downside VaR. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Downside Conditional Value At Risk (CVaR).
-Reference: https://www.investopedia.com/terms/c/conditional_value_at_risk.asp.
Downside CVaR. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Most negative percentage change over a rolling window.
-Most negative percentage change. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Maximum drawdown without any limit on date range.
-Reference: https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp.
Maximum drawdown. Float for OpenTimeSeries, Series[float] for
-OpenFrame.
SeriesOrFloat_co
-Share of percentage changes greater than zero.
-Share of positive returns. Float for OpenTimeSeries, Series[float]
-for OpenFrame.
SeriesOrFloat_co
-Implied annualized volatility from downside VaR.
-Assumes normally distributed returns.
-level (float) – The sought VaR level.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation type used by DataFrame.quantile.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
drift_adjust (bool) – Adjustment to remove the bias implied by the average -return.
self (Self)
Implied annualized volatility. Float for OpenTimeSeries,
-Series[float] for OpenFrame.
SeriesOrFloat_co
-Skew of the return distribution.
-Reference: https://www.investopedia.com/terms/s/skewness.asp.
Skewness. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Kurtosis of the return distribution.
-Reference: https://www.investopedia.com/terms/k/kurtosis.asp.
Kurtosis. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Z-score of the last return.
-Computed as (last return - mean return) / std dev of returns.
-Reference: https://www.investopedia.com/terms/z/zscore.asp.
Z-score. Float for OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-Target weight from VaR.
-Computes a position weight multiplier from the ratio between a VaR implied -volatility and a given target volatility. Multiplier = 1.0 → target met.
-target_vol (float) – Target volatility.
level (float) – The sought VaR level.
min_leverage_local (float) – Minimum adjustment factor.
max_leverage_local (float) – Maximum adjustment factor.
months_from_last (int | None) – Number of months offset as positive integer. Overrides
-use of from_date and to_date.
from_date (dt.date | None) – Specific from date.
to_date (dt.date | None) – Specific to date.
interpolation (LiteralQuantileInterp) – Interpolation type used by DataFrame.quantile.
periods_in_a_year_fixed (DaysInYearType | None) – Lock periods-in-a-year to simplify tests and -comparisons.
drift_adjust (bool) – Adjustment to remove the bias implied by the average -return.
self (Self)
Weight multiplier (or implied volatility if used downstream). Float for
-OpenTimeSeries, Series[float] for OpenFrame.
SeriesOrFloat_co
-The plotting methods generate fully responsive HTML output that automatically adapts to different screen sizes and device orientations. Plots are optimized for both desktop and mobile viewing with separate title containers and responsive CSS styling.
-Create a Plotly Scatter Figure.
-mode (LiteralLinePlotMode) – The type of scatter to use.
title (str | None) – A title above the plot.
tick_fmt (str | None) – Tick format for the y-axis, e.g. '%' or '.1%'.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
show_last (bool) – If True, highlight the last point in red with a label.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Create a Plotly Bar Figure.
-mode (LiteralBarPlotMode) – The type of bar to use.
title (str | None) – A title above the plot.
tick_fmt (str | None) – Tick format for the y-axis, e.g. '%' or '.1%'.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Create a Plotly Histogram Figure.
-plot_type (LiteralPlotlyHistogramPlotType) – Type of plot, "bars" or "lines".
histnorm (LiteralPlotlyHistogramHistNorm) – Normalization mode.
barmode (LiteralPlotlyHistogramBarMode) – How bar traces are displayed relative to one another.
xbins_size (float | None) – Width of each bin along the x-axis in data units.
opacity (float) – Trace opacity between 0 and 1.
bargap (float) – Gap between bars of adjacent location coordinates.
bargroupgap (float) – Gap between bar groups at the same location coordinate.
curve_type (LiteralPlotlyHistogramCurveType) – Type of distribution curve to overlay on the histogram.
title (str | None) – A title above the plot.
x_fmt (str | None) – Tick format for the x-axis.
y_fmt (str | None) – Tick format for the y-axis.
filename (str | None) – Name of the Plotly HTML file.
directory (DirectoryPath | None) – Directory where the Plotly HTML file is saved.
labels (list[str] | None) – Labels to override the column names of self.tsdf.
output_type (LiteralPlotlyOutput) – Determines output type.
include_plotlyjs (LiteralPlotlyJSlib) – How the plotly.js library is included.
cumulative (bool) – Whether to compute a cumulative histogram.
show_rug (bool) – Whether to draw a rug plot alongside the distribution.
auto_open (bool) – Whether to open a browser window with the plot.
add_logo (bool) – If True, a Captor logo is added to the plot.
self (Self)
A tuple (figure, output) where output is either a div string or
-a file path.
Dump timeseries data into a JSON file.
- -Save .tsdf DataFrame to an Excel spreadsheet file.
The Excel file path.
-NameError – If filename does not end with .xlsx.
FileExistsError – If the file exists and overwrite is False.
Chain two timeseries together.
-The function assumes that the two series have at least one date in common.
-front (TypeOpenTimeSeries) – Earlier series to chain with.
back (TypeOpenTimeSeries) – Later series to chain with.
old_fee (float) – Fee to apply to earlier series. Defaults to 0.0.
An OpenTimeSeries object or a subclass thereof.
-TypeOpenTimeSeries
-The ReturnSimulation class.
-Bases: BaseModel
The class ReturnSimulation allows for simulating financial timeseries.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Total number of days to simulate.
trading_days_in_year (Annotated[int, Strict(strict=True), Ge(ge=1), Le(le=366)]) – Number of trading days used to annualize.
mean_annual_return (float) – Mean annual return of the distribution.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean annual standard deviation of the distribution.
dframe (DataFrame) – Pandas DataFrame object holding the resulting values.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point in time. -Defaults to 0.0.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
seed (int | None) – Seed for random process initiation.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Annualized arithmetic mean of returns.
-Annualized arithmetic mean of returns.
-Annualized volatility.
-Annualized volatility.
-Create a Normal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Normal distribution simulation.
-ReturnSimulation
-Create a Lognormal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Lognormal distribution simulation.
-ReturnSimulation
-Create a Geometric Brownian Motion simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Geometric Brownian Motion simulation.
-ReturnSimulation
-Create a Merton Jump-Diffusion model simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point -in time.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Merton Jump-Diffusion model simulation.
-ReturnSimulation
-Create a pandas.DataFrame from simulation(s).
-name (str) – Name label of the serie(s).
start (dt.date | None) – Date when the simulation starts.
end (dt.date | None) – Date when the simulation ends.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
self (Self)
The simulation(s) data.
-DataFrame
-Bases: BaseModel
The class ReturnSimulation allows for simulating financial timeseries.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Total number of days to simulate.
trading_days_in_year (Annotated[int, Strict(strict=True), Ge(ge=1), Le(le=366)]) – Number of trading days used to annualize.
mean_annual_return (float) – Mean annual return of the distribution.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean annual standard deviation of the distribution.
dframe (DataFrame) – Pandas DataFrame object holding the resulting values.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point in time. -Defaults to 0.0.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
seed (int | None) – Seed for random process initiation.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Annualized arithmetic mean of returns.
-Annualized arithmetic mean of returns.
-Annualized volatility.
-Annualized volatility.
-Create a Normal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Normal distribution simulation.
-ReturnSimulation
-Create a Lognormal distribution simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Lognormal distribution simulation.
-ReturnSimulation
-Create a Geometric Brownian Motion simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Geometric Brownian Motion simulation.
-ReturnSimulation
-Create a Merton Jump-Diffusion model simulation.
-number_of_sims (Annotated[int, Gt(gt=0)]) – Number of simulations to generate.
trading_days (Annotated[int, Gt(gt=0)]) – Number of trading days to simulate.
mean_annual_return (float) – Mean return.
mean_annual_vol (Annotated[float, Gt(gt=0)]) – Mean standard deviation.
jumps_lamda (Annotated[float, Ge(ge=0)]) – This is the probability of a jump happening at each point -in time.
jumps_sigma (Annotated[float, Ge(ge=0)]) – This is the volatility of the jump size. Defaults to 0.0.
jumps_mu (float) – This is the average jump size. Defaults to 0.0.
trading_days_in_year (Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]) – Number of trading days used to annualize. -Defaults to 252.
seed (int | None) – Seed for random process initiation.
randomizer (Generator | None) – Random process generator.
ar1_coef (float) – Lag-1 autoregressive coefficient in (-1, 1) to induce -autocorrelation. Defaults to 0.0 (i.i.d. returns).
Merton Jump-Diffusion model simulation.
-ReturnSimulation
-Create a pandas.DataFrame from simulation(s).
-name (str) – Name label of the serie(s).
start (dt.date | None) – Date when the simulation starts.
end (dt.date | None) – Date when the simulation ends.
countries (CountriesType) – (List of) country code(s) according to ISO 3166-1 alpha-2. -Defaults to “SE”.
markets (list[str] | str | None) – (List of) markets code(s) supported by exchange_calendars.
self (Self)
The simulation(s) data.
-DataFrame
-The ReturnSimulation class is used to create simulated financial time series for testing and analysis purposes. It provides methods to generate realistic return patterns based on statistical distributions.
-Declaring types used throughout the project.
-Bases: StrEnum
Enum types of OpenTimeSeries to identify the output.
-Bases: StrEnum
Enum types of OpenTimeSeries to identify the output.
-The ValueType enum identifies the type of values in a time series (prices, returns, etc.).
-Type variable.
-The preferred way to construct a type variable is via the dedicated -syntax for generic functions, classes, and type aliases:
-class Sequence[T]: # T is a TypeVar
- ...
-This syntax can also be used to create bound and constrained type -variables:
-# S is a TypeVar bound to str
-class StrSequence[S: str]:
- ...
-
-# A is a TypeVar constrained to str or bytes
-class StrOrBytesSequence[A: (str, bytes)]:
- ...
-Type variables can also have defaults:
----
-- class IntDefault[T = int]:
- -
…
-
However, if desired, reusable type variables can also be constructed -manually, like so:
-T = TypeVar('T') # Can be anything
-S = TypeVar('S', bound=str) # Can be any subtype of str
-A = TypeVar('A', str, bytes) # Must be exactly str or bytes
-D = TypeVar('D', default=int) # Defaults to int
-Type variables exist primarily for the benefit of static type -checkers. They serve as the parameters for generic types as well -as for generic function and type alias definitions.
-The variance of type variables is inferred by type checkers when they
-are created through the type parameter syntax and when
-infer_variance=True is passed. Manually created type variables may
-be explicitly marked covariant or contravariant by passing
-covariant=True or contravariant=True. By default, manually
-created type variables are invariant. See PEP 484 and PEP 695 for more
-details.
Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]
Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[set[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]], MinLen(min_length=1)]
Represent a union type
-E.g. for int | str
-Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)]
Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01])$, ascii_only=None)]
Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[list[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=None, to_lower=None, strict=True, min_length=10, max_length=10, pattern=^d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]d|3[01])$, ascii_only=None)]], MinLen(min_length=1)]
Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
- -Runtime representation of an annotated type.
-At its core ‘Annotated[t, dec1, dec2, …]’ is an alias for the type ‘t’ -with extra metadata. The alias behaves like a normal typing alias. -Instantiating is the same as instantiating the underlying type; binding -it to types is also the same.
-The metadata itself is stored in a ‘__metadata__’ attribute as a tuple.
-alias of Annotated[int, FieldInfo(annotation=NoneType, required=True, metadata=[Strict(strict=True), Ge(ge=1), Le(le=366)])]
Represent a union type
-E.g. for int | str
-alias of Literal[‘values’, ‘tsdf’]
alias of Literal[‘before’, ‘after’, ‘both’]
Represent a union type
-E.g. for int | str
-alias of Literal[‘outer’, ‘inner’]
alias of Literal[‘linear’, ‘lower’, ‘higher’, ‘midpoint’, ‘nearest’]
alias of Literal[‘B’, ‘BME’, ‘BQE’, ‘BYE’]
Represent a union type
-E.g. for int | str
-alias of Literal[‘fill’, ‘drop’]
alias of Literal[‘up’, ‘down’, ‘both’]
alias of Literal[‘stack’, ‘group’, ‘overlay’, ‘relative’]
alias of Literal[‘file’, ‘div’]
alias of Literal[True, False, ‘cdn’]
alias of Literal[‘bars’, ‘lines’]
alias of Literal[‘stack’, ‘group’, ‘overlay’, ‘relative’]
alias of Literal[‘normal’, ‘kde’]
alias of Literal[‘percent’, ‘probability’, ‘density’, ‘probability density’]
alias of Literal[‘eq_weights’, ‘inv_vol’, ‘max_div’, ‘min_vol_overweight’]
alias of Literal[‘SLSQP’, ‘Nelder-Mead’, ‘Powell’, ‘CG’, ‘BFGS’, ‘Newton-CG’, ‘L-BFGS-B’, ‘TNC’, ‘COBYLA’, ‘trust-constr’, ‘dogleg’, ‘trust-ncg’, ‘trust-exact’, ‘trust-krylov’]
alias of Literal[‘value_ret’, ‘geo_ret’, ‘arithmetic_ret’, ‘vol’, ‘downside_deviation’, ‘ret_vol_ratio’, ‘sortino_ratio’, ‘kappa3_ratio’, ‘z_score’, ‘skew’, ‘kurtosis’, ‘positive_share’, ‘var_down’, ‘cvar_down’, ‘vol_from_var’, ‘worst’, ‘worst_month’, ‘max_drawdown_cal_year’, ‘max_drawdown’, ‘max_drawdown_date’, ‘first_idx’, ‘last_idx’, ‘length’, ‘span_of_days’, ‘yearfrac’, ‘periods_in_a_year’, ‘autocorr’, ‘partial_autocorr’]
alias of Literal[‘value_ret’, ‘geo_ret’, ‘arithmetic_ret’, ‘autocorr’, ‘vol’, ‘downside_deviation’, ‘ret_vol_ratio’, ‘sortino_ratio’, ‘kappa3_ratio’, ‘z_score’, ‘skew’, ‘kurtosis’, ‘positive_share’, ‘var_down’, ‘cvar_down’, ‘vol_from_var’, ‘worst’, ‘worst_month’, ‘max_drawdown’, ‘max_drawdown_date’, ‘max_drawdown_cal_year’, ‘first_indices’, ‘last_indices’, ‘lengths_of_items’, ‘span_of_days_all’]
Bases: BaseModel
Declare Countries.
-countryinput (Annotated[set[Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)]], MinLen(min_length=1)] | Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=2, max_length=2, pattern=^[A-Z]{2}$, ascii_only=None)])
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Bases: BaseModel
Declare Currency.
-ccy (Annotated[str, StringConstraints(strip_whitespace=True, to_upper=True, to_lower=None, strict=True, min_length=3, max_length=3, pattern=^[A-Z]{3}$, ascii_only=None)])
-Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
-Base class for allowed property arguments definition.
-Bases: PropertiesList
Allowed property arguments for the OpenTimeSeries class.
-args (LiteralSeriesProps)
-Property arguments for the OpenTimeSeries class.
-self (Self)
args (Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown_cal_year', 'max_drawdown', 'max_drawdown_date', 'first_idx', 'last_idx', 'length', 'span_of_days', 'yearfrac', 'periods_in_a_year', 'autocorr', 'partial_autocorr'])
None
-Bases: PropertiesList
Allowed property arguments for the OpenFrame class.
-args (LiteralFrameProps)
-Property arguments for the OpenFrame class.
-self (Self)
args (Literal['value_ret', 'geo_ret', 'arithmetic_ret', 'autocorr', 'vol', 'downside_deviation', 'ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'z_score', 'skew', 'kurtosis', 'positive_share', 'var_down', 'cvar_down', 'vol_from_var', 'worst', 'worst_month', 'max_drawdown', 'max_drawdown_date', 'max_drawdown_cal_year', 'first_indices', 'last_indices', 'lengths_of_items', 'span_of_days_all'])
None
-Bases: Exception
Raised when provided timeseries valuetypes are not the same.
-Bases: Exception
Raised when none of the possible frame inputs is provided.
-Bases: Exception
Raised when date input is not aligned with existing range.
-Bases: Exception
Raised when number of labels is not matching the number of timeseries.
-Bases: Exception
Raised when a calculation cannot be performed due to initial value(s) zero.
-Bases: Exception
Raised when countries argument is not provided in correct format.
-Bases: Exception
Raised when markets argument is not provided in correct format.
-Bases: Exception
Raised when trading days argument is not above zero.
-Bases: Exception
Raised when both start and end dates are provided.
-Bases: Exception
Raised when no weights are provided to function where necessary.
-Bases: Exception
Raised when provided label names are not unique.
-Bases: Exception
Raised when ratio keyword not provided correctly.
-Bases: Exception
Raised when a merge resulted in an empty DataFrame.
-Bases: Exception
Raised when correct combination of arguments is not provided.
-For details on changes, please visit the GitHub Releases page.
-Stay updated on new releases:
-Watch the openseries repository for release notifications
-Monitor openseries on PyPI for new versions
-Track updates on conda-forge
-We welcome contributions to openseries! This guide will help you get started with contributing to the project.
-Fork the repository on GitHub
Clone your fork locally:
git clone https://github.com/yourusername/openseries.git
-cd openseries
-Create the development environment. This installs the pinned uv version
-(uv==0.11.21), syncs locked dev and docs dependencies from
-uv.lock, and installs pre-commit hooks:
make install
-On Windows:
-.\make.ps1 make
-Create a new branch for your feature or bug fix:
git checkout -b feature/your-feature-name
-Make your changes
Run tests to ensure everything works:
make test
-Run linting and type checking:
make lint
-Commit your changes:
git add .
-git commit -m "Add your descriptive commit message"
-Push to your fork:
git push origin feature/your-feature-name
-Create a pull request on GitHub
openseries uses several tools to maintain code quality:
-Ruff: For linting and code formatting
mypy: For static type checking
pre-commit: For automated checks before commits
The configuration for these tools is in pyproject.toml.
All new code should include proper type hints:
-def calculate_returns(prices: list[float]) -> list[float]:
- """Calculate simple returns from prices."""
- returns = []
- for i in range(1, len(prices)):
- ret = (prices[i] / prices[i-1]) - 1
- returns.append(ret)
- return returns
-Use Google-style docstrings for all public functions and classes:
-def calculate_sharpe_ratio(returns: list[float], risk_free_rate: float = 0.0) -> float:
- """Calculate the Sharpe ratio.
-
- Args:
- returns: List of periodic returns.
- risk_free_rate: Risk-free rate for the same period. Defaults to 0.0.
-
- Returns:
- The Sharpe ratio.
-
- Raises:
- ValueError: If returns list is empty.
-
- Example:
- >>> returns = [0.01, 0.02, -0.01, 0.03]
- >>> sharpe = calculate_sharpe_ratio(returns)
- >>> print(f"Sharpe ratio: {sharpe:.3f}")
- """
- if not returns:
- raise ValueError("Returns list cannot be empty")
-
- mean_return = sum(returns) / len(returns)
- std_dev = (sum((r - mean_return) ** 2 for r in returns) / len(returns)) ** 0.5
-
- if std_dev == 0:
- return 0.0
-
- return (mean_return - risk_free_rate) / std_dev
-Tests are located in the tests/ directory and use pytest:
tests/
-├── __init__.py
-├── test_series.py
-├── test_frame.py
-├── test_portfoliotools.py
-└── ...
-Write comprehensive tests for new functionality:
-import pytest
-import pandas as pd
-from pandas.testing import assert_frame_equal
-from openseries import OpenTimeSeries
-
-class TestOpenTimeSeries:
- """Test cases for OpenTimeSeries class."""
-
- def test_from_arrays_basic(self):
- """Test basic creation from arrays."""
- dates = ['2023-01-01', '2023-01-02', '2023-01-03']
- values = [100.0, 102.0, 99.0]
-
- series = OpenTimeSeries.from_arrays(dates=dates, values=values, name="Test")
-
- if series.label != "Test":
- msg = f"Expected name 'Test', got '{series.label}'"
- raise ValueError(msg)
- if series.length != 3:
- msg = f"Expected length 3, got {series.length}"
- raise ValueError(msg)
- if series.first_idx != pd.Timestamp('2023-01-01').date():
- msg = f"Expected first_idx 2023-01-01, got {series.first_idx}"
- raise ValueError(msg)
- if series.last_idx != pd.Timestamp('2023-01-03').date():
- msg = f"Expected last_idx 2023-01-03, got {series.last_idx}"
- raise ValueError(msg)
-
- def test_from_arrays_invalid_dates(self):
- """Test that invalid dates raise appropriate errors."""
- with pytest.raises(ValueError):
- OpenTimeSeries.from_arrays(
- dates=['invalid-date'],
- values=[100.0],
- name="Test"
- )
-
- def test_calculate_returns(self):
- """Test return calculation."""
- dates = ['2023-01-01', '2023-01-02', '2023-01-03']
- values = [100.0, 102.0, 99.0]
-
- series = OpenTimeSeries.from_arrays(dates=dates, values=values, name="Test")
- series.value_to_ret() # Modifies original
-
- expected_returns = [0.02, -0.0294117647] # Approximate
- actual_returns = series.values
-
- if len(actual_returns) != 2:
- msg = f"Expected 2 returns, got {len(actual_returns)}"
- raise ValueError(msg)
- # Use tolerance-based comparison
- if abs(actual_returns[0] - expected_returns[0]) >= 1e-6:
- msg = f"First return mismatch: {actual_returns[0]} vs {expected_returns[0]}"
- raise ValueError(msg)
- if abs(actual_returns[1] - expected_returns[1]) >= 1e-6:
- msg = f"Second return mismatch: {actual_returns[1]} vs {expected_returns[1]}"
- raise ValueError(msg)
-Run all tests:
-make test
-Run specific test files:
-pytest tests/test_series.py
-Run tests with coverage:
-pytest --cov=openseries tests/
-openseries maintains high test coverage (>99%). New code should include comprehensive tests:
-Test normal use cases
Test edge cases
Test error conditions
Test with different data types and sizes
All public APIs must be documented
Include examples in docstrings where helpful
Update relevant documentation files when adding features
Use clear, concise language
To build documentation locally:
-cd docs
-make html
-The built documentation will be in docs/_build/html/.
Fork and Branch: Create a feature branch from master
Develop: Make your changes with tests and documentation
Test: Ensure all tests pass and coverage remains high
Lint: Run linting and fix any issues
Document: Update documentation as needed
Commit: Use clear, descriptive commit messages
Pull Request: Create a PR with a clear description
Use clear, descriptive commit messages:
-Add support for custom business day calendars
-
-- Implement custom calendar functionality in datefixer module
-- Add tests for various calendar configurations
-- Update documentation with examples
-- Fixes #123
-All contributions go through code review:
-Automated checks must pass (tests, linting, type checking)
At least one maintainer review is required
Address any feedback or requested changes
Once approved, the PR will be merged
When reporting bugs, please include:
-Clear description of the issue
Steps to reproduce
Expected vs. actual behavior
Environment details (Python version, OS, etc.)
Minimal code example if possible
For new features:
-Describe the use case and motivation
Provide examples of how it would be used
Consider backward compatibility
Discuss implementation approach if you have ideas
Areas where contributions are especially welcome:
-New financial metrics: Additional risk measures, performance ratios
Data sources: Integration with new data providers
Visualization: Enhanced plotting capabilities
Performance: Optimization of calculations
Documentation: Examples, tutorials, API documentation
Documentation improvements are always welcome:
-Fix typos or unclear explanations
Add examples to existing documentation
Create new tutorials or guides
Improve API documentation
For VS Code, recommended extensions:
-Python
Pylance
Ruff
mypy
Recommended settings in .vscode/settings.json:
{
- "python.defaultInterpreterPath": "venv/bin/python",
- "python.linting.enabled": true,
- "python.linting.ruffEnabled": true,
- "python.formatting.provider": "ruff",
- "python.typeChecking": "strict"
-}
-For debugging tests:
-pytest --pdb tests/test_specific.py::test_function
-For debugging with VS Code, create .vscode/launch.json:
{
- "version": "0.2.0",
- "configurations": [
- {
- "name": "Python: Current File",
- "type": "python",
- "request": "launch",
- "program": "${file}",
- "console": "integratedTerminal"
- },
- {
- "name": "Python: Pytest",
- "type": "python",
- "request": "launch",
- "module": "pytest",
- "args": ["${workspaceFolder}/tests"],
- "console": "integratedTerminal"
- }
- ]
-}
-openseries follows semantic versioning (MAJOR.MINOR.PATCH):
-MAJOR: Breaking changes
MINOR: New features, backward compatible
PATCH: Bug fixes, backward compatible
Releases are managed by maintainers and include:
-Version bump in pyproject.toml
Update CHANGELOG.md
Create GitHub release with release notes
Publish to PyPI and conda-forge
If you need help with contributing:
-Check existing issues and discussions on GitHub
Ask questions in GitHub Discussions
Reach out to maintainers
openseries is committed to providing a welcoming and inclusive environment:
-Be respectful and constructive in all interactions
Focus on what is best for the community
Show empathy towards other community members
Welcome newcomers and help them get started
Thank you for contributing to openseries!
-This example demonstrates how to create analysis reports using openseries and the built-in report functionality.
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame, report_html
-import pandas as pd
-
-# Load sample data for comparison
-tickers = ["AAPL", "MSFT", "GOOGL", "SPY"]
-names = ["Apple", "Microsoft", "Google", "S&P 500"]
-
-series_list = []
-for ticker, name in zip(tickers, names):
- data = yf.Ticker(ticker).history(period="3y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=name)
- series_list.append(series)
-
-# Create frame for report
-comparison_frame = OpenFrame(constituents=series_list)
-
-# Generate HTML report
-# The last asset in the frame is used as the benchmark
-figure, filepath = report_html(
- data=comparison_frame,
- output_type="file",
- filename="stock_comparison_report.html"
-)
-
-# filepath contains the path to the saved HTML file
-print(f"Report saved to: {filepath}")
-
-# The figure object can be used for further customization if needed
-# figure.show() # Display the figure interactively
-When you need to embed a report in an existing HTML page, use output_type="div":
# Generate HTML div section for embedding
-figure, html_div = report_html(
- data=comparison_frame,
- output_type="div"
-)
-
-# html_div contains the responsive HTML div section
-# that can be embedded in your existing HTML page
-# It includes both desktop and mobile layouts with CSS and JavaScript
-
-# Example: Save to a custom HTML template
-html_template = f"""
-<!DOCTYPE html>
-<html>
-<head>
- <title>My Custom Report</title>
-</head>
-<body>
- <h1>Portfolio Analysis Report</h1>
- {html_div}
-</body>
-</html>
-"""
-
-with open("custom_report.html", "w", encoding="utf-8") as f:
- f.write(html_template)
-This example shows how to analyze multiple assets simultaneously using OpenFrame.
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame
-
-# Define asset universe
-assets = {
- "AAPL": "Apple Inc.",
- "GOOGL": "Alphabet Inc.",
- "MSFT": "Microsoft Corp.",
- "AMZN": "Amazon.com Inc.",
- "TSLA": "Tesla Inc.",
- "NVDA": "NVIDIA Corp.",
- "META": "Meta Platforms Inc.",
- "NFLX": "Netflix Inc."
-}
-
-# Download data for all assets
-series_list = []
-for ticker, name in assets.items():
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="3y")
- series = OpenTimeSeries.from_df(
- dframe=data['Close']
- )
- series.set_new_label(lvl_zero=name)
- series_list.append(series)
- print(f"Loaded {name}: {series.length} observations")
-
-# Create OpenFrame
-tech_stocks = OpenFrame(constituents=series_list)
-print(f"\nCreated frame with {tech_stocks.item_count} assets")
-print(f"Common period: {tech_stocks.first_idx} to {tech_stocks.last_idx}")
-# Get metrics for all assets
-all_metrics = tech_stocks.all_properties()
-print("=== COMPARATIVE METRICS ===")
-print(all_metrics)
-
-# Focus on key metrics
-key_metrics = all_metrics.loc[['Geometric return', 'Volatility', 'Return vol ratio', 'Max drawdown']]
-key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
-
-# Convert to percentages for better readability
-percentage_metrics = key_metrics.copy()
-percentage_metrics.loc[['Annual Return', 'Volatility', 'Max Drawdown']] *= 100
-
-print("\n=== KEY METRICS COMPARISON ===")
-print(percentage_metrics.round(2))
-# Rank assets by different criteria using openseries metrics
-# Get key metrics for ranking
-returns = all_metrics.loc['Geometric return']
-volatilities = all_metrics.loc['Volatility']
-sharpe_ratios = all_metrics.loc['Return vol ratio']
-drawdowns = all_metrics.loc['Max drawdown']
-
-print("\n=== ASSET RANKINGS ===")
-print("Ranked by Return (highest first):")
-for i, (asset, ret) in enumerate(returns.sort_values(ascending=False).items(), 1):
- print(f" {i}. {asset}: {ret:.2%}")
-
-print("\nRanked by Volatility (lowest first):")
-for i, (asset, vol) in enumerate(volatilities.sort_values(ascending=True).items(), 1):
- print(f" {i}. {asset}: {vol:.2%}")
-
-print("\nRanked by Sharpe Ratio (highest first):")
-for i, (asset, sharpe) in enumerate(sharpe_ratios.sort_values(ascending=False).items(), 1):
- print(f" {i}. {asset}: {sharpe:.2f}")
-
-print("\nRanked by Max Drawdown (least negative first):")
-for i, (asset, dd) in enumerate(drawdowns.sort_values(ascending=False).items(), 1):
- print(f" {i}. {asset}: {dd:.2%}")
-# Calculate correlation matrix
-correlation_matrix = tech_stocks.correl_matrix
-print("\n=== CORRELATION MATRIX ===")
-print(correlation_matrix.round(3))
-
-# Find most and least correlated pairs
-corr_pairs = []
-for i in range(len(correlation_matrix.columns)):
- for j in range(i+1, len(correlation_matrix.columns)):
- asset1 = correlation_matrix.columns[i]
- asset2 = correlation_matrix.columns[j]
- corr = correlation_matrix.iloc[i, j]
- corr_pairs.append((asset1, asset2, corr))
-
-# Sort by correlation
-corr_pairs.sort(key=lambda x: x[2], reverse=True)
-
-print("\n=== HIGHEST CORRELATIONS ===")
-for asset1, asset2, corr in corr_pairs[:5]:
- print(f"{asset1} - {asset2}: {corr:.3f}")
-
-print("\n=== LOWEST CORRELATIONS ===")
-for asset1, asset2, corr in corr_pairs[-5:]:
- print(f"{asset1} - {asset2}: {corr:.3f}")
-# Analyze risk-return using openseries metrics
-returns = all_metrics.loc['Geometric return']
-volatilities = all_metrics.loc['Volatility']
-sharpe_ratios = all_metrics.loc['Return vol ratio']
-
-print("\n=== RISK-RETURN ANALYSIS ===")
-for asset in returns.index:
- ret_pct = returns[asset] * 100
- vol_pct = volatilities[asset] * 100
- sharpe = sharpe_ratios[asset]
- print(f"{asset}: Return={ret_pct:.2f}%, Volatility={vol_pct:.2f}%, Sharpe={sharpe:.2f}")
-
-# Identify efficient assets (high return per unit risk)
-# Calculate 75th percentile threshold manually
-sorted_sharpes = sorted(sharpe_ratios.values, reverse=True)
-threshold_idx = int(len(sorted_sharpes) * 0.25)
-efficient_threshold = sorted_sharpes[threshold_idx] if threshold_idx < len(sorted_sharpes) else sorted_sharpes[-1]
-
-print(f"\n=== MOST EFFICIENT ASSETS (Sharpe >= {efficient_threshold:.2f}) ===")
-for asset, sharpe in sharpe_ratios.items():
- if sharpe >= efficient_threshold:
- print(f"{asset}: {sharpe:.2f}")
-# Group assets by characteristics (example grouping)
-asset_groups = {
- 'Mega Cap': ['Apple Inc.', 'Microsoft Corp.', 'Alphabet Inc.', 'Amazon.com Inc.'],
- 'Growth': ['Tesla Inc.', 'NVIDIA Corp.', 'Netflix Inc.'],
- 'Social Media': ['Meta Platforms Inc.']
-}
-
-print("\n=== GROUP ANALYSIS ===")
-for group_name, group_assets in asset_groups.items():
- # Filter assets that exist in our data
- group_series = [s for s in tech_stocks.constituents if s.label in group_assets]
-
- if group_series:
- group_frame = OpenFrame(constituents=group_series)
- group_metrics = group_frame.all_properties()
-
- avg_return = group_metrics.loc['Geometric return'].mean()
- avg_vol = group_metrics.loc['Volatility'].mean()
- avg_sharpe = group_metrics.loc['Return vol ratio'].mean()
-
- print(f"\n{group_name} ({len(group_series)} assets):")
- print(f" Average Return: {avg_return:.2%}")
- print(f" Average Volatility: {avg_vol:.2%}")
- print(f" Average Sharpe: {avg_sharpe:.2f}")
-# Rolling correlation analysis
-# Pick two assets for detailed analysis
-apple = next(s for s in tech_stocks.constituents if "Apple" in s.label)
-microsoft = next(s for s in tech_stocks.constituents if "Microsoft" in s.label)
-
-pair_frame = OpenFrame(constituents=[apple, microsoft])
-rolling_corr = pair_frame.rolling_corr(observations=252) # 1-year rolling
-
-print(f"\n=== ROLLING CORRELATION: {apple.label} vs {microsoft.label} ===")
-print(f"Current correlation: {rolling_corr.iloc[-1, 0]:.3f}")
-print(f"Average correlation: {rolling_corr.mean().iloc[0]:.3f}")
-print(f"Correlation range: {rolling_corr.min().iloc[0]:.3f} to {rolling_corr.max().iloc[0]:.3f}")
-# Create equal-weighted portfolio for attribution
-portfolio_df = tech_stocks.make_portfolio(name="Tech Portfolio", weight_strat="eq_weights")
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"\n=== PORTFOLIO vs INDIVIDUAL ASSETS ===")
-print(f"Portfolio Return: {portfolio.geo_ret:.2%}")
-print(f"Portfolio Volatility: {portfolio.vol:.2%}")
-print(f"Portfolio Sharpe: {portfolio.ret_vol_ratio:.2f}")
-
-# Compare with individual assets using OpenFrame
-asset_metrics = tech_stocks.all_properties()
-individual_returns = asset_metrics.loc['Geometric return'].values
-individual_vols = asset_metrics.loc['Volatility'].values
-
-print(f"\nDiversification benefit:")
-equal_weights = [1/tech_stocks.item_count] * tech_stocks.item_count
-# Calculate weighted average manually
-weighted_avg_return = sum(ret * w for ret, w in zip(individual_returns, equal_weights))
-weighted_avg_vol = sum(vol * w for vol, w in zip(individual_vols, equal_weights))
-print(f" Weighted avg return: {weighted_avg_return:.2%}")
-print(f" Portfolio return: {portfolio.geo_ret:.2%}")
-print(f" Weighted avg volatility: {weighted_avg_vol:.2%}")
-print(f" Portfolio volatility: {portfolio.vol:.2%}")
-print(f" Volatility reduction: {(weighted_avg_vol - portfolio.vol):.2%}")
-# Identify worst market days (modifies original)
-market_proxy = tech_stocks.constituents[0] # Use first asset as market proxy
-market_proxy.value_to_ret()
-market_data = market_proxy.tsdf
-# Find worst 5% of days
-worst_threshold = market_data.quantile(0.05)
-worst_days = market_data[market_data <= worst_threshold]
-
-print(f"\n=== STRESS TEST ANALYSIS ===")
-print(f"Market stress threshold: {worst_threshold:.2%}")
-print(f"Number of stress days: {len(worst_days)}")
-
-# Analyze each asset's performance during stress
-print("\nAsset performance during market stress:")
-for series in tech_stocks.constituents:
- series.value_to_ret() # Modifies original
- asset_data = series.tsdf
- # Get returns on stress days
- stress_returns = asset_data.loc[worst_days.index]
- avg_stress_return = stress_returns.mean()
-
- print(f" {series.label}: {avg_stress_return:.2%}")
-# Export using openseries native methods
-# Export frame data
-tech_stocks.to_xlsx('multi_asset_analysis.xlsx')
-
-# Note: For comprehensive Excel export with multiple sheets,
-# you can use the DataFrame returned by all_properties() and correl_matrix
-# which are pandas DataFrames and support to_excel() method
-print("\nMulti-asset analysis exported to 'multi_asset_analysis.xlsx'")
-Here’s how to perform a complete multi-asset analysis using openseries methods directly:
-# Example: Analyze tech stocks using openseries methods
-tech_tickers = ["AAPL", "GOOGL", "MSFT", "AMZN", "TSLA"]
-
-# Load data using openseries methods
-series_list = []
-for ticker in tech_tickers:
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="3y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=ticker)
- series_list.append(series)
-
-if not series_list:
- print("No data loaded")
-else:
- # Create frame using openseries
- frame = OpenFrame(constituents=series_list)
-
- # Analysis using openseries properties and methods
- print(f"=== MULTI-ASSET ANALYSIS ===")
- print(f"Assets: {frame.item_count}")
- print(f"Period: {frame.first_idx} to {frame.last_idx}")
-
- # Key metrics using openseries all_properties method
- key_metrics = frame.all_properties(
- properties=['geo_ret', 'vol', 'ret_vol_ratio', 'max_drawdown']
- )
-
- print("\nKey Metrics:")
- print((key_metrics * 100).round(2)) # Convert to percentages
-
- # Correlations using openseries correl_matrix property
- correlations = frame.correl_matrix
- avg_correlation = correlations.mean().mean()
- print(f"\nAverage correlation: {avg_correlation:.3f}")
-
- # Create portfolio using openseries make_portfolio method
- portfolio_df = frame.make_portfolio(name="Equal Weight", weight_strat="eq_weights")
- portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
- print(f"\nEqual-weight portfolio:")
- print(f" Return: {portfolio.geo_ret:.2%}")
- print(f" Volatility: {portfolio.vol:.2%}")
- print(f" Sharpe: {portfolio.ret_vol_ratio:.2f}")
-This example demonstrates various portfolio optimization techniques using openseries, including both theoretical approaches and real-world applications with actual fund data.
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame
-from openseries import efficient_frontier, simulate_portfolios
-
-# Define investment universe
-universe = {
- "VTI": "Total Stock Market",
- "VEA": "Developed Markets",
- "VWO": "Emerging Markets",
- "BND": "Total Bond Market",
- "VNQ": "Real Estate",
- "VDE": "Energy",
- "VGT": "Technology",
- "VHT": "Healthcare"
-}
-
-# Load data
-assets = []
-for ticker, name in universe.items():
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="5y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=name)
- assets.append(series)
- print(f"Loaded {name}")
-
-# Create investment universe frame
-investment_universe = OpenFrame(constituents=assets)
-print(f"\nInvestment universe: {investment_universe.item_count} assets")
-print(f"Period: {investment_universe.first_idx} to {investment_universe.last_idx}")
-# Calculate efficient frontier
-# This may fail with various exceptions
-frontier_df, simulated_df, optimal_portfolio = efficient_frontier(
- eframe=investment_universe,
- num_ports=100,
- seed=42
-)
-
-print("=== EFFICIENT FRONTIER RESULTS ===")
-print(f"Generated {len(frontier_df)} efficient portfolios")
-print(f"Simulated {len(simulated_df)} random portfolios")
-
-# Find key portfolios
-returns = frontier_df['ret']
-volatilities = frontier_df['stdev']
-sharpe_ratios = returns / volatilities
-
-# Maximum Sharpe ratio portfolio
-max_sharpe_idx = sharpe_ratios.idxmax()
-max_sharpe_weights = optimal_portfolio[-len(investment_universe.constituents):]
-
-print(f"\n=== MAXIMUM SHARPE RATIO PORTFOLIO ===")
-print(f"Expected Return: {frontier_df.iloc[max_sharpe_idx]['ret']:.2%}")
-print(f"Volatility: {frontier_df.iloc[max_sharpe_idx]['stdev']:.2%}")
-print(f"Sharpe Ratio: {sharpe_ratios.iloc[max_sharpe_idx]:.2f}")
-
-print("\nOptimal Weights:")
-for i, weight in enumerate(max_sharpe_weights):
- asset_name = investment_universe.constituents[i].label
- if weight > 0.01: # Only show weights > 1%
- print(f" {asset_name}: {weight:.1%}")
-
-# Minimum volatility portfolio
-min_vol_idx = volatilities.idxmin()
-min_vol_weights = frontier_df.iloc[min_vol_idx][investment_universe.columns_lvl_zero].values
-
-print(f"\n=== MINIMUM VOLATILITY PORTFOLIO ===")
-print(f"Expected Return: {min_vol_row['ret']:.2%}")
-print(f"Volatility: {min_vol_row['stdev']:.2%}")
-print(f"Sharpe Ratio: {sharpe_ratios.iloc[min_vol_idx]:.2f}")
-
-print("\nMinimum Volatility Weights:")
-for col in investment_universe.columns_lvl_zero:
- weight = min_vol_row[col]
- if weight > 0.01:
- print(f" {col}: {weight:.1%}")
-# Generate random portfolios
-# This may fail with various exceptions
-simulation_results = simulate_portfolios(
- simframe=investment_universe,
- num_ports=50000,
- seed=42
-)
-
-print(f"\n=== MONTE CARLO SIMULATION ===")
-print(f"Simulated {len(simulation_results)} random portfolios")
-
-sim_returns = simulation_results['ret'].values
-sim_volatilities = simulation_results['stdev'].values
-sim_sharpe_ratios = sim_returns / sim_volatilities
-
-# Statistics of simulated portfolios
-print(f"\nSimulation Statistics:")
-print(f"Return range: {sim_returns.min():.2%} to {sim_returns.max():.2%}")
-print(f"Volatility range: {sim_volatilities.min():.2%} to {sim_volatilities.max():.2%}")
-print(f"Sharpe range: {sim_sharpe_ratios.min():.2f} to {sim_sharpe_ratios.max():.2f}")
-
-# Best portfolios from simulation
-sorted_indices = sorted(range(len(sim_sharpe_ratios)), key=lambda i: sim_sharpe_ratios.iloc[i], reverse=True)
-top_sharpe_indices = sorted_indices[:5]
-
-print(f"\n=== TOP 5 SIMULATED PORTFOLIOS ===")
-for i, idx in enumerate(reversed(top_sharpe_indices)):
- print(f"\nRank {i+1}:")
- print(f" Return: {sim_returns[idx]:.2%}")
- print(f" Volatility: {sim_volatilities[idx]:.2%}")
- print(f" Sharpe: {sim_sharpe_ratios[idx]:.2f}")
-
- weights = simulation_results.iloc[idx][investment_universe.columns_lvl_zero].values
- print(" Weights:")
- for j, weight in enumerate(weights):
- if weight > 0.05: # Only show weights > 5%
- asset_name = investment_universe.constituents[j].label
- print(f" {asset_name}: {weight:.1%}")
-# Equal weight portfolio using native weight_strat
-equal_weight_portfolio_df = investment_universe.make_portfolio(
- name="Equal Weight",
- weight_strat="eq_weights"
-)
-equal_weight_portfolio = OpenTimeSeries.from_df(dframe=equal_weight_portfolio_df)
-
-print(f"\n=== EQUAL WEIGHT PORTFOLIO ===")
-print(f"Return: {equal_weight_portfolio.geo_ret:.2%}")
-print(f"Volatility: {equal_weight_portfolio.vol:.2%}")
-print(f"Sharpe: {equal_weight_portfolio.ret_vol_ratio:.2f}")
-# Inverse volatility weighting using native weight_strat
-inv_vol_portfolio_df = investment_universe.make_portfolio(
- name="Inverse Volatility",
- weight_strat="inv_vol"
-)
-inv_vol_portfolio = OpenTimeSeries.from_df(dframe=inv_vol_portfolio_df)
-
-print(f"\n=== INVERSE VOLATILITY PORTFOLIO ===")
-print(f"Return: {inv_vol_portfolio.geo_ret:.2%}")
-print(f"Volatility: {inv_vol_portfolio.vol:.2%}")
-print(f"Sharpe: {inv_vol_portfolio.ret_vol_ratio:.2f}")
-The maximum diversification strategy aims to maximize portfolio diversification by optimizing the correlation structure. This strategy can encounter numerical issues in certain scenarios:
-# Maximum diversification portfolio using native weight_strat
-# This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError
-max_div_portfolio_df = investment_universe.make_portfolio(
- name="Maximum Diversification",
- weight_strat="max_div"
-)
-max_div_portfolio = OpenTimeSeries.from_df(dframe=max_div_portfolio_df)
-
-print(f"\n=== MAXIMUM DIVERSIFICATION PORTFOLIO ===")
-print(f"Return: {max_div_portfolio.geo_ret:.2%}")
-print(f"Volatility: {max_div_portfolio.vol:.2%}")
-print(f"Sharpe: {max_div_portfolio.ret_vol_ratio:.2f}")
-# Minimum volatility overweight portfolio using native weight_strat
-min_vol_portfolio_df = investment_universe.make_portfolio(
- name="Min Vol Overweight",
- weight_strat="min_vol_overweight"
-)
-min_vol_portfolio = OpenTimeSeries.from_df(dframe=min_vol_portfolio_df)
-
-print(f"\n=== MINIMUM VOLATILITY OVERWEIGHT PORTFOLIO ===")
-print(f"Return: {min_vol_portfolio.geo_ret:.2%}")
-print(f"Volatility: {min_vol_portfolio.vol:.2%}")
-print(f"Sharpe: {min_vol_portfolio.ret_vol_ratio:.2f}")
-# Compare all portfolio strategies
-portfolios = [
- equal_weight_portfolio,
- inv_vol_portfolio,
- max_div_portfolio,
- min_vol_portfolio
-]
-
-# Add optimized portfolios if available
-if 'max_sharpe_weights' in locals():
- investment_universe.weights = max_sharpe_weights.tolist()
- max_sharpe_portfolio_df = investment_universe.make_portfolio(
- name="Max Sharpe (Optimized)"
- )
- max_sharpe_portfolio = OpenTimeSeries.from_df(dframe=max_sharpe_portfolio_df)
- portfolios.append(max_sharpe_portfolio)
-
-if 'min_vol_weights' in locals():
- investment_universe.weights = min_vol_weights.tolist()
- min_vol_portfolio_df = investment_universe.make_portfolio(
- name="Min Vol (Optimized)"
- )
- min_vol_portfolio = OpenTimeSeries.from_df(dframe=min_vol_portfolio_df)
- portfolios.append(min_vol_portfolio)
-
-# Create comparison frame
-comparison_frame = OpenFrame(constituents=portfolios)
-comparison_metrics = comparison_frame.all_properties()
-
-# Display key metrics
-key_metrics = comparison_metrics.loc[['geo_ret', 'vol', 'ret_vol_ratio', 'max_drawdown']]
-key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
-
-print(f"\n=== PORTFOLIO STRATEGY COMPARISON ===")
-print((key_metrics * 100).round(2)) # Convert to percentages
-The openseries library provides several built-in weight strategies for portfolio construction:
-Assigns equal weight to all assets
Most robust strategy, always works
Good baseline for comparison
Weights assets inversely to their volatility
Lower volatility assets get higher weights
Generally stable and reliable
Optimizes correlation structure for maximum diversification
Can encounter numerical issues with certain data patterns
May produce negative weights in some scenarios
Raises MaxDiversificationNaNError for numerical issues
Raises MaxDiversificationNegativeWeightsError for negative weights
Overweights the least volatile asset (60% weight)
Distributes remaining 40% equally among other assets
Based on the low volatility anomaly
When using the maximum diversification strategy, it’s recommended to handle potential exceptions:
-from openseries.owntypes import (
- MaxDiversificationNaNError,
- MaxDiversificationNegativeWeightsError
-)
-
-# This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError
-portfolio_df = frame.make_portfolio(name="Max Div", weight_strat="max_div")
-# Define strategies to backtest using native weight_strat
-strategies = {
- 'Equal Weight': 'eq_weights',
- 'Inverse Volatility': 'inv_vol',
- 'Max Diversification': 'max_div',
- 'Min Vol Overweight': 'min_vol_overweight'
-}
-
-# Run backtest using native strategies
-backtest_results = {}
-for strategy_name, weight_strat in strategies.items():
- # This may fail with MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError, or other exceptions
- portfolio_df = investment_universe.make_portfolio(
- name=strategy_name,
- weight_strat=weight_strat
- )
- portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
- backtest_results[strategy_name] = {
- 'return': portfolio.geo_ret,
- 'volatility': portfolio.vol,
- 'sharpe': portfolio.ret_vol_ratio,
- 'max_drawdown': portfolio.max_drawdown,
- 'calmar': portfolio.geo_ret / abs(portfolio.max_drawdown) if portfolio.max_drawdown != 0 else float('nan')
- }
-
-print(f"\n=== BACKTEST RESULTS ===")
-for strategy_name, metrics in backtest_results.items():
- print(f"\n{strategy_name}:")
- print(f" Return: {metrics['return']:.4f}")
- print(f" Volatility: {metrics['volatility']:.4f}")
- print(f" Sharpe: {metrics['sharpe']:.4f}")
- print(f" Max Drawdown: {metrics['max_drawdown']:.4f}")
- print(f" Calmar: {metrics['calmar']:.4f}")
-
-# Rank strategies
-sorted_strategies = sorted(backtest_results.items(), key=lambda x: x[1]['sharpe'], reverse=True)
-best_strategy = sorted_strategies[0][0]
-
-print(f"\nBest performing strategy: {best_strategy}")
-print(f"Sharpe ratio: {sorted_strategies[0][1]['sharpe']:.3f}")
-# Export using openseries native methods
-# Export frame data
-investment_universe.to_xlsx('portfolio_optimization_results.xlsx')
-
-# Note: For comprehensive Excel export with multiple sheets,
-# the DataFrames returned by all_properties() and correl_matrix
-# are pandas DataFrames and support to_excel() method
-print("\nOptimization results exported to 'portfolio_optimization_results.xlsx'")
-This section demonstrates portfolio optimization using actual fund data from professional fund managers, showing how optimization techniques apply in practice.
-Here’s how to work with real fund data using openseries methods directly:
-from requests import get as requests_get
-from openseries import (
- OpenTimeSeries, OpenFrame, ValueType,
- efficient_frontier, prepare_plot_data, sharpeplot,
- load_plotly_dict, get_previous_business_day_before_today
-)
-
-# Define fund universe for optimization
-fund_universe_isins = [
- "SE0015243886", # Global High Yield
- "SE0011337195", # Global Equity
- "SE0011670843", # Global Bond
- "SE0017832280", # Alternative Strategy
- "SE0017832330", # Multi-Asset Strategy
-]
-
-# Load fund data using openseries methods
-response = requests_get(url="https://api.captor.se/public/api/nav", timeout=10)
-response.raise_for_status()
-
-series_list = []
-result = response.json()
-
-for data in result:
- if data["isin"] in fund_universe_isins:
- series = OpenTimeSeries.from_arrays(
- name=data["longName"],
- isin=data["isin"],
- baseccy=data["currency"],
- dates=data["dates"],
- values=data["navPerUnit"],
- valuetype=ValueType.PRICE,
- )
- series_list.append(series)
-
-# Create fund universe using openseries OpenFrame
-fund_universe = OpenFrame(constituents=series_list)
-
-# Process data using openseries methods
-fund_universe = fund_universe.value_nan_handle().trunc_frame().to_cumret()
-
-print(f"Fund universe created with {fund_universe.item_count} funds")
-print(f"Analysis period: {fund_universe.first_idx} to {fund_universe.last_idx}")
-# Set optimization parameters
-simulations = 10000
-frontier_points = 50
-seed = 55
-
-# Create current portfolio (equal weights)
-current_portfolio_df = fund_universe.make_portfolio(
- name="Current Portfolio",
- weight_strat="eq_weights",
-)
-current_portfolio = OpenTimeSeries.from_df(dframe=current_portfolio_df)
-
-# Calculate efficient frontier
-frontier, simulated_portfolios, optimal_portfolio = efficient_frontier(
- eframe=fund_universe,
- num_ports=simulations,
- seed=seed,
- frontier_points=frontier_points,
-)
-
-# Prepare visualization data
-plot_data = prepare_plot_data(
- assets=fund_universe,
- current=current_portfolio,
- optimized=optimal_portfolio,
-)
-
-# Load plotly configuration
-figdict, _ = load_plotly_dict()
-
-# Create efficient frontier plot
-optimization_plot, _ = sharpeplot(
- sim_frame=simulated_portfolios,
- line_frame=frontier,
- point_frame=plot_data,
- point_frame_mode="markers+text",
- title="Real Fund Portfolio Optimization",
- add_logo=False,
- auto_open=False,
- output_type="div",
-)
-optimization_plot = optimization_plot.update_layout(width=1200, height=700)
-
-# Display the optimization results
-optimization_plot.show(config=figdict["config"])
-# Compare different portfolio strategies
-strategies = {}
-
-# Equal weight portfolio
-equal_weight_portfolio_df = fund_universe.make_portfolio(
- name="Equal Weight", weight_strat="eq_weights"
-)
-equal_weight_portfolio = OpenTimeSeries.from_df(dframe=equal_weight_portfolio_df)
-strategies['Equal Weight'] = equal_weight_portfolio
-
-# Optimal portfolio from efficient frontier
-fund_universe.weights = optimal_portfolio[-fund_universe.item_count:].tolist()
-optimal_portfolio_df = fund_universe.make_portfolio(name="Optimal Portfolio")
-optimal_portfolio_series = OpenTimeSeries.from_df(dframe=optimal_portfolio_df)
-strategies['Optimal Portfolio'] = optimal_portfolio_series
-
-# Create comparison frame
-comparison_frame = OpenFrame(constituents=list(strategies.values()))
-comparison_metrics = comparison_frame.all_properties()
-
-# Display key metrics
-key_metrics = comparison_metrics.loc[['geo_ret', 'vol', 'ret_vol_ratio', 'max_drawdown']]
-key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
-
-print("=== PORTFOLIO STRATEGY COMPARISON ===")
-print((key_metrics * 100).round(2))
-
-# Calculate improvement metrics
-improvement = {
- 'Return Improvement': (optimal_portfolio_series.geo_ret - equal_weight_portfolio.geo_ret) * 100,
- 'Volatility Change': (optimal_portfolio_series.vol - equal_weight_portfolio.vol) * 100,
- 'Sharpe Improvement': optimal_portfolio_series.ret_vol_ratio - equal_weight_portfolio.ret_vol_ratio,
-}
-
-print("\n=== OPTIMIZATION IMPROVEMENTS ===")
-for metric, value in improvement.items():
- print(f"{metric}: {value:+.2f}")
-Here’s how to perform portfolio optimization using openseries methods directly:
-# Example: Optimize ETF portfolio using openseries methods
-etf_tickers = ["VTI", "VEA", "VWO", "BND", "VNQ"]
-
-# Load data using openseries methods
-assets = []
-for ticker in etf_tickers:
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="5y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=ticker)
- assets.append(series)
-
-if len(assets) < 2:
- print("Need at least 2 assets for optimization")
-else:
- frame = OpenFrame(constituents=assets)
-
- # Use openseries native weight strategies
- strategies = {
- 'Equal Weight': 'eq_weights',
- 'Inverse Volatility': 'inv_vol',
- 'Max Diversification': 'max_div',
- 'Min Vol Overweight': 'min_vol_overweight'
- }
-
- # Create portfolios using openseries make_portfolio method
- results = {}
- for name, weight_strat in strategies.items():
- # This may fail with MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError, or other exceptions
- portfolio_df = frame.make_portfolio(name=name, weight_strat=weight_strat)
- portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
- results[name] = {
- 'Return': portfolio.geo_ret,
- 'Volatility': portfolio.vol,
- 'Sharpe': portfolio.ret_vol_ratio,
- 'Max Drawdown': portfolio.max_drawdown
- }
-
- print("=== PORTFOLIO OPTIMIZATION RESULTS ===")
- for name, metrics in results.items():
- print(f"\n{name}:")
- print(f" Return: {metrics['Return']*100:.2f}%")
- print(f" Volatility: {metrics['Volatility']*100:.2f}%")
- print(f" Sharpe: {metrics['Sharpe']:.2f}")
- print(f" Max Drawdown: {metrics['Max Drawdown']*100:.2f}%")
-This example demonstrates comprehensive analysis of a single financial asset using openseries.
-import yfinance as yf
-from openseries import OpenTimeSeries
-import numpy as np
-
-# Download Apple stock data
-ticker = yf.Ticker("AAPL")
-data = ticker.history(period="5y")
-
-# Create OpenTimeSeries
-apple = OpenTimeSeries.from_df(
- dframe=data['Close']
-)
-
-# Set descriptive label
-apple.set_new_label(lvl_zero="Apple Inc. (AAPL)")
-
-print(f"Loaded {apple.length} observations")
-print(f"Date range: {apple.first_idx} to {apple.last_idx}")
-# Basic performance metrics
-print("=== PERFORMANCE METRICS ===")
-print(f"Total Return: {apple.value_ret:.2%}")
-print(f"Annualized Return: {apple.geo_ret:.2%}")
-print(f"Annualized Volatility: {apple.vol:.2%}")
-print(f"Sharpe Ratio: {apple.ret_vol_ratio:.2f}")
-
-# Get all metrics at once
-all_metrics = apple.all_properties()
-print("\n=== ALL METRICS ===")
-print(all_metrics)
-# Risk metrics
-print("=== RISK ANALYSIS ===")
-print(f"Maximum Drawdown: {apple.max_drawdown:.2%}")
-print(f"Max Drawdown Date: {apple.max_drawdown_date}")
-print(f"95% VaR (daily): {apple.var_down:.2%}")
-print(f"95% CVaR (daily): {apple.cvar_down:.2%}")
-print(f"Worst Single Day: {apple.worst:.2%}")
-print(f"Sortino Ratio: {apple.sortino_ratio:.2f}")
-# Convert to returns (modifies original)
-apple.value_to_ret()
-print(f"Returns series length: {apple.length}")
-
-# Create drawdown series (modifies original)
-apple.to_drawdown_series()
-
-# Convert to log returns (modifies original)
-apple.value_to_log()
-
-# Resample to monthly (modifies original)
-apple.resample_to_business_period_ends(freq="BME")
-print(f"Monthly data points: {apple.length}")
-# Rolling volatility (1-year window)
-rolling_vol = apple.rolling_vol(observations=252)
-print(f"Current 1Y volatility: {rolling_vol.iloc[-1, 0]:.2%}")
-print(f"Average 1Y volatility: {rolling_vol.mean().iloc[0]:.2%}")
-
-# Rolling returns (30-day)
-rolling_returns = apple.rolling_return(observations=30)
-
-# Rolling VaR
-rolling_var = apple.rolling_var_down(observations=252)
-# Plot price series
-fig, _ = apple.plot_series()
-
-# Plot returns histogram
-fig, _ = apple_returns.plot_histogram()
-
-# Plot drawdown series
-fig, _ = apple_drawdowns.plot_series()
-# Annual returns by calendar year
-years = [2019, 2020, 2021, 2022, 2023, 2024]
-
-print("=== CALENDAR YEAR RETURNS ===")
-for year in years:
- # This may fail if no data exists for the year
- year_return = apple.value_ret_calendar_period(year=year)
- print(f"{year}: {year_return:.2%}")
-# Export to Excel
-apple.to_xlsx("apple_analysis.xlsx")
-
-# Export metrics to CSV
-all_metrics.to_csv("apple_metrics.csv")
-
-# Export to JSON
-apple.to_json("apple_data.json")
-Here’s how to perform comprehensive single asset analysis using openseries methods directly:
-import yfinance as yf
-from openseries import OpenTimeSeries
-
-# Example: Analyze Apple stock using openseries methods
-ticker_symbol = "AAPL"
-
-# Download data using openseries methods
-ticker = yf.Ticker(ticker_symbol)
-data = ticker.history(period="5y")
-
-# Create series using openseries from_df method
-series = OpenTimeSeries.from_df(
- dframe=data['Close'],
- name=ticker_symbol
-)
-
-# Analysis using openseries properties and methods
-print(f"=== {ticker_symbol} ANALYSIS ===")
-print(f"Period: {series.first_idx} to {series.last_idx}")
-print(f"Observations: {series.length}")
-
-# Key metrics using openseries properties
-metrics = {
- 'Total Return': f"{series.value_ret:.2%}",
- 'Annual Return': f"{series.geo_ret:.2%}",
- 'Volatility': f"{series.vol:.2%}",
- 'Sharpe Ratio': f"{series.ret_vol_ratio:.2f}",
- 'Max Drawdown': f"{series.max_drawdown:.2%}",
- '95% VaR': f"{series.var_down:.2%}",
- 'Skewness': f"{series.skew:.2f}",
- 'Kurtosis': f"{series.kurtosis:.2f}"
-}
-
-for metric, value in metrics.items():
- print(f"{metric}: {value}")
-
-# Export results using openseries to_xlsx method
-filename = f"{ticker_symbol.lower()}_analysis.xlsx"
-series.to_xlsx(filename)
-print(f"\nResults exported to {filename}")
-
-# Example: Analyze multiple assets
-tickers = ["AAPL", "TSLA", "MSFT"]
-for ticker_symbol in tickers:
- ticker = yf.Ticker(ticker_symbol)
- data = ticker.history(period="2y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=ticker_symbol)
-
- print(f"\n{ticker_symbol}:")
- print(f" Return: {series.geo_ret:.2%}")
- print(f" Volatility: {series.vol:.2%}")
- print(f" Sharpe: {series.ret_vol_ratio:.2f}")
-openseries is a Python library for analyzing financial time series data. It provides tools to work with single assets or groups of assets, designed specifically for daily or less frequent data.
-The library is built around two main classes:
-OpenTimeSeries: For managing and analyzing individual time series
OpenFrame: For managing groups of time series and portfolio analysis
Financial Analysis: Comprehensive set of financial metrics and ratios
Risk Management: VaR, CVaR, drawdown analysis, and risk-adjusted returns
Portfolio Tools: Portfolio optimization, rebalancing, and performance attribution
Visualization: Interactive plots using Plotly
Data Handling: Robust date handling and business day calendars
Type Safety: Built with Pydantic for data validation and type safety
Install openseries using pip:
-pip install openseries
-Or using conda:
-conda install -c conda-forge openseries
-Here’s a simple example to get you started:
-from openseries import OpenTimeSeries
-import yfinance as yf
-
-# Download data
-ticker = yf.Ticker("^GSPC")
-history = ticker.history(period="5y")
-
-# Create OpenTimeSeries
-series = OpenTimeSeries.from_df(dframe=history.loc[:, "Close"])
-series.set_new_label(lvl_zero="S&P 500")
-
-# Calculate key metrics
-print(f"Annual Return: {series.geo_ret:.2%}")
-print(f"Volatility: {series.vol:.2%}")
-print(f"Sharpe Ratio: {series.ret_vol_ratio:.2f}")
-print(f"Max Drawdown: {series.max_drawdown:.2%}")
-
-# Create interactive plot
-series.plot_series()
-User Guide
-Tutorials
- -Examples
-Important Notes
- -API Reference
-Development
- -This tutorial covers advanced openseries features including custom analysis, integration with other libraries, and extending functionality.
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame
-
-# Load factor data (Fama-French factors would be ideal, using proxies here)
-factor_tickers = {
- "^GSPC": "Market",
- "^RUT": "Small Cap", # Size factor proxy
- "EFA": "International", # International factor
- "TLT": "Bonds" # Interest rate factor
-}
-
-# Load factor data
-factor_series = []
-for ticker, name in factor_tickers.items():
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="3y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=name)
- factor_series.append(series)
-
-# Create factor frame
-factors = OpenFrame(constituents=factor_series)
-
-# Load individual stock for analysis
-stock_data = yf.Ticker("AAPL").history(period="3y")
-apple = OpenTimeSeries.from_df(dframe=stock_data['Close'])
-apple.set_new_label(lvl_zero="Apple")
-
-# Add stock to factor frame for regression
-analysis_frame = OpenFrame(constituents=factor_series + [apple])
-
-# Perform multi-factor regression
-# This may fail with various exceptions
-regression_results = analysis_frame.multi_factor_linear_regression(
- dependent_variable_idx=-1 # Apple is the last series (dependent variable)
-)
-
-print("\n=== MULTI-FACTOR REGRESSION RESULTS ===")
-print("Regression Summary:")
-print(regression_results['summary'])
-
-print("\nFactor Loadings (Betas):")
-for i, factor_name in enumerate([s.label for s in factor_series]):
- beta = regression_results['coefficients'][i+1] # Skip intercept
- print(f" {factor_name}: {beta:.4f}")
-
-print(f"\nR-squared: {regression_results['r_squared']:.4f}")
-print(f"Adjusted R-squared: {regression_results['adj_r_squared']:.4f}")
-# Rolling beta analysis with market
-market_series = factor_series[0] # S&P 500
-stock_vs_market = OpenFrame(constituents=[apple, market_series])
-
-# Calculate rolling beta
-rolling_beta = stock_vs_market.rolling_beta(observations=252) # 1-year rolling
-
-print(f"\n=== ROLLING BETA ANALYSIS ===")
-print(f"Current Beta: {rolling_beta.iloc[-1, 0]:.3f}")
-print(f"Average Beta: {rolling_beta.mean().iloc[0]:.3f}")
-print(f"Beta Range: {rolling_beta.min().iloc[0]:.3f} to {rolling_beta.max().iloc[0]:.3f}")
-print(f"Beta Volatility: {rolling_beta.std().iloc[0]:.3f}")
-
-# Rolling correlation
-rolling_corr = stock_vs_market.rolling_corr(observations=252)
-
-print(f"\n=== ROLLING CORRELATION ANALYSIS ===")
-print(f"Current Correlation: {rolling_corr.iloc[-1, 0]:.3f}")
-print(f"Average Correlation: {rolling_corr.mean().iloc[0]:.3f}")
-print(f"Correlation Range: {rolling_corr.min().iloc[0]:.3f} to {rolling_corr.max().iloc[0]:.3f}")
-The export_plotly_figure function allows you to export any Plotly figure to a mobile-responsive HTML file. This is useful when you create custom visualizations using Plotly’s graph objects that aren’t directly available through openseries plotting methods.
You can create any Plotly figure and export it using the same responsive HTML format that openseries uses internally:
-import plotly.graph_objects as go
-from plotly.subplots import make_subplots
-from openseries import export_plotly_figure
-from pathlib import Path
-
-# Create a custom subplot figure
-fig = make_subplots(
- rows=2, cols=2,
- subplot_titles=('Price Chart', 'Volume', 'Returns Distribution', 'Drawdown'),
- specs=[[{"secondary_y": True}, {"type": "bar"}],
- [{"type": "histogram"}, {"type": "scatter"}]]
-)
-
-# Add traces (example data)
-fig.add_trace(
- go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], name="Price"),
- row=1, col=1
-)
-fig.add_trace(
- go.Bar(x=[1, 2, 3, 4], y=[100, 200, 150, 300], name="Volume"),
- row=1, col=2
-)
-fig.add_trace(
- go.Histogram(x=[0.01, -0.02, 0.015, -0.01, 0.02], name="Returns"),
- row=2, col=1
-)
-fig.add_trace(
- go.Scatter(x=[1, 2, 3, 4], y=[0, -0.05, -0.03, -0.08], name="Drawdown"),
- row=2, col=2
-)
-
-# Update layout
-fig.update_layout(height=800, title_text="Custom Multi-Panel Dashboard")
-
-# Export to responsive HTML
-output_path = export_plotly_figure(
- figure=fig,
- fig_config={"responsive": True},
- output_type="file",
- filename="custom_dashboard.html",
- include_plotlyjs="cdn",
- plotfile=Path("output/custom_dashboard.html"),
- title="Custom Financial Dashboard",
- auto_open=True,
-)
-
-print(f"Dashboard saved to: {output_path}")
-You can also use export_plotly_figure with Plotly Express figures:
import plotly.express as px
-import pandas as pd
-from openseries import export_plotly_figure
-from pathlib import Path
-
-# Create sample data
-df = pd.DataFrame({
- 'Date': pd.date_range('2020-01-01', periods=100),
- 'Asset_A': 100 + pd.Series(range(100)).cumsum() * 0.1,
- 'Asset_B': 100 + pd.Series(range(100)).cumsum() * 0.15,
-})
-
-# Create a Plotly Express figure
-fig = px.line(
- df, x='Date', y=['Asset_A', 'Asset_B'],
- title='Asset Comparison',
- labels={'value': 'Price', 'variable': 'Asset'}
-)
-
-# Export with responsive HTML
-export_plotly_figure(
- figure=fig,
- fig_config={"responsive": True, "displayModeBar": True},
- output_type="file",
- filename="asset_comparison.html",
- include_plotlyjs="cdn",
- plotfile=Path("output/asset_comparison.html"),
- title="Asset Price Comparison",
- auto_open=False,
-)
-For embedding in web applications or reports, you can generate inline HTML divs:
-import plotly.graph_objects as go
-from openseries import export_plotly_figure
-
-# Create a simple figure
-fig = go.Figure(data=go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13]))
-
-# Generate inline HTML div
-html_div = export_plotly_figure(
- figure=fig,
- fig_config={},
- output_type="div",
- filename="my_plot.html",
- include_plotlyjs="cdn",
- plotfile=Path("dummy.html"), # Ignored for div output
-)
-
-# html_div can now be embedded in HTML documents
-print(html_div[:100]) # Preview the HTML
-The export_plotly_figure function provides several advantages over Plotly’s default HTML export:
Mobile Responsive: Automatically adapts to different screen sizes and device orientations
Optimized Viewport: Proper viewport settings for mobile devices
Auto-Resize: JavaScript handles window resizing and orientation changes
Consistent Styling: Uses the same responsive CSS as openseries internal plots
Optional Title Container: Can include a title and logo in a responsive header
This makes it ideal for creating dashboards and reports that need to work well on both desktop and mobile devices.
-This tutorial demonstrates how to extend openseries with advanced functionality for sophisticated financial analysis workflows.
-This tutorial demonstrates how to perform fundamental financial analysis using openseries with real market data.
-First, let’s import the necessary libraries and download some data:
-import yfinance as yf
-import pandas as pd
-import numpy as np
-from openseries import OpenTimeSeries, OpenFrame
-from datetime import date, datetime
-
-# Download S&P 500 data for the last 5 years
-ticker = yf.Ticker("^GSPC")
-data = ticker.history(period="5y")
-
-# Create OpenTimeSeries
-sp500 = OpenTimeSeries.from_df(
- dframe=data['Close']
-)
-
-# Set a descriptive label
-sp500.set_new_label(lvl_zero="S&P 500 Index")
-
-print(f"Loaded {sp500.length} observations")
-print(f"Date range: {sp500.first_idx} to {sp500.last_idx}")
-Let’s calculate the fundamental performance metrics:
-# Total return over the period
-total_return = sp500.value_ret
-print(f"Total Return: {total_return:.2%}")
-
-# Annualized return (CAGR)
-annual_return = sp500.geo_ret
-print(f"Annualized Return (CAGR): {annual_return:.2%}")
-
-# Arithmetic mean return
-arithmetic_return = sp500.arithmetic_ret
-print(f"Arithmetic Mean Return: {arithmetic_return:.2%}")
-
-# Time period analysis
-print(f"Investment period: {sp500.yearfrac:.2f} years")
-print(f"Number of observations: {sp500.length}")
-print(f"Periods per year: {sp500.periods_in_a_year:.1f}")
-Now let’s examine the risk characteristics:
-# Volatility (annualized standard deviation)
-volatility = sp500.vol
-print(f"Annualized Volatility: {volatility:.2%}")
-
-# Downside deviation (volatility of negative returns only)
-downside_vol = sp500.downside_deviation
-print(f"Downside Deviation: {downside_vol:.2%}")
-
-# Value at Risk (95% confidence level)
-var_95 = sp500.var_down
-print(f"95% Value at Risk (daily): {var_95:.2%}")
-
-# Conditional Value at Risk (Expected Shortfall)
-cvar_95 = sp500.cvar_down
-print(f"95% CVaR (daily): {cvar_95:.2%}")
-
-# Maximum single-day loss
-worst_day = sp500.worst
-print(f"Worst single day: {worst_day:.2%}")
-Calculate risk-adjusted performance metrics:
-# Sharpe Ratio (return per unit of total risk)
-sharpe_ratio = sp500.ret_vol_ratio
-print(f"Sharpe Ratio: {sharpe_ratio:.2f}")
-
-# Sortino Ratio (return per unit of downside risk)
-sortino_ratio = sp500.sortino_ratio
-print(f"Sortino Ratio: {sortino_ratio:.2f}")
-
-# Kappa-3 Ratio (penalizes larger downside deviations more)
-kappa3_ratio = sp500.kappa3_ratio
-print(f"Kappa-3 Ratio: {kappa3_ratio:.2f}")
-
-# Omega Ratio
-omega_ratio = sp500.omega_ratio
-print(f"Omega Ratio: {omega_ratio:.2f}")
-Analyze drawdowns to understand downside risk:
-# Maximum drawdown
-max_drawdown = sp500.max_drawdown
-max_dd_date = sp500.max_drawdown_date
-print(f"Maximum Drawdown: {max_drawdown:.2%}")
-print(f"Max Drawdown Date: {max_dd_date}")
-
-# Create drawdown series for visualization (modifies original)
-sp500.to_drawdown_series()
-
-# Plot drawdowns
-sp500.plot_series()
-# This will open an interactive plot in your browser
-
-# Worst calendar year drawdown
-worst_year_dd = sp500.max_drawdown_cal_year
-print(f"Worst Calendar Year Drawdown: {worst_year_dd:.2%}")
-Examine the return distribution characteristics:
-# Convert to returns for distribution analysis (modifies original)
-sp500.value_to_ret()
-
-# Note: value_to_ret() modifies the original series in place
-# Restore the original series for further analysis
-sp500 = OpenTimeSeries.from_df(dframe=data['Close'])
-sp500.set_new_label(lvl_zero="S&P 500 Index")
-
-# Skewness (asymmetry of the distribution)
-skewness = sp500.skew
-print(f"Skewness: {skewness:.2f}")
-if skewness < 0:
- print(" → Negative skew: more extreme negative returns")
-elif skewness > 0:
- print(" → Positive skew: more extreme positive returns")
-
-# Kurtosis (tail heaviness)
-kurtosis = sp500.kurtosis
-print(f"Kurtosis: {kurtosis:.2f}")
-if kurtosis > 3:
- print(" → Fat tails: more extreme returns than normal distribution")
-
-# Percentage of positive days
-positive_share = sp500.positive_share
-print(f"Positive Days: {positive_share:.1%}")
-
-# Current Z-score (how unusual is the last return?)
-z_score = sp500.z_score
-print(f"Last Return Z-score: {z_score:.2f}")
-Break down performance by different time periods:
-# Resample to monthly data (modifies original)
-sp500.resample_to_business_period_ends(freq="BME")
-print(f"Monthly observations: {sp500.length}")
-
-# Monthly metrics
-monthly_return = sp500.geo_ret
-monthly_vol = sp500.vol
-print(f"Monthly Return (annualized): {monthly_return:.2%}")
-print(f"Monthly Volatility (annualized): {monthly_vol:.2%}")
-
-# Worst month
-worst_month = sp500.worst_month
-print(f"Worst Month: {worst_month:.2%}")
-
-# Annual data (modifies original)
-sp500.resample_to_business_period_ends(freq="BYE")
-print(f"Annual observations: {sp500.length}")
-# Calculate calendar year returns
-years = range(2019, 2025) # Adjust based on your data range
-
-for year in years:
- # This may fail if no data exists for the year
- year_return = sp500.value_ret_calendar_period(year=year)
- print(f"{year}: {year_return:.2%}")
-Analyze how metrics change over time:
-# 252-day (1-year) rolling volatility
-rolling_vol = sp500.rolling_vol(observations=252)
-print(f"Rolling volatility calculated for {len(rolling_vol)} periods")
-
-# 30-day rolling returns
-rolling_returns = sp500.rolling_return(observations=30)
-
-# Plot rolling volatility
-# Convert to OpenTimeSeries for plotting
-vol_dates = rolling_vol.index.strftime('%Y-%m-%d').tolist()
-vol_values = rolling_vol.iloc[:, 0].tolist()
-
-vol_series = OpenTimeSeries.from_arrays(
- dates=vol_dates,
- values=vol_values,
- name="Rolling Volatility"
-)
-
-vol_series.plot_series()
-Get all metrics at once:
-# Generate comprehensive metrics report
-all_metrics = sp500.all_properties()
-print("\n=== COMPREHENSIVE ANALYSIS REPORT ===")
-print(all_metrics)
-
-# Save to Excel for further analysis
-sp500.to_xlsx(filename="sp500_analysis.xlsx")
-all_metrics.to_excel(excel_writer="sp500_metrics.xlsx", engine="openpyxl")
-Create various visualizations:
-# Price chart
-sp500.plot_series()
-
-# Returns bar plot and histogram
-returns = sp500.from_deepcopy()
-returns.value_to_ret()
-returns.plot_bars()
-returns.plot_histogram()
-
-# Drawdown chart
-sp500.to_drawdown_series()
-sp500.plot_series()
-Let’s compare with a bond index:
-# Download bond data (10-year Treasury)
-bond_ticker = yf.Ticker("^TNX")
-bond_data = bond_ticker.history(period="5y")
-
-# Create bond series (using yield data)
-bonds = OpenTimeSeries.from_df(
- dframe=bond_data['Close']
-)
-bonds.set_new_label(lvl_zero="10Y Treasury Yield")
-
-# Create frame for comparison
-comparison_frame = OpenFrame(constituents=[sp500, bonds])
-
-# Compare metrics
-comparison_metrics = comparison_frame.all_properties()
-print("\n=== ASSET COMPARISON ===")
-print(comparison_metrics)
-
-# Calculate correlation
-correlation_matrix = comparison_frame.correl_matrix
-print("\n=== CORRELATION MATRIX ===")
-print(correlation_matrix)
-Calculate some advanced risk measures:
-# VaR at different confidence levels
-var_90 = sp500.var_down_func(level=0.90)
-var_95 = sp500.var_down_func(level=0.95)
-var_99 = sp500.var_down_func(level=0.99)
-
-print(f"90% VaR: {var_90:.2%}")
-print(f"95% VaR: {var_95:.2%}")
-print(f"99% VaR: {var_99:.2%}")
-
-# CVaR at different confidence levels
-cvar_90 = sp500.cvar_down_func(level=0.90)
-cvar_95 = sp500.cvar_down_func(level=0.95)
-cvar_99 = sp500.cvar_down_func(level=0.99)
-
-print(f"90% CVaR: {cvar_90:.2%}")
-print(f"95% CVaR: {cvar_95:.2%}")
-print(f"99% CVaR: {cvar_99:.2%}")
-
-# Implied volatility from VaR (assuming normal distribution)
-vol_from_var = sp500.vol_from_var
-print(f"Volatility implied from VaR: {vol_from_var:.2%}")
-print(f"Actual volatility: {sp500.vol:.2%}")
-print("\n=== INVESTMENT SUMMARY ===")
-print(f"Asset: {sp500.label}")
-print(f"Period: {sp500.first_idx} to {sp500.last_idx}")
-print(f"Total Return: {sp500.value_ret:.2%}")
-print(f"Annualized Return: {sp500.geo_ret:.2%}")
-print(f"Annualized Volatility: {sp500.vol:.2%}")
-print(f"Sharpe Ratio: {sp500.ret_vol_ratio:.2f}")
-print(f"Maximum Drawdown: {sp500.max_drawdown:.2%}")
-print(f"95% VaR (daily): {sp500.var_down:.2%}")
-
-# Risk assessment
-if sp500.ret_vol_ratio > 1.0:
- print("✓ Good risk-adjusted returns (Sharpe > 1.0)")
-else:
- print("⚠ Moderate risk-adjusted returns (Sharpe < 1.0)")
-
-if abs(sp500.max_drawdown) < 0.20:
- print("✓ Moderate maximum drawdown (< 20%)")
-else:
- print("⚠ Significant maximum drawdown (> 20%)")
-This tutorial provides a comprehensive foundation for financial analysis using openseries. You can adapt these techniques for any financial time series data.
-This tutorial demonstrates how to construct and analyze portfolios using openseries, including optimization techniques and performance attribution.
-Let’s start by downloading data for a diversified set of assets:
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame
-from openseries import efficient_frontier, simulate_portfolios
-
-# Define our universe of assets
-tickers = {
- "^GSPC": "S&P 500",
- "EFA": "EAFE International",
- "EEM": "Emerging Markets",
- "AGG": "US Aggregate Bonds",
- "VNQ": "US REITs",
- "GLD": "Gold",
- "DBC": "Commodities"
-}
-
-# Download 5 years of data
-series_list = []
-for ticker, name in tickers.items():
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="5y")
- series = OpenTimeSeries.from_df(
- dframe=data['Close']
- )
- series.set_new_label(lvl_zero=name)
- series_list.append(series)
- print(f"Loaded {name}: {series.length} observations")
-
-# Create OpenFrame
-assets = OpenFrame(constituents=series_list)
-print(f"\nCreated frame with {assets.item_count} assets")
-print(f"Common date range: {assets.first_idx} to {assets.last_idx}")
-First, let’s analyze the individual assets:
-# Get metrics for all assets
-asset_metrics = assets.all_properties()
-print("=== INDIVIDUAL ASSET METRICS ===")
-print(asset_metrics)
-
-# Key metrics comparison
-returns = asset_metrics.loc['Geometric return']
-volatilities = asset_metrics.loc['Volatility']
-sharpe_ratios = asset_metrics.loc['Return vol ratio']
-max_drawdowns = asset_metrics.loc['Max drawdown']
-
-print("\n=== ASSET COMPARISON ===")
-for asset in returns.index:
- print(f"{asset}:")
- print(f" Annual Return: {returns[asset]:.2%}")
- print(f" Volatility: {volatilities[asset]:.2%}")
- print(f" Sharpe Ratio: {sharpe_ratios[asset]:.2f}")
- print(f" Max Drawdown: {max_drawdowns[asset]:.2%}")
-Understanding correlations is crucial for portfolio construction:
-# Calculate correlation matrix
-correlation_matrix = assets.correl_matrix
-print("\n=== CORRELATION MATRIX ===")
-print(correlation_matrix.round(3))
-
-# Identify highly correlated pairs
-print("\n=== HIGHLY CORRELATED PAIRS (>0.7) ===")
-for i in range(len(correlation_matrix.columns)):
- for j in range(i+1, len(correlation_matrix.columns)):
- corr = correlation_matrix.iloc[i, j]
- if abs(corr) > 0.7:
- asset1 = correlation_matrix.columns[i]
- asset2 = correlation_matrix.columns[j]
- print(f"{asset1} - {asset2}: {corr:.3f}")
-
-# Average correlation with other assets
-avg_correlations = correlation_matrix.mean()
-print("\n=== AVERAGE CORRELATIONS ===")
-for asset, avg_corr in avg_correlations.items():
- print(f"{asset}: {avg_corr:.3f}")
-Let’s start with basic portfolio construction methods:
-# Create equal-weighted portfolio using native weight_strat
-portfolio_df = assets.make_portfolio(name="Equal Weight Portfolio", weight_strat="eq_weights")
-equal_weight_portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"Equal Weight Portfolio Return: {equal_weight_portfolio.geo_ret:.2%}")
-print(f"Equal Weight Portfolio Volatility: {equal_weight_portfolio.vol:.2%}")
-print(f"Equal Weight Portfolio Sharpe: {equal_weight_portfolio.ret_vol_ratio:.2f}")
-You can also specify custom weights for portfolio construction:
-# Define custom weights (must sum to 1)
-custom_weights = [0.50, 0.15, 0.10, 0.15, 0.05, 0.03, 0.02]
-
-assets.weights = custom_weights
-portfolio_df = assets.make_portfolio(name="Custom Weighted")
-custom_portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"Custom Portfolio Return: {custom_portfolio.geo_ret:.2%}")
-print(f"Custom Portfolio Volatility: {custom_portfolio.vol:.2%}")
-print(f"Custom Portfolio Sharpe: {custom_portfolio.ret_vol_ratio:.2f}")
-# Use native inverse volatility weighting (risk parity)
-portfolio_df = assets.make_portfolio(name="Risk Parity", weight_strat="inv_vol")
-risk_parity_portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"Risk Parity Portfolio Return: {risk_parity_portfolio.geo_ret:.2%}")
-print(f"Risk Parity Portfolio Volatility: {risk_parity_portfolio.vol:.2%}")
-print(f"Risk Parity Portfolio Sharpe: {risk_parity_portfolio.ret_vol_ratio:.2f}")
-OpenSeries provides additional weight strategies beyond basic equal weighting and risk parity:
-The maximum diversification strategy optimizes the correlation structure to maximize portfolio diversification:
-from openseries.owntypes import MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError
-
-# This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError
-max_div_portfolio_df = assets.make_portfolio(
- name="Maximum Diversification",
- weight_strat="max_div"
-)
-max_div_portfolio = OpenTimeSeries.from_df(dframe=max_div_portfolio_df)
-
-print(f"Max Diversification Return: {max_div_portfolio.geo_ret:.2%}")
-print(f"Max Diversification Volatility: {max_div_portfolio.vol:.2%}")
-print(f"Max Diversification Sharpe: {max_div_portfolio.ret_vol_ratio:.2f}")
-The minimum volatility overweight strategy overweights the least volatile asset:
-# This may fail with various exceptions
-min_vol_portfolio_df = assets.make_portfolio(
- name="Min Vol Overweight",
- weight_strat="min_vol_overweight"
-)
-min_vol_portfolio = OpenTimeSeries.from_df(dframe=min_vol_portfolio_df)
-
-print(f"Min Vol Overweight Return: {min_vol_portfolio.geo_ret:.2%}")
-print(f"Min Vol Overweight Volatility: {min_vol_portfolio.vol:.2%}")
-print(f"Min Vol Overweight Sharpe: {min_vol_portfolio.ret_vol_ratio:.2f}")
-When comparing multiple strategies, it’s important to handle potential failures gracefully:
-strategies = {
- 'Equal Weight': 'eq_weights',
- 'Risk Parity': 'inv_vol',
- 'Max Diversification': 'max_div',
- 'Min Vol Overweight': 'min_vol_overweight'
-}
-
-results = {}
-for name, strategy in strategies.items():
- # This may fail with MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError, or other exceptions
- portfolio_df = assets.make_portfolio(name=name, weight_strat=strategy)
- portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
- results[name] = {
- 'Return': portfolio.geo_ret,
- 'Volatility': portfolio.vol,
- 'Sharpe': portfolio.ret_vol_ratio
- }
-
-if results:
- print("\n=== STRATEGY COMPARISON ===")
- for strategy_name, metrics in results.items():
- print(f"\n{strategy_name}:")
- print(f" Return: {metrics['return']*100:.2f}%")
- print(f" Volatility: {metrics['volatility']*100:.2f}%")
- print(f" Sharpe: {metrics['sharpe']:.2f}")
- print(f" Max Drawdown: {metrics['max_drawdown']*100:.2f}%")
-Now let’s use openseries’ optimization tools:
-# Calculate efficient frontier
-# This may fail with various exceptions
-frontier_df, simulated_df, optimal_portfolio = efficient_frontier(
- eframe=assets,
- num_ports=50,
- seed=42
-)
-
-print("Efficient frontier calculated successfully")
-print(f"Number of frontier points: {len(frontier_df)}")
-print(f"Number of simulated portfolios: {len(simulated_df)}")
-
-# Find maximum Sharpe ratio portfolio
-sharpe_ratios = frontier_df['ret'] / frontier_df['stdev']
-max_sharpe_idx = sharpe_ratios.idxmax()
-
-print(f"\n=== MAXIMUM SHARPE RATIO PORTFOLIO ===")
-print(f"Expected Return: {frontier_df.iloc[max_sharpe_idx]['ret']:.2%}")
-print(f"Volatility: {frontier_df.iloc[max_sharpe_idx]['stdev']:.2%}")
-print(f"Sharpe Ratio: {sharpe_ratios.iloc[max_sharpe_idx]:.2f}")
-
-# Get optimal weights
-optimal_weights = optimal_portfolio[-len(assets.constituents):]
-print("\nOptimal Weights:")
-for i, weight in enumerate(optimal_weights):
- asset_name = assets.constituents[i].label
- print(f" {asset_name}: {weight:.1%}")
-# Simulate random portfolios
-# This may fail with various exceptions
-simulation_results = simulate_portfolios(
- simframe=assets,
- num_ports=10000,
- seed=42
-)
-
-print(f"\nSimulated {len(simulation_results)} random portfolios")
-
-# Find best performing portfolios
-sim_sharpe_ratios = simulation_results['ret'] / simulation_results['stdev']
-
-# Top 5 Sharpe ratios
-sorted_indices = sorted(range(len(sim_sharpe_ratios)), key=lambda i: sim_sharpe_ratios.iloc[i], reverse=True)
-top_indices = sorted_indices[:5]
-
-print("\n=== TOP 5 SIMULATED PORTFOLIOS ===")
-for i, idx in enumerate(top_indices, 1):
- print(f"\nRank {i}:")
- print(f" Return: {simulation_results.iloc[idx]['ret']:.2%}")
- print(f" Volatility: {simulation_results.iloc[idx]['stdev']:.2%}")
- print(f" Sharpe: {sim_sharpe_ratios.iloc[idx]:.2f}")
-Let’s compare all our portfolios:
-# Add all portfolios to a comparison frame
-portfolios = [equal_weight_portfolio, market_cap_portfolio, risk_parity_portfolio]
-
-# Add individual assets for comparison
-all_series = assets.constituents + portfolios
-comparison_frame = OpenFrame(constituents=all_series)
-
-# Get comprehensive metrics
-portfolio_metrics = comparison_frame.all_properties()
-
-# Focus on key metrics
-key_metrics = portfolio_metrics.loc[['Geometric return', 'Volatility', 'Return vol ratio', 'Max drawdown']]
-key_metrics.index = ['Annual Return', 'Volatility', 'Sharpe Ratio', 'Max Drawdown']
-
-print("\n=== PORTFOLIO COMPARISON ===")
-print((key_metrics * 100).round(2)) # Convert to percentages
-Analyze the risk contribution of each asset:
-# Calculate portfolio statistics using openseries methods
-# Create equal weight portfolio
-equal_weight_portfolio_df = assets.make_portfolio(name="Equal Weight", weight_strat="eq_weights")
-equal_weight_portfolio = OpenTimeSeries.from_df(dframe=equal_weight_portfolio_df)
-
-print("\n=== RISK ATTRIBUTION (Equal Weight Portfolio) ===")
-print(f"Portfolio Volatility: {equal_weight_portfolio.vol:.4f}")
-print(f"Portfolio Return: {equal_weight_portfolio.geo_ret:.4f}")
-
-# Individual asset contributions can be analyzed using openseries properties
-for i, series in enumerate(assets.constituents):
- weight = equal_weights[i]
- asset_vol = series.vol
- print(f"\n{series.label}:")
- print(f" Weight: {weight:.4f}")
- print(f" Individual Volatility: {asset_vol:.4f}")
- print(f" Weighted Contribution: {weight * asset_vol:.4f}")
-Analyze performance contribution over time:
-# Calculate performance attribution using openseries
-# Individual asset performance is available through openseries properties
-print("\n=== PERFORMANCE ATTRIBUTION ===")
-for i, series in enumerate(assets.constituents):
- weight = equal_weights[i]
- asset_return = series.geo_ret
- contribution = weight * asset_return
- print(f"{series.label}:")
- print(f" Weight: {weight:.2%}")
- print(f" Return: {asset_return:.2%}")
- print(f" Contribution: {contribution:.2%}")
-
-# Cumulative contribution
-cumulative_contrib = (1 + weighted_returns).cumprod()
-
-print("\n=== PERFORMANCE ATTRIBUTION ===")
-print("Final cumulative contribution by asset:")
-final_contrib = cumulative_contrib.iloc[-1]
-for asset, contrib in final_contrib.items():
- print(f" {asset}: {contrib:.3f}")
-Analyze how portfolio characteristics change over time:
-# Rolling correlation with market (S&P 500)
-market_proxy = assets.constituents[0] # Assuming first asset is S&P 500
-
-# Create frame with portfolio and market
-portfolio_vs_market = OpenFrame(constituents=[equal_weight_portfolio, market_proxy])
-
-# Calculate rolling correlation
-rolling_corr = portfolio_vs_market.rolling_corr(observations=252) # 1-year rolling
-
-print(f"\nRolling correlation calculated for {len(rolling_corr)} periods")
-print(f"Average correlation: {rolling_corr.mean().iloc[0]:.3f}")
-print(f"Correlation range: {rolling_corr.min().iloc[0]:.3f} to {rolling_corr.max().iloc[0]:.3f}")
-
-# Rolling portfolio volatility
-portfolio_rolling_vol = equal_weight_portfolio.rolling_vol(observations=252)
-
-print(f"\nRolling volatility statistics:")
-print(f"Average volatility: {portfolio_rolling_vol.mean().iloc[0]:.2%}")
-print(f"Volatility range: {portfolio_rolling_vol.min().iloc[0]:.2%} to {portfolio_rolling_vol.max().iloc[0]:.2%}")
-Analyze the impact of rebalancing frequency using the realistic rebalanced_portfolio method:
-# Compare different rebalancing frequencies using realistic simulation
-frequencies = [1, 21, 63] # Daily, monthly, quarterly
-frequency_names = ["Daily", "Monthly", "Quarterly"]
-
-rebalanced_portfolios = []
-
-for freq, name in zip(frequencies, frequency_names):
- portfolio = assets.rebalanced_portfolio(
- name=f"{name} Rebalanced",
- frequency=freq,
- bal_weights=equal_weights
- )
- rebalanced_portfolios.append(portfolio.constituents[-1])
-
-# Compare with theoretical portfolio
-assets.weights = equal_weights
-theoretical_portfolio_df = assets.make_portfolio(name="Theoretical")
-theoretical_portfolio = OpenTimeSeries.from_df(dframe=theoretical_portfolio_df)
-
-# Create comprehensive comparison
-all_portfolios = [theoretical_portfolio] + rebalanced_portfolios
-comparison_frame = OpenFrame(constituents=all_portfolios)
-comparison_metrics = comparison_frame.all_properties()
-
-print("\n=== REALISTIC REBALANCING COMPARISON ===")
-print("Strategy | Return | Volatility | Sharpe | Max DD")
-print("-" * 50)
-
-for series in all_portfolios:
- ret = comparison_metrics.loc['Geometric return', series.label].iloc[0] * 100
- vol = comparison_metrics.loc['Volatility', series.label].iloc[0] * 100
- sharpe = comparison_metrics.loc['Return vol ratio', series.label].iloc[0]
- max_dd = comparison_metrics.loc['Max drawdown', series.label].iloc[0] * 100
-
- print(f"{series.label:>15} | {ret:6.2f}% | {vol:10.2f}% | {sharpe:6.2f} | {max_dd:6.2f}%")
-
-# Analyze transaction costs
-print(f"\n=== TRANSACTION COST ANALYSIS ===")
-for freq, name in zip(frequencies, frequency_names):
- detailed_portfolio = assets.rebalanced_portfolio(
- name=f"{name} Detailed",
- frequency=freq,
- bal_weights=equal_weights,
- drop_extras=False # Get detailed trading data
- )
-
- # Count rebalancing events
- rebalancing_days = 0
- for series in detailed_portfolio.constituents:
- if "buysell_qty" in series.label:
- # Count days with non-zero trading
- trading_days = (series.tsdf != 0).any(axis=1).sum()
- rebalancing_days = max(rebalancing_days, trading_days)
-
- print(f"{name:>15}: {rebalancing_days} rebalancing events")
-Test portfolio performance during market stress:
-# Identify worst periods for the market (modifies original)
-market_proxy.value_to_ret()
-market_returns_df = market_proxy.tsdf
-
-# Find worst 5% of days
-worst_days_threshold = market_returns_df.quantile(0.05).iloc[0]
-worst_days = market_returns_df[market_returns_df <= worst_days_threshold]
-
-print(f"\n=== STRESS TEST RESULTS ===")
-print(f"Market stress threshold: {worst_days_threshold:.2%}")
-print(f"Number of stress days: {len(worst_days)}")
-
-# Portfolio performance during stress (modifies original)
-equal_weight_portfolio.value_to_ret()
-portfolio_returns_df = equal_weight_portfolio.tsdf
-
-# Align dates and calculate portfolio performance during market stress
-stress_dates = worst_days.index
-portfolio_stress_returns = portfolio_returns_df.loc[stress_dates]
-
-print(f"Portfolio average return during stress: {portfolio_stress_returns.mean().iloc[0]:.2%}")
-print(f"Portfolio worst day during stress: {portfolio_stress_returns.min().iloc[0]:.2%}")
-Generate a comprehensive portfolio analysis report:
-print("\n" + "="*60)
-print("PORTFOLIO ANALYSIS SUMMARY REPORT")
-print("="*60)
-
-print(f"\nAnalysis Period: {assets.first_idx} to {assets.last_idx}")
-print(f"Number of Assets: {assets.item_count}")
-print(f"Asset Universe: {', '.join([s.label for s in assets.constituents])}")
-
-print(f"\n--- EQUAL WEIGHT PORTFOLIO PERFORMANCE ---")
-print(f"Total Return: {equal_weight_portfolio.value_ret:.2%}")
-print(f"Annualized Return: {equal_weight_portfolio.geo_ret:.2%}")
-print(f"Annualized Volatility: {equal_weight_portfolio.vol:.2%}")
-print(f"Sharpe Ratio: {equal_weight_portfolio.ret_vol_ratio:.2f}")
-print(f"Maximum Drawdown: {equal_weight_portfolio.max_drawdown:.2%}")
-print(f"95% VaR (daily): {equal_weight_portfolio.var_down:.2%}")
-
-print(f"\n--- PORTFOLIO CHARACTERISTICS ---")
-avg_correlation = correlation_matrix.mean().mean()
-print(f"Average Asset Correlation: {avg_correlation:.3f}")
-print(f"Portfolio Diversification Benefit: {(asset_metrics.loc['Volatility'].mean() - equal_weight_portfolio.vol):.2%}")
-
-# Export results
-portfolio_metrics.to_excel("portfolio_analysis.xlsx")
-correlation_matrix.to_excel("correlation_matrix.xlsx")
-
-print(f"\nResults exported to Excel files")
-print("Analysis complete!")
-This tutorial provides a comprehensive framework for portfolio analysis using openseries. You can extend these techniques for more sophisticated portfolio management strategies.
-This tutorial demonstrates comprehensive risk management techniques using openseries, including VaR calculations, stress testing, and risk monitoring.
-Let’s start with a portfolio of assets for risk analysis:
-import yfinance as yf
-from openseries import OpenTimeSeries, OpenFrame
-from datetime import datetime, timedelta
-import warnings
-warnings.filterwarnings('ignore')
-
-# Download data for a mixed portfolio
-tickers = {
- "AAPL": "Apple Inc.",
- "GOOGL": "Alphabet Inc.",
- "MSFT": "Microsoft Corp.",
- "TSLA": "Tesla Inc.",
- "SPY": "SPDR S&P 500 ETF",
- "QQQ": "Invesco QQQ Trust",
- "TLT": "iShares 20+ Year Treasury",
- "GLD": "SPDR Gold Shares"
-}
-
-# Download 3 years of data
-series_list = []
-for ticker, name in tickers.items():
- # This may fail if the ticker is invalid or data unavailable
- data = yf.Ticker(ticker).history(period="3y")
- series = OpenTimeSeries.from_df(
- dframe=data['Close']
- )
- series.set_new_label(lvl_zero=name)
- series_list.append(series)
- print(f"Loaded {name}: {series.length} observations")
-
-# Create portfolio frame
-portfolio_assets = OpenFrame(constituents=series_list)
-
-# Create equal-weighted portfolio
-n_assets = portfolio_assets.item_count
-
-# Set weights on the frame first
-portfolio_df = portfolio_assets.make_portfolio(
- name="Diversified Portfolio",
- weight_strat="eq_weights"
-)
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"\nPortfolio created with {n_assets} assets")
-print(f"Date range: {portfolio.first_idx} to {portfolio.last_idx}")
-Start with fundamental risk measurements:
-print("=== BASIC RISK METRICS ===")
-
-# Volatility measures
-print(f"Annualized Volatility: {portfolio.vol:.2%}")
-print(f"Downside Deviation: {portfolio.downside_deviation:.2%}")
-
-# Return distribution
-print(f"Skewness: {portfolio.skew:.3f}")
-print(f"Kurtosis: {portfolio.kurtosis:.3f}")
-
-# Tail risk
-print(f"Worst Single Day: {portfolio.worst:.2%}")
-print(f"Worst Month: {portfolio.worst_month:.2%}")
-
-# Drawdown analysis
-print(f"Maximum Drawdown: {portfolio.max_drawdown:.2%}")
-print(f"Max Drawdown Date: {portfolio.max_drawdown_date}")
-Calculate VaR at different confidence levels:
-print("\n=== VALUE AT RISK ANALYSIS ===")
-
-# VaR at different confidence levels
-confidence_levels = [0.90, 0.95, 0.99]
-
-for level in confidence_levels:
- var_value = portfolio.var_down_func(level=level)
- print(f"{level*100:.0f}% VaR (daily): {var_value:.2%}")
-
-# Convert daily VaR to different time horizons
-# Assuming normal distribution and independence
-daily_var_95 = portfolio.var_down_func(level=0.95)
-
-print(f"\n=== VaR TIME HORIZONS (95% confidence) ===")
-print(f"1-day VaR: {daily_var_95:.2%}")
-# Scale VaR to different time horizons
-print(f"1-week VaR: {daily_var_95 * (5 ** 0.5):.2%}")
-print(f"1-month VaR: {daily_var_95 * (22 ** 0.5):.2%}")
-print(f"1-year VaR: {daily_var_95 * (252 ** 0.5):.2%}")
-Analyze expected shortfall beyond VaR:
-print("\n=== CONDITIONAL VALUE AT RISK (CVaR) ===")
-
-for level in confidence_levels:
- cvar_value = portfolio.cvar_down_func(level=level)
- var_value = portfolio.var_down_func(level=level)
-
- print(f"{level*100:.0f}% CVaR: {cvar_value:.2%} (VaR: {var_value:.2%})")
- print(f" Expected loss beyond VaR: {cvar_value - var_value:.2%}")
-Monitor how risk changes over time:
-# Calculate rolling risk metrics
-window = 252 # 1-year rolling window
-
-print(f"\n=== ROLLING RISK ANALYSIS ({window}-day window) ===")
-
-# Rolling volatility
-rolling_vol = portfolio.rolling_vol(observations=window)
-print(f"Rolling Volatility - Current: {rolling_vol.iloc[-1, 0]:.2%}")
-print(f"Rolling Volatility - Average: {rolling_vol.mean().iloc[0]:.2%}")
-print(f"Rolling Volatility - Range: {rolling_vol.min().iloc[0]:.2%} to {rolling_vol.max().iloc[0]:.2%}")
-
-# Rolling VaR
-rolling_var = portfolio.rolling_var_down(observations=window)
-print(f"Rolling VaR (95%) - Current: {rolling_var.iloc[-1, 0]:.2%}")
-print(f"Rolling VaR (95%) - Average: {rolling_var.mean().iloc[0]:.2%}")
-
-# Rolling CVaR
-rolling_cvar = portfolio.rolling_cvar_down(observations=window)
-print(f"Rolling CVaR (95%) - Current: {rolling_cvar.iloc[-1, 0]:.2%}")
-print(f"Rolling CVaR (95%) - Average: {rolling_cvar.mean().iloc[0]:.2%}")
-Test portfolio performance under extreme scenarios:
-print("\n=== HISTORICAL STRESS TESTING ===")
-
-# Convert to returns for analysis (modifies original)
-portfolio.value_to_ret()
-returns_data = portfolio.tsdf
-
-# Note: value_to_ret() modifies the original series in place
-# Restore the original portfolio for further analysis
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-# Identify worst periods
-worst_1_percent = returns_data.quantile(0.01).iloc[0]
-worst_5_percent = returns_data.quantile(0.05).iloc[0]
-
-print(f"Worst 1% threshold: {worst_1_percent:.2%}")
-print(f"Worst 5% threshold: {worst_5_percent:.2%}")
-
-# Count extreme events
-extreme_events_1pct = (returns_data <= worst_1_percent).sum().iloc[0]
-extreme_events_5pct = (returns_data <= worst_5_percent).sum().iloc[0]
-
-print(f"Days with returns <= 1% threshold: {extreme_events_1pct}")
-print(f"Days with returns <= 5% threshold: {extreme_events_5pct}")
-
-# Worst consecutive days - simplified approach
-print(f"\nWorst 5 single days:")
-returns_series = returns_data.iloc[:, 0] # Get the first (and only) column
-worst_5_days = returns_series.nsmallest(5)
-for i, (date, return_val) in enumerate(worst_5_days.items()):
- print(f" {i+1}. {date.strftime('%Y-%m-%d')}: {return_val:.2%}")
-print("\n=== SCENARIO ANALYSIS ===")
-
-# Define stress scenarios (percentage moves in underlying assets)
-scenarios = {
- "Market Crash": [-0.20, -0.25, -0.22, -0.30, -0.18, -0.20, 0.05, 0.10],
- "Tech Selloff": [-0.35, -0.40, -0.30, -0.45, -0.10, -0.15, 0.02, 0.03],
- "Interest Rate Shock": [-0.10, -0.12, -0.08, -0.15, -0.05, -0.08, -0.15, 0.01],
- "Flight to Quality": [0.05, 0.02, 0.08, -0.10, 0.10, 0.12, 0.20, 0.15]
-}
-
-print("Portfolio impact under stress scenarios:")
-for scenario_name, asset_moves in scenarios.items():
- # Calculate portfolio impact
- portfolio_impact = sum(w * move for w, move in zip(equal_weights, asset_moves))
- print(f" {scenario_name}: {portfolio_impact:.2%}")
-Use Monte Carlo methods for risk assessment:
-print("\n=== MONTE CARLO RISK SIMULATION ===")
-
-# Import the simulate_portfolios function
-from openseries.portfoliotools import simulate_portfolios
-
-# Monte Carlo simulation using native function
-num_simulations = 10000
-seed = 42 # For reproducible results
-
-# Generate simulated portfolios using the native function
-simulated_portfolios = simulate_portfolios(
- simframe=portfolio_assets,
- num_ports=num_simulations,
- seed=seed
-)
-
-# Extract portfolio metrics from simulation
-portfolio_returns = simulated_portfolios['ret']
-portfolio_volatilities = simulated_portfolios['stdev']
-portfolio_sharpes = simulated_portfolios['sharpe']
-
-# Calculate risk metrics from simulation
-# Calculate 5th percentile manually
-sorted_returns = sorted(portfolio_returns)
-percentile_idx = int(len(sorted_returns) * 0.05)
-sim_var_95 = sorted_returns[percentile_idx]
-sim_cvar_95 = portfolio_returns[portfolio_returns <= sim_var_95].mean()
-
-print(f"Monte Carlo Results ({num_simulations:,} simulations):")
-print(f"Expected Return: {portfolio_returns.mean():.2%}")
-print(f"Average Volatility: {portfolio_volatilities.mean():.2%}")
-print(f"95% VaR: {sim_var_95:.2%}")
-print(f"95% CVaR: {sim_cvar_95:.2%}")
-# Calculate percentiles manually
-worst_idx = int(len(sorted_returns) * 0.001)
-best_idx = int(len(sorted_returns) * 0.999)
-print(f"Worst Case (0.1%): {sorted_returns[worst_idx]:.2%}")
-print(f"Best Case (99.9%): {sorted_returns[best_idx]:.2%}")
-print(f"Average Sharpe Ratio: {portfolio_sharpes.mean():.3f}")
-
-# Show distribution of portfolio characteristics
-print(f"\nPortfolio Distribution:")
-print(f"Return Range: {portfolio_returns.min():.2%} to {portfolio_returns.max():.2%}")
-print(f"Volatility Range: {portfolio_volatilities.min():.2%} to {portfolio_volatilities.max():.2%}")
-print(f"Sharpe Range: {portfolio_sharpes.min():.3f} to {portfolio_sharpes.max():.3f}")
-Analyze risk contribution by asset:
-print("\n=== RISK DECOMPOSITION ===")
-
-# Calculate individual asset volatilities using OpenFrame
-asset_metrics = portfolio_assets.all_properties()
-asset_vols = asset_metrics.loc['Volatility'].values
-
-# Portfolio volatility
-portfolio_vol = portfolio.vol
-
-# Calculate correlation matrix
-correlation_matrix = portfolio_assets.correl_matrix
-
-# Risk contribution analysis using openseries
-# Create portfolio to get portfolio-level metrics
-portfolio_df = portfolio_assets.make_portfolio(name="Portfolio", weight_strat="eq_weights")
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-portfolio_vol = portfolio.vol
-
-print("Risk Contribution Analysis:")
-for i, series in enumerate(portfolio_assets.constituents):
- weight = equal_weights[i]
- asset_vol = asset_vols[i]
- print(f"\n{series.label}:")
- print(f" Weight: {weight:.4f}")
- print(f" Individual Volatility: {asset_vol:.4f}")
- print(f" Weighted Volatility Contribution: {weight * asset_vol:.4f}")
-print(f"\nPortfolio Volatility: {portfolio_vol:.4f}")
-
-# Verify portfolio metrics
-print(f"\nVerification:")
-print(f"Portfolio volatility: {portfolio_vol:.4f}")
-Evaluate risk-adjusted returns:
-print("\n=== RISK-ADJUSTED PERFORMANCE ===")
-
-# Sharpe ratio
-print(f"Sharpe Ratio: {portfolio.ret_vol_ratio:.3f}")
-
-# Sortino ratio (downside risk only)
-print(f"Sortino Ratio: {portfolio.sortino_ratio:.3f}")
-
-# Kappa-3 ratio (higher-order downside risk)
-print(f"Kappa-3 Ratio: {portfolio.kappa3_ratio:.3f}")
-
-# Omega ratio
-print(f"Omega Ratio: {portfolio.omega_ratio:.3f}")
-
-# Compare with individual assets
-print(f"\n=== RISK-ADJUSTED COMPARISON ===")
-all_assets = portfolio_assets.constituents + [portfolio]
-comparison_frame = OpenFrame(constituents=all_assets)
-
-risk_adj_metrics = comparison_frame.all_properties(
- properties=['ret_vol_ratio', 'sortino_ratio', 'kappa3_ratio', 'omega_ratio']
-)
-
-print(risk_adj_metrics.round(3))
-Create a comprehensive risk monitoring summary using openseries properties and methods:
-print("\n" + "="*60)
-print("RISK MONITORING DASHBOARD")
-print("="*60)
-
-# Current date and lookback period
-current_date = portfolio.last_idx
-lookback_date = portfolio.first_idx
-
-print(f"Portfolio: {portfolio.label}")
-print(f"Current Date: {current_date}")
-print(f"Analysis Period: {lookback_date} to {current_date}")
-print(f"Observations: {portfolio.length}")
-
-# Risk metrics using openseries properties
-print(f"\n--- CURRENT RISK METRICS ---")
-print(f"Volatility (annualized): {portfolio.vol:.2%}")
-print(f"Downside Deviation: {portfolio.downside_deviation:.2%}")
-print(f"95% VaR (daily): {portfolio.var_down:.2%}")
-print(f"95% CVaR (daily): {portfolio.cvar_down:.2%}")
-print(f"Maximum Drawdown: {portfolio.max_drawdown:.2%}")
-
-# Performance metrics using openseries properties
-print(f"\n--- PERFORMANCE METRICS ---")
-print(f"Total Return: {portfolio.value_ret:.2%}")
-print(f"Annualized Return: {portfolio.geo_ret:.2%}")
-print(f"Sharpe Ratio: {portfolio.ret_vol_ratio:.3f}")
-print(f"Sortino Ratio: {portfolio.sortino_ratio:.3f}")
-
-# Distribution characteristics using openseries properties
-print(f"\n--- RETURN DISTRIBUTION ---")
-print(f"Skewness: {portfolio.skew:.3f}")
-print(f"Kurtosis: {portfolio.kurtosis:.3f}")
-print(f"Positive Days: {portfolio.positive_share:.1%}")
-
-# Recent performance using openseries properties
-recent_return = portfolio.z_score
-print(f"\n--- RECENT ACTIVITY ---")
-print(f"Last Return Z-Score: {recent_return:.2f}")
-
-if abs(recent_return) > 2:
- print(" ⚠️ ALERT: Recent return is unusual (|z| > 2)")
-elif abs(recent_return) > 3:
- print(" 🚨 WARNING: Recent return is extreme (|z| > 3)")
-else:
- print(" ✅ Recent return is within normal range")
-
-# Risk alerts based on openseries metrics
-print(f"\n--- RISK ALERTS ---")
-alerts = []
-
-if portfolio.vol > 0.25:
- alerts.append("High volatility (>25%)")
-
-if abs(portfolio.max_drawdown) > 0.20:
- alerts.append("Large maximum drawdown (>20%)")
-
-if portfolio.ret_vol_ratio < 0.5:
- alerts.append("Low Sharpe ratio (<0.5)")
-
-if portfolio.skew < -1:
- alerts.append("Highly negative skew (<-1)")
-
-if portfolio.kurtosis > 5:
- alerts.append("High kurtosis (>5) - fat tails")
-
-if alerts:
- for alert in alerts:
- print(f" ⚠️ {alert}")
-else:
- print(" ✅ No risk alerts")
-Implement risk limit monitoring:
-print("\n=== RISK LIMITS MONITORING ===")
-
-# Define risk limits
-risk_limits = {
- 'max_volatility': 0.20, # 20% annual volatility
- 'max_var_daily': -0.03, # 3% daily VaR
- 'max_drawdown': -0.15, # 15% maximum drawdown
- 'min_sharpe': 0.5, # Minimum Sharpe ratio
- 'max_concentration': 0.30 # Maximum single asset weight
-}
-
-# Check current metrics against limits
-current_metrics = {
- 'volatility': portfolio.vol,
- 'var_daily': portfolio.var_down,
- 'drawdown': portfolio.max_drawdown,
- 'sharpe': portfolio.ret_vol_ratio,
- 'max_weight': max(equal_weights)
-}
-
-print("Risk Limit Monitoring:")
-print("-" * 40)
-
-# Volatility check
-if current_metrics['volatility'] > risk_limits['max_volatility']:
- print(f"❌ BREACH: Volatility {current_metrics['volatility']:.2%} > {risk_limits['max_volatility']:.2%}")
-else:
- print(f"✅ OK: Volatility {current_metrics['volatility']:.2%} <= {risk_limits['max_volatility']:.2%}")
-
-# VaR check
-if current_metrics['var_daily'] < risk_limits['max_var_daily']:
- print(f"❌ BREACH: VaR {current_metrics['var_daily']:.2%} < {risk_limits['max_var_daily']:.2%}")
-else:
- print(f"✅ OK: VaR {current_metrics['var_daily']:.2%} >= {risk_limits['max_var_daily']:.2%}")
-
-# Drawdown check
-if current_metrics['drawdown'] < risk_limits['max_drawdown']:
- print(f"❌ BREACH: Drawdown {current_metrics['drawdown']:.2%} < {risk_limits['max_drawdown']:.2%}")
-else:
- print(f"✅ OK: Drawdown {current_metrics['drawdown']:.2%} >= {risk_limits['max_drawdown']:.2%}")
-
-# Sharpe ratio check
-if current_metrics['sharpe'] < risk_limits['min_sharpe']:
- print(f"❌ BREACH: Sharpe {current_metrics['sharpe']:.3f} < {risk_limits['min_sharpe']:.3f}")
-else:
- print(f"✅ OK: Sharpe {current_metrics['sharpe']:.3f} >= {risk_limits['min_sharpe']:.3f}")
-
-# Concentration check
-if current_metrics['max_weight'] > risk_limits['max_concentration']:
- print(f"❌ BREACH: Max weight {current_metrics['max_weight']:.2%} > {risk_limits['max_concentration']:.2%}")
-else:
- print(f"✅ OK: Max weight {current_metrics['max_weight']:.2%} <= {risk_limits['max_concentration']:.2%}")
-Save comprehensive risk analysis:
-# Create comprehensive risk report
-# Create risk report using openseries methods
-print("\n=== RISK REPORT ===")
-print("Risk metrics are available through openseries properties:")
-for series in portfolio_assets.constituents:
- print(f"\n{series.label}:")
- print(f" VaR (95%): {series.var_down:.4f}")
- print(f" CVaR (95%): {series.cvar_down:.4f}")
- print(f" Volatility: {series.vol:.4f}")
- print(f" Max Drawdown: {series.max_drawdown:.4f}")
-
-# Note: For comprehensive Excel export, use openseries to_xlsx() method
-portfolio_assets.to_xlsx('risk_analysis_report.xlsx')
-
-# Alternative: risk_report = pd.DataFrame({
- 'Metric': [
- 'Annualized Return', 'Annualized Volatility', 'Sharpe Ratio',
- 'Sortino Ratio', 'Maximum Drawdown', '95% VaR (daily)',
- '95% CVaR (daily)', 'Skewness', 'Kurtosis', 'Positive Days %'
- ],
- 'Value': [
- f"{portfolio.geo_ret:.2%}",
- f"{portfolio.vol:.2%}",
- f"{portfolio.ret_vol_ratio:.3f}",
- f"{portfolio.sortino_ratio:.3f}",
- f"{portfolio.max_drawdown:.2%}",
- f"{portfolio.var_down:.2%}",
- f"{portfolio.cvar_down:.2%}",
- f"{portfolio.skew:.3f}",
- f"{portfolio.kurtosis:.3f}",
- f"{portfolio.positive_share:.1%}"
- ]
-})
-
-# Export to Excel
-# Export using openseries native method (commented out ExcelWriter approach)
-# with pd.ExcelWriter('risk_analysis_report.xlsx') as writer:
- risk_report.to_excel(writer, sheet_name='Risk Metrics', index=False)
- risk_decomp.to_excel(writer, sheet_name='Risk Decomposition', index=False)
- correlation_matrix.to_excel(writer, sheet_name='Correlations')
-
- # Add rolling metrics if available
- if 'rolling_vol' in locals():
- rolling_vol.to_excel(writer, sheet_name='Rolling Volatility')
- if 'rolling_var' in locals():
- rolling_var.to_excel(writer, sheet_name='Rolling VaR')
-
-print(f"\nRisk analysis report exported to 'risk_analysis_report.xlsx'")
-print("Risk management analysis complete!")
-This comprehensive risk management tutorial provides the foundation for implementing robust risk controls and monitoring systems using openseries.
-This section explains the fundamental concepts and design principles behind openseries.
-openseries is built around two main classes that inherit from Pydantic’s BaseModel:
-OpenTimeSeries: Manages individual financial time series
OpenFrame: Manages collections of OpenTimeSeries objects
Both classes provide:
-Type safety through Pydantic validation
Immutable data - original data is preserved
Consistent API - similar methods across both classes
Financial focus - methods designed for financial analysis
openseries favors in-place transformations. Many methods modify the existing object
-and return self for chaining rather than creating a new object.
-On OpenTimeSeries, the dates and values arrays are always left untouched,
-while the working data in the tsdf pandas DataFrame is mutable.
-On OpenFrame, the tsdf DataFrame is also mutable and reflects
-transformations applied to the frame. If you need to preserve the original state or
-compare before/after results, create an explicit copy
-(for example, OpenTimeSeries.from_deepcopy() or OpenFrame.from_deepcopy()).
Every OpenTimeSeries has these fundamental properties:
-# Create a sample series using openseries simulation
-from openseries import ReturnSimulation, ValueType
-import datetime as dt
-
-simulation = ReturnSimulation.from_lognormal(
- number_of_sims=1,
- trading_days=100,
- mean_annual_return=0.25, # ~0.001 daily
- mean_annual_vol=0.32, # ~0.02 daily
- trading_days_in_year=252,
- seed=42
-)
-
-series = OpenTimeSeries.from_df(
- dframe=simulation.to_dataframe(name="Sample Asset", end=dt.date(2023, 12, 31)),
- valuetype=ValueType.RTRN
-).to_cumret() # Convert returns to cumulative prices
-
-# Core properties
-print(f"Name: {series.label}")
-print(f"Length: {series.length}")
-print(f"First date: {series.first_idx}")
-print(f"Last date: {series.last_idx}")
-print(f"Value type: {series.valuetype}")
-The original data is never modified:
-# Original data is preserved
-original_dates = series.dates # List of date strings
-original_values = series.values # List of float values
-
-# Working data is in the tsdf DataFrame
-working_data = series.tsdf # pandas DataFrame
-
-# Transformations modify the original object (method chaining)
-series.value_to_ret() # Modifies original series
-print(f"Series length: {series.length}") # Usually length - 1
-The ValueType enum identifies what the series represents:
-from openseries import ValueType
-
-# Common value types
-print(ValueType.PRICE) # "Price(Close)"
-print(ValueType.RTRN) # "Return(Total)"
-print(ValueType.ROLLVOL) # "Rolling volatility"
-
-# Check series type
-print(f"Series type: {series.valuetype}")
-
-# Type changes with transformations
-series.value_to_ret() # Modifies original
-print(f"Returns type: {series.valuetype}")
-OpenFrame manages collections of OpenTimeSeries:
-from openseries import OpenFrame
-
-# Create multiple series using openseries simulation
-simulation = ReturnSimulation.from_lognormal(
- number_of_sims=3,
- trading_days=100,
- mean_annual_return=0.25, # ~0.001 daily
- mean_annual_vol=0.32, # ~0.02 daily
- trading_days_in_year=252,
- seed=42
-)
-
-# Create OpenFrame with multiple series from simulation
-frame = OpenFrame(
- constituents=[
- OpenTimeSeries.from_df(
- dframe=simulation.to_dataframe(name="Asset", end=dt.date(2023, 12, 31)),
- column_nmbr=serie,
- valuetype=ValueType.RTRN,
- ).to_cumret() # Convert returns to cumulative prices
- for serie in range(simulation.number_of_sims)
- ]
-)
-
-# Frame properties
-print(f"Number of series: {frame.item_count}")
-print(f"Column names: {frame.columns_lvl_zero}")
-print(f"Common length: {frame.length}")
-OpenFrame concatenates series data but does not automatically align them. -The library provides explicit methods for alignment that require user choice:
-# Series with different date ranges are concatenated (not aligned)
-print("Individual series lengths:")
-print(frame.lengths_of_items)
-
-print(f"Frame length (concatenated): {frame.length}")
-
-# Explicit alignment methods require user choice:
-
-# 1. Truncate to common date range
-frame.trunc_frame()
-
-# 2. Align to business day calendar (modifies original)
-frame.align_index_to_local_cdays(countries="US")
-
-# 3. Handle missing values (modifies original)
-frame.value_nan_handle(method="fill")
-
-# 4. Merge with explicit join strategy
-frame.merge_series(how="inner")
-frame.merge_series(how="outer")
-openseries uses standard financial formulas:
-# Simple returns: (P_t / P_{t-1}) - 1
-series.value_to_ret() # Modifies original
-
-# Log returns: ln(P_t / P_{t-1})
-series.value_to_log() # Modifies original
-
-# Cumulative returns: rebasing to start at 1.0 (modifies original)
-series.to_cumret()
-Metrics are annualized using the actual number of observations per year:
-# Automatic calculation of periods per year
-print(f"Periods per year: {series.periods_in_a_year:.1f}")
-
-# Annualized return (geometric mean)
-annual_return = series.geo_ret
-print(f"Annualized return: {annual_return:.2%}")
-
-# Annualized volatility
-annual_vol = series.vol
-print(f"Annualized volatility: {annual_vol:.2%}")
-Risk calculations follow industry standards:
-# Value at Risk (95% confidence)
-var_95 = series.var_down
-print(f"95% VaR: {var_95:.2%}")
-
-# Conditional Value at Risk (Expected Shortfall)
-cvar_95 = series.cvar_down
-print(f"95% CVaR: {cvar_95:.2%}")
-
-# Maximum Drawdown
-max_dd = series.max_drawdown
-print(f"Maximum Drawdown: {max_dd:.2%}")
-
-# Sortino Ratio (downside deviation)
-sortino = series.sortino_ratio
-print(f"Sortino Ratio: {sortino:.2f}")
-openseries integrates with business day calendars:
-# Align to specific country's business days (modifies original)
-series.align_index_to_local_cdays(countries="US")
-
-# Multiple countries (intersection of business days) (modifies original)
-series.align_index_to_local_cdays(countries=["US", "GB"])
-
-# Custom markets using pandas-market-calendars (modifies original)
-series.align_index_to_local_cdays(markets="NYSE")
-Convert between different frequencies:
-# Resample to month-end (modifies original)
-series.resample_to_business_period_ends(freq="BME")
-
-# Resample to quarter-end (modifies original)
-series.resample_to_business_period_ends(freq="BQE")
-
-# Custom resampling (modifies original)
-series.resample(freq="W")
-Pydantic ensures data integrity:
-# Dates must be valid ISO format strings
-# This will fail with a validation error
-invalid_series = OpenTimeSeries.from_arrays(
- dates=["invalid-date"],
- values=[100.0]
-)
-
-# Values must be numeric
-# This will fail with a validation error
-invalid_series = OpenTimeSeries.from_arrays(
- dates=["2023-01-01"],
- values=["not a number"]
-)
-The library performs consistency checks:
-# Dates and values must have same length
-# Mixed value types in OpenFrame are detected
-# Date alignment issues are caught
-openseries methods fall into several categories:
-Properties: Return calculated values (e.g., series.vol)
Methods: Perform operations or take parameters (e.g., series.vol_func())
# Property - uses full series
-volatility = series.vol
-
-# Method - can specify date range
-recent_vol = series.vol_func(months_from_last=12)
-Methods that modify the original object (return self for chaining):
-# Data transformations (modify original)
-series.value_to_ret() # Prices to returns
-series.to_drawdown_series() # Drawdown series
-series.to_cumret() # Cumulative returns
-
-# Time transformations (modify original)
-series.resample_to_business_period_ends(freq="BME")
-series.align_index_to_local_cdays(countries="US")
-Methods that return new objects:
-# Analysis methods (return new objects)
-rolling_vol = series.rolling_vol(observations=30)
-rolling_ret = series.rolling_return(observations=30)
-Methods that return calculated values:
-# Rolling calculations
-rolling_vol = series.rolling_vol(observations=30)
-rolling_corr = frame.rolling_corr(observations=60)
-
-# Statistical analysis
-beta = frame.beta()
-tracking_error = frame.tracking_error_func()
-Methods for saving results:
-# File exports
-series.to_xlsx("analysis.xlsx")
-series.to_json("data.json")
-
-# Visualization
-series.plot_series()
-series.plot_histogram()
-# Prefer from_df for pandas data
-series = OpenTimeSeries.from_df(dframe=dataframe['Close'])
-series.set_new_label(lvl_zero="Asset")
-
-# Use from_arrays for custom data
-series = OpenTimeSeries.from_arrays(dates=date_list, values=value_list)
-
-# Always set meaningful names
-series.set_new_label(lvl_zero="Descriptive Name")
-# 1. Load and validate data
-series = OpenTimeSeries.from_df(dframe=data['Close'])
-series.set_new_label(lvl_zero="Asset")
-
-# 2. Basic analysis
-metrics = series.all_properties()
-
-# 3. Specific calculations
-series.to_drawdown_series() # Convert to drawdown (modifies original)
-rolling_metrics = series.rolling_vol(observations=252) # Returns DataFrame
-
-# 4. Visualization
-series.plot_series()
-
-# 5. Export results
-series.to_xlsx(fiilename="analysis.xlsx")
-# Original data is preserved - use deepcopy if needed
-series_copy = OpenTimeSeries.from_deepcopy(series)
-
-# Large datasets - consider resampling (modifies original)
-series.resample_to_business_period_ends(freq="BME")
-
-# Clean up intermediate results
-del intermediate_series
-OpenFrame provides several built-in weight strategies for portfolio construction:
-from openseries.owntypes import MaxDiversificationNaNError, MaxDiversificationNegativeWeightsError
-
-# Available weight strategies
-strategies = {
- 'eq_weights': 'Equal weights for all assets',
- 'inv_vol': 'Inverse volatility weighting (risk parity)',
- 'max_div': 'Maximum diversification optimization',
- 'min_vol_overweight': 'Minimum volatility overweight strategy'
-}
-
-# Example with error handling
-# This may fail with MaxDiversificationNaNError or MaxDiversificationNegativeWeightsError
-portfolio_df = frame.make_portfolio(name="Max Div", weight_strat="max_div")
-Understanding these core concepts will help you use openseries effectively and build more sophisticated financial analysis workflows.
-This guide covers data loading, validation, transformation, and management in openseries.
-The most common way to load data is from pandas objects:
-import pandas as pd
-from openseries import OpenTimeSeries
-
-# From pandas Series with DatetimeIndex
-data = pd.Series([100, 101, 99, 102],
- index=pd.date_range('2023-01-01', periods=4))
-series = OpenTimeSeries.from_df(dframe=data)
-series.set_new_label(lvl_zero="Sample")
-
-# From pandas DataFrame column
-df = pd.DataFrame({
- 'Date': pd.date_range('2023-01-01', periods=4),
- 'Close': [100, 101, 99, 102],
- 'Volume': [1000, 1100, 900, 1200]
-})
-df.set_index('Date', inplace=True)
-series = OpenTimeSeries.from_df(dframe=df['Close'])
-series.set_new_label(lvl_zero="Stock")
-For custom data or when working with lists:
-# From date strings and values
-dates = ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04']
-values = [100.0, 101.0, 99.0, 102.0]
-
-series = OpenTimeSeries.from_arrays(
- dates=dates,
- values=values,
- name="Custom Data"
-)
-Generate synthetic data from a fixed rate:
-from datetime import date
-
-# Create 252 trading days at 5% annual rate
-series = OpenTimeSeries.from_fixed_rate(
- rate=0.05,
- days=252,
- end_date=date(2023, 12, 31),
- name="5% Fixed Rate"
-)
-openseries enforces strict date formats:
-# Valid date formats
-valid_dates = ['2023-01-01', '2023-12-31', '2024-02-29'] # ISO format
-
-# Invalid formats will raise ValidationError
-# This will fail with a validation error
-invalid_series = OpenTimeSeries.from_arrays(
- dates=['01/01/2023', '2023-1-1'], # Wrong format
- values=[100, 101]
-)
-Values must be numeric and finite:
-import numpy as np
-
-# Valid values
-valid_values = [100.0, 101.5, 99.25, 102.75]
-
-# Handle NaN values appropriately
-values_with_nan = [100.0, np.nan, 99.0, 102.0]
-series = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04'],
- values=values_with_nan,
- name="Data with NaN"
-)
-
-# Clean NaN values (modifies original)
-series.value_nan_handle() # Forward fill
-Dates and values must have the same length:
-# This will raise an error
-# This will fail with a length mismatch error
-invalid_series = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02'],
- values=[100.0, 101.0, 102.0] # Different length
-)
-# Assume we have a price series
-prices = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02', '2023-01-03'],
- values=[100.0, 102.0, 99.0],
- name="Stock Price"
-)
-
-# Convert to simple returns (modifies original)
-prices.value_to_ret()
-print(f"Returns: {prices.values}") # [0.02, -0.0294...]
-
-# Convert to log returns (modifies original)
-prices.value_to_log()
-
-# Convert returns back to cumulative values (modifies original)
-prices.to_cumret()
-
-# Convert to differences (absolute changes) (modifies original)
-prices.value_to_diff()
-Change the frequency of your data:
-# Daily to monthly (business month end) (modifies original)
-series.resample_to_business_period_ends(freq="BME")
-
-# Daily to quarterly (modifies original)
-series.resample_to_business_period_ends(freq="BQE")
-
-# Daily to annual (modifies original)
-series.resample_to_business_period_ends(freq="BYE")
-
-# Custom resampling with pandas frequency strings (modifies original)
-series.resample(freq="W")
-
-# Resample with specific method (modifies original)
-series.resample(freq="W", method="mean")
-Align data to business day calendars:
-# Align to US business days (modifies original)
-series.align_index_to_local_cdays(countries="US")
-
-# Align to multiple countries (intersection) (modifies original)
-series.align_index_to_local_cdays(countries=["US", "GB", "JP"])
-
-# Align to specific market calendar (modifies original)
-series.align_index_to_local_cdays(markets="NYSE")
-import numpy as np
-
-# Create series with missing values
-dates = ['2023-01-01', '2023-01-02', '2023-01-03', '2023-01-04']
-values = [100.0, np.nan, 102.0, np.nan]
-
-series_with_nan = OpenTimeSeries.from_arrays(
- dates=dates, values=values, name="With NaN"
-)
-
-# Forward fill missing values (for price series) (modifies original)
-series_with_nan.value_nan_handle()
-
-# For return series, replace NaN with 0.0 (modifies original)
-series_with_nan.value_to_ret()
-series_with_nan.return_nan_handle()
-# Remove NaN values entirely (modifies original)
-series_with_nan.value_nan_handle(method="drop")
-from openseries import OpenFrame
-
-# Create multiple series
-series1 = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02', '2023-01-03'],
- values=[100, 102, 99], name="Asset A"
-)
-
-series2 = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02', '2023-01-03'],
- values=[50, 51, 49], name="Asset B"
-)
-
-# Create frame
-frame = OpenFrame(constituents=[series1, series2])
-OpenFrame automatically handles series with different date ranges:
-# Series with different start/end dates
-early_series = OpenTimeSeries.from_arrays(
- dates=['2022-12-01', '2023-01-01', '2023-01-02'],
- values=[95, 100, 102], name="Early Start"
-)
-
-late_series = OpenTimeSeries.from_arrays(
- dates=['2023-01-02', '2023-01-03', '2023-01-04'],
- values=[51, 49, 52], name="Late Start"
-)
-
-# Frame will align to common date range
-frame = OpenFrame(constituents=[early_series, late_series])
-print(f"Frame date range: {frame.first_idx} to {frame.last_idx}")
-# Add a new series
-new_series = OpenTimeSeries.from_arrays(
- dates=['2023-01-01', '2023-01-02', '2023-01-03'],
- values=[200, 205, 198], name="Asset C"
-)
-frame.add_timeseries(new_series)
-
-# Remove a series by index
-frame.delete_timeseries(item_idx=0)
-# Export single series
-series.to_xlsx(filename="single_series.xlsx")
-
-# Export frame (multiple series)
-frame.to_xlsx(filename="multiple_series.xlsx")
-
-# Export with custom sheet title
-series.to_xlsx(
- filename="formatted_export.xlsx",
- sheet_title="Analysis"
-)
-# Export series values only
-series.to_json(what_output="values", filename="series_values.json")
-
-# Export full dataframe structure
-series.to_json(what_output="tsdf", filename="series_dataframe.json")
-
-# Export frame data
-frame.to_json(what_output="values", filename="frame_values.json")
-import yfinance as yf
-
-# Single asset
-ticker = yf.Ticker("AAPL")
-data = ticker.history(period="2y")
-
-apple = OpenTimeSeries.from_df(
- dframe=data['Close'],
- name="Apple Inc."
-)
-
-# Multiple assets
-tickers = ["AAPL", "GOOGL", "MSFT"]
-series_list = []
-
-for ticker_symbol in tickers:
- ticker = yf.Ticker(ticker_symbol)
- data = ticker.history(period="1y")
- series = OpenTimeSeries.from_df(
- dframe=data['Close'],
- name=ticker_symbol
- )
- series_list.append(series)
-
-tech_frame = OpenFrame(constituents=series_list)
-# Load from CSV
-df = pd.read_csv("stock_data.csv", index_col=0, parse_dates=True)
-
-series = OpenTimeSeries.from_df(
- dframe=df['Close'],
- name="Stock from CSV"
-)
-# Check for data quality issues
-print(f"Series length: {series.length}")
-print(f"Date range: {series.first_idx} to {series.last_idx}")
-print(f"Span of days: {series.span_of_days}")
-
-# Check for gaps in data
-expected_length = (series.last_idx - series.first_idx).days + 1
-actual_length = series.length
-
-if expected_length != actual_length:
- print(f"Data gaps detected: expected {expected_length}, got {actual_length}")
-# Convert to returns for outlier analysis (modifies original)
-series.value_to_ret()
-
-# Detect outliers using the built-in method
-outliers = series.outliers(threshold=3.0)
-print(f"Found {len(outliers)} outliers (|z| > 3)")
-
-# For OpenFrame, outliers returns a DataFrame
-frame_outliers = frame.outliers(threshold=3.0)
-print(f"Found outliers in frame: {len(frame_outliers)} rows")
-
-# Customize threshold and date range
-recent_outliers = series.outliers(
- threshold=2.5,
- months_from_last=6
-)
-# For large datasets, consider resampling
-large_series = series # Assume this is large daily data
-
-# Reduce to monthly for analysis (modifies original)
-large_series.resample_to_business_period_ends(freq="BME")
-
-# Use monthly for computationally intensive operations
-monthly_metrics = large_series.all_properties()
-# When loading multiple assets, batch the operations
-tickers = ["AAPL", "GOOGL", "MSFT", "AMZN", "TSLA"]
-
-# Download all at once
-data = yf.download(tickers, period="2y")['Close']
-
-# Create series efficiently
-series_list = []
-for ticker in tickers:
- series = OpenTimeSeries.from_df(
- dframe=data[ticker].dropna(),
- name=ticker
- )
- series_list.append(series)
-
-frame = OpenFrame(constituents=series_list)
-This comprehensive guide should help you handle various data scenarios effectively with openseries.
-openseries requires Python 3.11 or higher and is compatible with:
-Operating Systems: Windows, macOS, Linux
Python versions: 3.11, 3.12, 3.13, 3.14
The easiest way to install openseries is using pip:
-pip install openseries
-openseries is also available on conda-forge:
-conda install -c conda-forge openseries
-To install the latest development version from GitHub:
-git clone https://github.com/CaptorAB/openseries.git
-cd openseries
-pip install -e .
-openseries automatically installs the following dependencies:
-pandas (>=2.1.2) - Data manipulation and analysis
numpy (>=1.23.2) - Numerical computing
pydantic (>=2.5.2) - Data validation and settings management
plotly (>=5.18.0) - Interactive plotting
scipy (>=1.14.1) - Scientific computing
scikit-learn (>=1.4.0) - Machine learning utilities
exchange-calendars (>=4.8) - Trading calendar support
holidays (>=0.30) - Holiday calendar support
python-dateutil (>=2.8.2) - Date parsing utilities
tzdata (>=2025.3) - IANA time zone data
openpyxl (>=3.1.2) - Excel file support
requests (>=2.20.0) - HTTP library
For data acquisition examples, you may want to install:
-pip install yfinance # For Yahoo Finance data
-To verify that openseries is installed correctly, run:
-import openseries
-print(openseries.__version__)
-You can also run a quick test:
-from openseries import OpenTimeSeries, ReturnSimulation, ValueType
-import datetime as dt
-
-# Create sample data using openseries simulation
-simulation = ReturnSimulation.from_lognormal(
- number_of_sims=1,
- trading_days=100,
- mean_annual_return=0.25, # ~0.001 daily
- mean_annual_vol=0.32, # ~0.02 daily
- trading_days_in_year=252,
- seed=42
-)
-
-# Create OpenTimeSeries
-series = OpenTimeSeries.from_df(
- dframe=simulation.to_dataframe(name="Test Series", end=dt.date(2023, 12, 31)),
- valuetype=ValueType.RTRN
-).to_cumret() # Convert returns to cumulative prices
-
-print(f"Series length: {series.length}")
-print(f"Annual return: {series.geo_ret:.2%}")
-If you plan to contribute to openseries or need the development dependencies,
-use the same pinned tooling as CI (uv==0.11.21):
git clone https://github.com/CaptorAB/openseries.git
-cd openseries
-make install
-On Windows, run .\make.ps1 make instead of make install.
This creates venv, installs locked runtime, development, and documentation
-dependencies from uv.lock, and installs pre-commit hooks. Development
-dependencies include:
pytest (>=9.1.0) - Testing framework
pytest-cov (>=7.1.0) - Coverage plugin
pytest-xdist (>=3.8.0) - Parallel test runner
mypy (==2.1.0) - Static type checking
ruff (==0.15.18) - Linting and formatting
pre-commit (>=4.6.0) - Git hooks for code quality
ImportError: No module named ‘openseries’
-Make sure openseries is installed in the correct Python environment. If using virtual environments, ensure it’s activated.
-Version conflicts
-If you encounter dependency conflicts, try creating a fresh virtual environment:
-python -m venv openseries_env
-source openseries_env/bin/activate # On Windows: openseries_env\Scripts\activate
-pip install openseries
-Performance issues
-For better performance with large datasets, consider installing optional accelerated packages:
-pip install numba # For numerical acceleration
-pip install bottleneck # For faster pandas operations
-If you encounter issues:
-Check the GitHub Issues
Review the Release Notes
Create a new issue with a minimal reproducible example
On Windows, you may need to install Microsoft Visual C++ Build Tools if you encounter compilation errors with dependencies.
-On macOS with Apple Silicon (M1/M2), all dependencies should install without issues. If you encounter problems, try using conda instead of pip.
-Most Linux distributions should work without issues. On minimal installations, you may need to install additional system packages for some dependencies.
-This guide will get you up and running with openseries in just a few minutes.
-Let’s start by creating a simulated financial time series using openseries’ built-in simulation capabilities:
-from openseries import OpenTimeSeries, ReturnSimulation, ValueType
-import datetime as dt
-
-# Create a simulated time series using lognormal distribution
-simulation = ReturnSimulation.from_lognormal(
- number_of_sims=1,
- trading_days=1000,
- mean_annual_return=0.08, # 8% annual return
- mean_annual_vol=0.15, # 15% annual volatility
- trading_days_in_year=252,
- seed=71
-)
-
-# Convert simulation to OpenTimeSeries
-sp500 = OpenTimeSeries.from_df(
- dframe=simulation.to_dataframe(name="S&P 500", end=dt.date(2023, 12, 31)),
- valuetype=ValueType.RTRN
-).to_cumret() # Convert returns to cumulative prices
-
-sp500.set_new_label(lvl_zero="S&P 500")
-
-# Display basic information
-print(f"Series: {sp500.label}")
-print(f"Start date: {sp500.first_idx}")
-print(f"End date: {sp500.last_idx}")
-print(f"Number of observations: {sp500.length}")
-Alternatively, you can load data from external sources like yfinance:
-import yfinance as yf # pip install yfinance
-from openseries import OpenTimeSeries
-
-# Download S&P 500 data
-ticker = yf.Ticker("^GSPC")
-data = ticker.history(period="2y")
-
-# Create OpenTimeSeries from the Close prices
-sp500 = OpenTimeSeries.from_df(dframe=data['Close'])
-
-# Set a more descriptive label
-sp500.set_new_label(lvl_zero="S&P 500 Index")
-
-print(f"Loaded {sp500.length} observations")
-print(f"Date range: {sp500.first_idx} to {sp500.last_idx}")
-openseries provides a comprehensive set of financial metrics:
-# Key performance metrics
-print(f"Total Return: {sp500.value_ret:.2%}")
-print(f"Annualized Return (CAGR): {sp500.geo_ret:.2%}")
-print(f"Annualized Volatility: {sp500.vol:.2%}")
-print(f"Sharpe Ratio: {sp500.ret_vol_ratio:.2f}")
-print(f"Maximum Drawdown: {sp500.max_drawdown:.2%}")
-
-# Risk metrics
-print(f"95% VaR (daily): {sp500.var_down:.2%}")
-print(f"95% CVaR (daily): {sp500.cvar_down:.2%}")
-print(f"Sortino Ratio: {sp500.sortino_ratio:.2f}")
-
-# Distribution statistics
-print(f"Skewness: {sp500.skew:.2f}")
-print(f"Kurtosis: {sp500.kurtosis:.2f}")
-print(f"Positive Days: {sp500.positive_share:.1%}")
-Use the all_properties attribute to get a comprehensive overview:
# Get all metrics of an OpenTimeSeries or OpenFrame
-metrics = sp500.all_properties()
-print(metrics)
-openseries integrates with Plotly for interactive visualizations:
-# Plot the timeseries
-sp500.plot_series()
-# This opens an interactive plot in your browser
-
-# Plot returns histogram
-returns = sp500.from_deepcopy()
-returns.value_to_ret() # Convert to returns (modifies original)
-returns.plot_histogram()
-
-# Plot bar chart (useful for plotting returns)
-returns.plot_bars()
-
-# Plot drawdown series
-sp500.to_drawdown_series() # Convert to drawdown (modifies original)
-sp500.plot_series()
-For multi-asset analysis, use the OpenFrame class:
-from openseries import OpenFrame
-import yfinance as yf
-
-# Download data for multiple assets
-tickers = ["^GSPC", "^IXIC", "^RUT"] # S&P 500, NASDAQ, Russell 2000
-names = ["S&P 500", "NASDAQ", "Russell 2000"]
-
-series_list = []
-for ticker, name in zip(tickers, names):
- data = yf.Ticker(ticker).history(period="2y")
- series = OpenTimeSeries.from_df(dframe=data['Close'])
- series.set_new_label(lvl_zero=name)
- series_list.append(series)
-
-# Create OpenFrame
-frame = OpenFrame(constituents=series_list)
-frame.value_nan_handle().trunc_frame()
-
-# Get metrics for all series
-all_metrics = frame.all_properties()
-print(all_metrics)
-
-# Calculate correlations
-correlations = frame.correl_matrix
-print("\nCorrelation Matrix:")
-print(correlations)
-Create and analyze portfolios:
-# Equal-weighted portfolio
-portfolio_df = frame.make_portfolio(name="Equal Weight", weight_strat="eq_weights")
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-
-print(f"Equal Weight Portfolio Return: {portfolio.geo_ret:.2%}")
-print(f"Equal Weight Portfolio Volatility: {portfolio.vol:.2%}")
-print(f"Equal Weight Portfolio Sharpe: {portfolio.ret_vol_ratio:.2f}")
-
-# Create custom weighted portfolio
-frame.weights = [0.8, 0.2] # Custom allocation
-custom_df = frame.make_portfolio(name="Custom Portfolio")
-custom_portfolio = OpenTimeSeries.from_df(dframe=custom_df)
-print(f"Custom Portfolio Sharpe: {custom_portfolio.ret_vol_ratio:.2f}")
-
-# Compare with individual assets
-frame.add_timeseries(portfolio)
-frame.add_timeseries(custom_portfolio)
-comparison = frame.all_properties()
-print(comparison)
-openseries provides various data transformation methods:
-# Convert prices to returns (modifies original)
-sp500.value_to_ret()
-print(f"Returns series length: {sp500.length}")
-
-# Convert to log returns (modifies original)
-sp500.value_to_log()
-
-# Calculate rolling statistics
-rolling_vol = sp500.rolling_vol(observations=30) # 30-day rolling volatility
-rolling_ret = sp500.rolling_return(observations=30) # 30-day rolling returns
-
-# Resample to monthly data (modifies original)
-sp500.resample_to_business_period_ends(freq="BME")
-print(f"Monthly data points: {sp500.length}")
-Save your analysis results:
-# Export to Excel
-sp500.to_xlsx(filename="sp500_analysis.xlsx")
-
-# Export to JSON
-sp500.to_json(filename="sp500_data.json", what_output="tsdf")
-openseries handles business day calendars automatically:
-# Align to Swedish business days (modifies original)
-sp500.align_index_to_local_cdays(countries="SE")
-
-# Use multiple countries (modifies original)
-sp500.align_index_to_local_cdays(countries=["US", "GB"])
-
-# Handle missing values (modifies original)
-sp500.value_nan_handle() # Forward fill NaN values
-Now that you’ve learned the basics, explore:
-Tutorials - Detailed examples for specific use cases
API Reference - Complete documentation of all methods and properties
Examples - Real-world analysis scenarios
OpenTimeSeries: For single asset analysis
OpenFrame: For multi-asset and portfolio analysis
ValueType: Enum to identify data types (prices, returns, etc.)
Business day handling: Automatic alignment to trading calendars
Interactive plotting: Built-in Plotly integration
Type safety: Pydantic-based validation ensures data integrity
Here are some common usage patterns:
-# Pattern 1: Load, analyze, visualize
-series = OpenTimeSeries.from_df(dframe=data['Close'])
-series.set_new_label(lvl_zero="Asset")
-metrics = series.all_properties()
-series.plot_series()
-
-# Pattern 2: Multi-asset comparison
-frame = OpenFrame(constituents=[series1, series2, series3])
-comparison = frame.all_properties()
-correlations = frame.correl_matrix
-
-# Pattern 3: Portfolio construction (built-in strategies)
-portfolio_df = frame.make_portfolio(name="Equal Weight", weight_strat="eq_weights")
-portfolio = OpenTimeSeries.from_df(dframe=portfolio_df)
-frame.add_timeseries(portfolio)
-
-# Pattern 3b: Custom portfolio construction (create fresh frame)
-custom_frame = OpenFrame(constituents=[series1, series2, series3])
-custom_frame.weights = [0.4, 0.3, 0.3]
-custom_df = custom_frame.make_portfolio(name="Custom Portfolio")
-custom_portfolio = OpenTimeSeries.from_df(dframe=custom_df)
-
-# Pattern 4: Risk analysis
-risk_series = series.from_deepcopy() # Create copy for risk analysis
-var_95 = risk_series.var_down # VaR on returns
-max_dd = series.max_drawdown
-rolling_risk = risk_series.rolling_vol(observations=252)
-
-# Drawdown analysis (on original series)
-series.to_drawdown_series() # Convert to drawdown (modifies original)
-This should give you a solid foundation to start using openseries for your financial analysis needs!
-