├── ape_risk ├── py.typed ├── __init__.py ├── stats │ ├── __init__.py │ └── montecarlo.py └── strategies │ ├── __init__.py │ ├── simulation.py │ └── core.py ├── tests ├── __init__.py ├── conftest.py ├── stats │ ├── __init__.py │ ├── conftest.py │ ├── test_montecarlo.py │ └── test_multivariate_montecarlo.py └── strategies │ ├── __init__.py │ ├── test_core.py │ └── test_simulation.py ├── notebook ├── multi_example.png └── example.ipynb ├── setup.cfg ├── ape-config.yaml ├── .github ├── workflows │ ├── draft.yaml │ ├── title.yaml │ ├── commitlint.yaml │ ├── publish.yaml │ └── test.yaml ├── PULL_REQUEST_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── feature.md │ ├── bug.md │ └── work-item.md └── release-drafter.yml ├── .pre-commit-config.yaml ├── pyproject.toml ├── CONTRIBUTING.md ├── README.md ├── .gitignore ├── setup.py └── LICENSE /ape_risk/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/stats/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ape_risk/__init__.py: -------------------------------------------------------------------------------- 1 | # Add module top-level imports here 2 | -------------------------------------------------------------------------------- /notebook/multi_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smolquants/ape-risk/HEAD/notebook/multi_example.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 100 3 | exclude = 4 | venv* 5 | .eggs 6 | docs 7 | build 8 | -------------------------------------------------------------------------------- /ape-config.yaml: -------------------------------------------------------------------------------- 1 | name: ape-risk 2 | 3 | plugins: 4 | - name: notebook 5 | 6 | default_ecosystem: ethereum 7 | -------------------------------------------------------------------------------- /ape_risk/stats/__init__.py: -------------------------------------------------------------------------------- 1 | from .montecarlo import MonteCarlo, MultivariateMonteCarlo 2 | 3 | __all__ = [ 4 | "MonteCarlo", 5 | "MultivariateMonteCarlo", 6 | ] 7 | -------------------------------------------------------------------------------- /ape_risk/strategies/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import gbms, multi_gbms, multi_sims, sims 2 | from .simulation import MultivariateSimulationStrategy, SimulationStrategy 3 | 4 | __all__ = [ 5 | "sims", 6 | "multi_sims", 7 | "gbms", 8 | "multi_gbms", 9 | "SimulationStrategy", 10 | "MultivariateSimulationStrategy", 11 | ] 12 | -------------------------------------------------------------------------------- /.github/workflows/draft.yaml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update-draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Drafts your next Release notes as Pull Requests are merged into "main" 13 | - uses: release-drafter/release-drafter@v5 14 | with: 15 | disable-autolabeler: true 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### What I did 2 | 3 | 4 | fixes: # 5 | 6 | ### How I did it 7 | 8 | ### How to verify it 9 | 10 | ### Checklist 11 | 12 | - [ ] Passes all linting checks (pre-commit and CI jobs) 13 | - [ ] New test cases have been added and are passing 14 | - [ ] Documentation has been updated 15 | - [ ] PR title follows [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) standard (will be automatically included in the changelog) 16 | -------------------------------------------------------------------------------- /tests/stats/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from ape_risk.stats.montecarlo import MonteCarlo, MultivariateMonteCarlo 4 | 5 | 6 | @pytest.fixture 7 | def mc(): 8 | dist_type = "norm" 9 | num_points = 100000 10 | num_sims = 10 11 | return MonteCarlo(dist_type=dist_type, num_points=num_points, num_sims=num_sims) 12 | 13 | 14 | @pytest.fixture 15 | def mmc(): 16 | dist_type = "norm" 17 | num_points = 100000 18 | num_sims = 10 19 | num_rvs = 3 20 | return MultivariateMonteCarlo( 21 | dist_type=dist_type, 22 | num_points=num_points, 23 | num_sims=num_sims, 24 | num_rvs=num_rvs, 25 | ) 26 | -------------------------------------------------------------------------------- /.github/workflows/title.yaml: -------------------------------------------------------------------------------- 1 | name: PR Title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | check: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | 17 | - name: Setup Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: 3.8 21 | 22 | - name: Install Dependencies 23 | run: pip install commitizen 24 | 25 | - name: Check PR Title 26 | env: 27 | TITLE: ${{ github.event.pull_request.title }} 28 | run: cz check --message "$TITLE" 29 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yaml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Commit Message 4 | 5 | # NOTE: Skip check on PR so as not to confuse contributors 6 | # NOTE: Also install a PR title checker so we don't mess up merges 7 | jobs: 8 | check: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | 16 | - name: Setup Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: 3.8 20 | 21 | - name: Install Dependencies 22 | run: pip install .[dev] 23 | 24 | - name: Check commit history 25 | run: cz check --rev-range $(git rev-list --all --reverse | head -1)..HEAD 26 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.2.0 4 | hooks: 5 | - id: check-yaml 6 | 7 | - repo: https://github.com/pre-commit/mirrors-isort 8 | rev: v5.10.1 9 | hooks: 10 | - id: isort 11 | 12 | - repo: https://github.com/psf/black 13 | rev: 22.6.0 14 | hooks: 15 | - id: black 16 | name: black 17 | 18 | - repo: https://gitlab.com/pycqa/flake8 19 | rev: 4.0.1 20 | hooks: 21 | - id: flake8 22 | 23 | - repo: https://github.com/pre-commit/mirrors-mypy 24 | rev: v0.971 25 | hooks: 26 | - id: mypy 27 | additional_dependencies: [types-requests] 28 | 29 | 30 | default_language_version: 31 | python: python3.8 32 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: 3.8 19 | 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -e .[release] 24 | 25 | - name: Build 26 | run: python setup.py sdist bdist_wheel 27 | 28 | - name: Publish 29 | env: 30 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 31 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 32 | run: twine upload dist/* --verbose 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Request a new feature, or an improvement to existing functionality. 4 | labels: 'enhancement' 5 | --- 6 | 7 | ### Overview 8 | 9 | Provide a simple overview of what you wish to see added. Please include: 10 | 11 | * What you are trying to do 12 | * Why Ape's current functionality is inadequate to address your goal 13 | 14 | ### Specification 15 | 16 | Describe the syntax and semantics of how you would like to see this feature implemented. The more detailed the better! 17 | 18 | Remember, your feature is much more likely to be included if it does not involve any breaking changes. 19 | 20 | ### Dependencies 21 | 22 | Include links to any open issues that must be resolved before this feature can be implemented. 23 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: '$RESOLVED_VERSION' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | 4 | categories: 5 | - title: 'Features' 6 | labels: 7 | - 'feat' 8 | - title: 'Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - title: 'Other updates' 12 | labels: 13 | - 'refactor' 14 | - 'chore' 15 | - 'docs' 16 | 17 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 18 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 19 | 20 | version-resolver: 21 | major: 22 | labels: 23 | - 'major' 24 | minor: 25 | labels: 26 | - 'minor' 27 | patch: 28 | labels: 29 | - 'patch' 30 | default: patch 31 | 32 | template: | 33 | ## Changes 34 | 35 | $CHANGES 36 | 37 | Special thanks to: $CONTRIBUTORS 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an error that you've encountered. 4 | labels: 'bug' 5 | --- 6 | ### Environment information 7 | 8 | * `ape` and plugin versions: 9 | 10 | ``` 11 | $ ape --version 12 | # ...copy and paste result of above command here... 13 | 14 | $ ape plugins list 15 | # ...copy and paste result of above command here... 16 | ``` 17 | 18 | * Python Version: x.x.x 19 | * OS: osx/linux/win 20 | 21 | ### What went wrong? 22 | 23 | Please include information like: 24 | 25 | * what command you ran 26 | * the code that caused the failure (see [this link](https://help.github.com/articles/basic-writing-and-formatting-syntax/) for help with formatting code) 27 | * full output of the error you received 28 | 29 | ### How can it be fixed? 30 | 31 | Fill this in if you have ideas on how the bug could be fixed. 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=51.1.1", "wheel", "setuptools_scm[toml]>=5.0"] 3 | 4 | [tool.mypy] 5 | exclude = "build/" 6 | 7 | [tool.setuptools_scm] 8 | write_to = "ape_risk/version.py" 9 | 10 | # NOTE: you have to use single-quoted strings in TOML for regular expressions. 11 | # It's the equivalent of r-strings in Python. Multiline strings are treated as 12 | # verbose regular expressions by Black. Use [ ] to denote a significant space 13 | # character. 14 | 15 | [tool.black] 16 | line-length = 100 17 | target-version = ['py38', 'py39', 'py310'] 18 | include = '\.pyi?$' 19 | 20 | [tool.pytest.ini_options] 21 | addopts = """ 22 | -n auto 23 | -p no:ape_test 24 | --cov-branch 25 | --cov-report term 26 | --cov-report html 27 | --cov-report xml 28 | --cov=ape_risk 29 | """ 30 | python_files = "test_*.py" 31 | testpaths = "tests" 32 | markers = "fuzzing: Run Hypothesis fuzz test suite" 33 | 34 | [tool.isort] 35 | line_length = 100 36 | force_grid_wrap = 0 37 | include_trailing_comma = true 38 | multi_line_output = 3 39 | use_parentheses = true 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/work-item.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Work item 3 | about: New work item for Ape team 4 | labels: 'backlog' 5 | 6 | --- 7 | 8 | ### Elevator pitch: 9 | 10 | 11 | ### Value: 12 | 17 | 18 | ### Dependencies: 19 | 20 | 21 | ### Design approach: 22 | 26 | 27 | ### Task list: 28 | 29 | * [ ] Tasks go here 30 | 31 | ### Estimated completion date: 32 | 33 | 34 | ### Design review: 35 | 36 | Do not signoff unless: 37 | - 1) agreed the tasks and design approach will achieve acceptance, and 38 | - 2) the work can be completed by one person within the SLA. 39 | Design reviewers should consider simpler approaches to achieve goals. 40 | 41 | (Please leave a comment to sign off) 42 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | To get started with working on the codebase, use the following steps prepare your local environment: 4 | 5 | ```bash 6 | # clone the github repo and navigate into the folder 7 | git clone https://github.com/ApeWorX/ape-risk.git 8 | cd ape-risk 9 | 10 | # create and load a virtual environment 11 | python3 -m venv venv 12 | source venv/bin/activate 13 | 14 | # install ape-risk into the virtual environment 15 | python setup.py install 16 | 17 | # install the developer dependencies (-e is interactive mode) 18 | pip install -e .'[dev]' 19 | ``` 20 | 21 | ## Pre-Commit Hooks 22 | 23 | We use [`pre-commit`](https://pre-commit.com/) hooks to simplify linting and ensure consistent formatting among contributors. 24 | Use of `pre-commit` is not a requirement, but is highly recommended. 25 | 26 | Install `pre-commit` locally from the root folder: 27 | 28 | ```bash 29 | pip install pre-commit 30 | pre-commit install 31 | ``` 32 | 33 | Committing will now automatically run the local hooks and ensure that your commit passes all lint checks. 34 | 35 | ## Pull Requests 36 | 37 | Pull requests are welcomed! Please adhere to the following: 38 | 39 | - Ensure your pull request passes our linting checks 40 | - Include test cases for any new functionality 41 | - Include any relevant documentation updates 42 | 43 | It's a good idea to make pull requests early on. 44 | A pull request represents the start of a discussion, and doesn't necessarily need to be the final, finished submission. 45 | 46 | If you are opening a work-in-progress pull request to verify that it passes CI tests, please consider 47 | [marking it as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests). 48 | 49 | Join the Ethereum Python [Discord](https://discord.gg/PcEJ54yX) if you have any questions. 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quick Start 2 | 3 | Tools for analyzing risk in DeFi. 4 | 5 | ## Dependencies 6 | 7 | * [python3](https://www.python.org/downloads) version 3.8 or greater, python3-dev 8 | 9 | ## Installation 10 | 11 | ### via `pip` 12 | 13 | You can install the latest release via [`pip`](https://pypi.org/project/pip/): 14 | 15 | ```bash 16 | pip install ape-risk 17 | ``` 18 | 19 | ### via `setuptools` 20 | 21 | You can clone the repository and use [`setuptools`](https://github.com/pypa/setuptools) for the most up-to-date version: 22 | 23 | ```bash 24 | git clone https://github.com/ApeWorX/ape-risk.git 25 | cd ape-risk 26 | python3 setup.py install 27 | ``` 28 | 29 | ## Quick Usage 30 | 31 | Provides [hypothesis](https://github.com/HypothesisWorks/hypothesis) strategies to use in fuzz tests. 32 | 33 | e.g. Mock prices generated by [Geometric Brownian motion](https://en.wikipedia.org/wiki/Geometric_Brownian_motion): 34 | 35 | ```python 36 | import numpy as np 37 | from ape_risk import strategies 38 | 39 | 40 | @given(strategies.gbms(initial_value=1.0, num_points=100000, params=[0, 0.005])) 41 | def test_gbms_param_fuzz(p): 42 | # strat gives a numpy.ndarray of simulated prices for each hypothesis run 43 | assert p.shape == (100000, 1) 44 | assert isinstance(p, np.ndarray) 45 | 46 | 47 | C = np.asarray([[1, 0.5, 0.8], [0.5, 1, 0.4], [0.8, 0.4, 1]]) 48 | scale = np.linalg.cholesky(C).tolist() 49 | 50 | 51 | @given(strategies.multi_gbms(initial_values=[1.0, 0.9, 0.8], num_points=100000, num_rvs=3, params=[0, 0.005], scale=scale, shift=[0, 0, 0])) 52 | def test_multi_gbms_param_fuzz(p): 53 | # strat gives a numpy.ndarray of multiple simulated prices for each hypothesis run 54 | assert p.shape == (100000, 1, 3) 55 | assert isinstance(p, np.ndarray) 56 | ``` 57 | 58 |  59 | 60 | ## Development 61 | 62 | This project is in development and should be considered an alpha. 63 | Things might not be in their final state and breaking changes may occur. 64 | Comments, questions, criticisms and pull requests are welcomed. 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.swp 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | pip-wheel-metadata/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 92 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 93 | # install all needed dependencies. 94 | #Pipfile.lock 95 | 96 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # mkdocs documentation 113 | /site 114 | 115 | # mypy 116 | .mypy_cache/ 117 | .dmypy.json 118 | dmypy.json 119 | 120 | # setuptools-scm 121 | version.py 122 | 123 | # Ape stuff 124 | .build/ 125 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: ["push", "pull_request"] 2 | 3 | name: Test 4 | 5 | jobs: 6 | linting: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Setup Python 13 | uses: actions/setup-python@v2 14 | with: 15 | python-version: 3.8 16 | 17 | - name: Install Dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install .[lint] 21 | 22 | - name: Run Black 23 | run: black --check . 24 | 25 | - name: Run flake8 26 | run: flake8 . 27 | 28 | - name: Run isort 29 | run: isort --check-only . 30 | 31 | type-check: 32 | runs-on: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Setup Python 38 | uses: actions/setup-python@v2 39 | with: 40 | python-version: 3.8 41 | 42 | - name: Install Dependencies 43 | run: | 44 | python -m pip install --upgrade pip 45 | pip install .[lint,test] 46 | 47 | - name: Run MyPy 48 | run: mypy . 49 | 50 | functional: 51 | runs-on: ${{ matrix.os }} 52 | 53 | strategy: 54 | matrix: 55 | os: [ubuntu-latest, macos-latest] # eventually add `windows-latest` 56 | python-version: [3.8, 3.9, "3.10"] 57 | 58 | steps: 59 | - uses: actions/checkout@v2 60 | 61 | - name: Setup Python 62 | uses: actions/setup-python@v2 63 | with: 64 | python-version: ${{ matrix.python-version }} 65 | 66 | - name: Install Dependencies 67 | run: | 68 | python -m pip install --upgrade pip 69 | pip install .[test] 70 | 71 | - name: Run Tests 72 | run: pytest -m "not fuzzing" -n 0 -s --cov 73 | 74 | # NOTE: uncomment this block after you've marked tests with @pytest.mark.fuzzing 75 | # fuzzing: 76 | # runs-on: ubuntu-latest 77 | # 78 | # strategy: 79 | # fail-fast: true 80 | # 81 | # steps: 82 | # - uses: actions/checkout@v2 83 | # 84 | # - name: Setup Python 85 | # uses: actions/setup-python@v2 86 | # with: 87 | # python-version: 3.8 88 | # 89 | # - name: Install Dependencies 90 | # run: pip install .[test] 91 | # 92 | # - name: Run Tests 93 | # run: pytest -m "fuzzing" --no-cov -s --hypothesis-show-statistics 94 | -------------------------------------------------------------------------------- /tests/stats/test_montecarlo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from scipy import stats # type: ignore 4 | 5 | 6 | def test_dist(mc): 7 | # test succeeds when dist type supported by stats 8 | assert mc.dist == getattr(stats, mc.dist_type) 9 | 10 | # test fails when not supported by stats 11 | mc.dist_type = "fake_dist" 12 | with pytest.raises(Exception): 13 | _ = mc.dist 14 | 15 | 16 | def test_rv(mc): 17 | params = [0.1, 0.001] # loc, scale 18 | 19 | # test rv throws when not frozen 20 | with pytest.raises(Exception): 21 | mc.rv 22 | 23 | # test rv does not throw after frozen and matches expected 24 | mc.freeze(params) 25 | rv = mc.dist(*params) 26 | np.testing.assert_equal(mc.rv.dist._updated_ctor_param(), rv.dist._updated_ctor_param()) 27 | np.testing.assert_equal(mc.rv.args, rv.args) 28 | 29 | 30 | def test_params(mc): 31 | # freeze the dist for a rv to sample from 32 | params = [0.1, 0.001] # loc, scale 33 | mc.freeze(params) 34 | 35 | # check params match what was frozen 36 | np.testing.assert_equal(mc.params, params) 37 | 38 | 39 | def test_freeze(mc): 40 | # freeze the dist for a rv to sample from 41 | params = [0.1, 0.001] # loc, scale 42 | mc.freeze(params) 43 | 44 | # expected frozen rv 45 | rv = mc.dist(*params) 46 | 47 | # check private _rv set 48 | np.testing.assert_equal(mc._rv.dist._updated_ctor_param(), rv.dist._updated_ctor_param()) 49 | np.testing.assert_equal(mc._rv.args, rv.args) 50 | 51 | 52 | def test_sims(mc): 53 | # freeze the dist for a rv to sample from 54 | params = [0.1, 0.001] # loc, scale 55 | mc.freeze(params) 56 | 57 | # generate samples and check return shape is correct 58 | nd_samples = mc.sims() 59 | assert isinstance(nd_samples, np.ndarray) 60 | assert nd_samples.shape == (mc.num_points, mc.num_sims) 61 | 62 | # check loc, scale of all samples (assumes iid) 63 | # flatten nd array 64 | size = mc.num_points * mc.num_sims 65 | nd_samples_reshaped = np.reshape(nd_samples, size) 66 | 67 | # fit and check params close 68 | fit_params = mc.dist.fit(nd_samples_reshaped) 69 | np.testing.assert_allclose(fit_params, mc.params, rtol=1e-2) 70 | 71 | 72 | def test_fit(mc): 73 | # freeze the dist for a rv to sample from 74 | params = [0.1, 0.001] # loc, scale 75 | mc.freeze(params) 76 | 77 | new_params = list(np.asarray(params) * 0.5) # new params 78 | 79 | # generate data 80 | size = mc.num_points * mc.num_sims 81 | nd_samples = mc.dist.rvs(*new_params, size=size) 82 | 83 | # fit mc.params to data 84 | mc.fit(nd_samples) 85 | 86 | # check mc.params refit to close to new params 87 | np.testing.assert_allclose(new_params, mc.params, rtol=1e-2) 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | from setuptools import find_packages, setup # type: ignore 4 | 5 | extras_require = { 6 | "test": [ # `test` GitHub Action jobs uses this 7 | "pytest>=6.0", # Core testing package 8 | "pytest-xdist", # multi-process runner 9 | "pytest-cov", # Coverage analyzer plugin 10 | ], 11 | "lint": [ 12 | "black>=22.6.0", # auto-formatter and linter 13 | "mypy>=0.971", # Static type analyzer 14 | "flake8>=4.0.1", # Style linter 15 | "isort>=5.10.1", # Import sorting linter 16 | ], 17 | "release": [ # `release` GitHub Action job uses this 18 | "setuptools<60.0", # Installation tool 19 | "wheel", # Packaging tool 20 | "twine", # Package upload tool 21 | ], 22 | "dev": [ 23 | "commitizen", # Manage commits and publishing releases 24 | "pre-commit", # Ensure that linters are run prior to commiting 25 | "pytest-watch", # `ptw` test watcher/runner 26 | "IPython", # Console for interacting 27 | "ipdb", # Debugger (Must use `export PYTHONBREAKPOINT=ipdb.set_trace`) 28 | "matplotlib", # plots 29 | ], 30 | } 31 | 32 | # NOTE: `pip install -e .[dev]` to install package 33 | extras_require["dev"] = ( 34 | extras_require["test"] 35 | + extras_require["lint"] 36 | + extras_require["release"] 37 | + extras_require["dev"] 38 | ) 39 | 40 | with open("./README.md") as readme: 41 | long_description = readme.read() 42 | 43 | 44 | setup( 45 | name="ape-risk", 46 | use_scm_version=True, 47 | setup_requires=["setuptools_scm"], 48 | description="""ape-risk: DeFi risk analysis as an ApeWorX plugin""", 49 | long_description=long_description, 50 | long_description_content_type="text/markdown", 51 | author="smolquants", 52 | author_email="dev@smolquants.xyz", 53 | url="https://github.com/smolquants/ape-risk", 54 | include_package_data=True, 55 | install_requires=[ 56 | "eth-ape>=0.6.12,<0.7.0", 57 | "pandas", 58 | "scipy", 59 | "numpy>=1.21,<2.0", 60 | "hypothesis>=6.2.0,<7.0", # Strategy-based fuzzer 61 | ], 62 | python_requires=">=3.8,<4", 63 | extras_require=extras_require, 64 | py_modules=["ape_risk"], 65 | license="Apache-2.0", 66 | zip_safe=False, 67 | keywords="ethereum", 68 | packages=find_packages(exclude=["tests", "tests.*"]), 69 | package_data={"ape_risk": ["py.typed"]}, 70 | classifiers=[ 71 | "Development Status :: 5 - Production/Stable", 72 | "Intended Audience :: Developers", 73 | "License :: OSI Approved :: Apache Software License", 74 | "Natural Language :: English", 75 | "Operating System :: MacOS", 76 | "Operating System :: POSIX", 77 | "Programming Language :: Python :: 3", 78 | "Programming Language :: Python :: 3.8", 79 | "Programming Language :: Python :: 3.9", 80 | "Programming Language :: Python :: 3.10", 81 | "Programming Language :: Python :: 3.11", 82 | ], 83 | ) 84 | -------------------------------------------------------------------------------- /ape_risk/strategies/simulation.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | from hypothesis import strategies as st 6 | from hypothesis.internal.conjecture.data import ConjectureData 7 | 8 | from ape_risk.stats import MonteCarlo, MultivariateMonteCarlo 9 | 10 | 11 | class SimulationStrategy(st.SearchStrategy): 12 | """ 13 | Monte Carlo simulation strategy. 14 | """ 15 | 16 | _mc: MonteCarlo 17 | 18 | def __init__( 19 | self, 20 | dist_type: str, 21 | num_points: int, 22 | params: List, 23 | hist_data: Optional[List] = None, 24 | ): 25 | # init the monte carlo simulator 26 | self._mc = MonteCarlo( 27 | dist_type=dist_type, 28 | num_points=num_points, 29 | num_sims=1, # for each hypothesis run to be a single sim 30 | ) 31 | self._mc.freeze(params) 32 | 33 | # fit params to historical data if given 34 | if hist_data is not None: 35 | if self._mc.num_points > len(hist_data): 36 | raise ValueError( 37 | f"Sample size {self._mc.num_points} not in expected range 0 <= num_points <= {len(hist_data)}" # noqa: E501 38 | ) 39 | 40 | self._mc.fit(np.asarray(hist_data)) 41 | 42 | def do_draw(self, data: ConjectureData) -> npt.ArrayLike: 43 | """ 44 | Draws a new Monte Carlo simulation using a 32-bit generated seed. 45 | 46 | Args: 47 | data (:class:`hypothesis.internal.conjecture.data.ConjectureData`): 48 | The conjecture data for the draw. 49 | 50 | Returns: 51 | numpy.typing.ArrayLike 52 | """ 53 | seed = data.draw_bits(32) # 32 bits is max for seed to np.random.seed 54 | np.random.seed( 55 | seed 56 | ) # TODO: fix since not best practice; see: https://numpy.org/devdocs/reference/random/generated/numpy.random.RandomState.seed.html # noqa: E501 57 | return self._mc.sims() 58 | 59 | 60 | class MultivariateSimulationStrategy(st.SearchStrategy): 61 | """ 62 | Multivariate Monte Carlo simulation strategy. 63 | """ 64 | 65 | _mmc: MultivariateMonteCarlo 66 | 67 | def __init__( 68 | self, 69 | dist_type: str, 70 | num_points: int, 71 | num_rvs: int, 72 | params: List, 73 | scale: List[List], 74 | shift: List, 75 | hist_data: Optional[List[List]] = None, 76 | ): 77 | # init the monte carlo simulator 78 | self._mmc = MultivariateMonteCarlo( 79 | dist_type=dist_type, 80 | num_points=num_points, 81 | num_sims=1, # for each hypothesis run to be a single sim 82 | num_rvs=num_rvs, 83 | ) 84 | self._mmc.freeze(params) 85 | self._mmc.mix(np.asarray(scale), np.asarray(shift)) 86 | 87 | # fit scale, shift to historical data if given 88 | if hist_data is not None: 89 | hist_size = np.asarray(hist_data).shape[0] 90 | if self._mmc.num_points > hist_size: 91 | raise ValueError( 92 | f"Sample size {self._mmc.num_points} not in expected range 0 <= num_points <= {hist_size}" # noqa: E501 93 | ) 94 | 95 | self._mmc.fit(np.asarray(hist_data)) 96 | 97 | def do_draw(self, data: ConjectureData) -> npt.ArrayLike: 98 | """ 99 | Draws a new multivariate Monte Carlo simulation using a 32-bit generated seed. 100 | 101 | Args: 102 | data (:class:`hypothesis.internal.conjecture.data.ConjectureData`): 103 | The conjecture data for the draw. 104 | 105 | Returns: 106 | numpy.typing.ArrayLike 107 | """ 108 | seed = data.draw_bits(32) # 32 bits is max for seed to np.random.seed 109 | np.random.seed( 110 | seed 111 | ) # TODO: fix since not best practice; see: https://numpy.org/devdocs/reference/random/generated/numpy.random.RandomState.seed.html # noqa: E501 112 | return self._mmc.sims() 113 | -------------------------------------------------------------------------------- /tests/stats/test_multivariate_montecarlo.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | 5 | def test_scale(mmc): 6 | # test fails when mix not called yet 7 | with pytest.raises(Exception): 8 | _ = mmc.scale 9 | 10 | # test succeeds with manually set private attr 11 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 12 | scale = np.linalg.cholesky(C) 13 | mmc._scale = scale 14 | mmc_scale = mmc.scale 15 | np.testing.assert_equal(mmc_scale, scale) 16 | 17 | 18 | def test_shift(mmc): 19 | # test fails when mix not called yet 20 | with pytest.raises(Exception): 21 | _ = mmc.shift 22 | 23 | # test succeeds with manually set private attr 24 | shift = np.asarray([0.1, 0.2, 0.3]) 25 | mmc._shift = shift 26 | mmc_shift = mmc.shift 27 | np.testing.assert_equal(mmc_shift, shift) 28 | 29 | 30 | def test_mix_sets_properties(mmc): 31 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 32 | scale = np.linalg.cholesky(C) 33 | shift = np.asarray([0.1, 0.2, 0.3]) 34 | mmc.mix(scale, shift) 35 | 36 | np.testing.assert_equal(mmc.scale, scale) 37 | np.testing.assert_equal(mmc.shift, shift) 38 | 39 | 40 | def test_mix_fails_when_invalid_scale_shape(mmc): 41 | C = np.asarray([[1, 0.1], [0.1, 1]]) 42 | scale = np.linalg.cholesky(C) 43 | shift = np.asarray([0.1, 0.2, 0.3]) 44 | with pytest.raises(Exception): 45 | mmc.mix(scale, shift) 46 | 47 | 48 | def test_mix_fails_when_invalid_shift_shape(mmc): 49 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 50 | scale = np.linalg.cholesky(C) 51 | shift = np.asarray([0.1, 0.2]) 52 | with pytest.raises(Exception): 53 | mmc.mix(scale, shift) 54 | 55 | 56 | def test_sims(mmc): 57 | # freeze the dist for a rv to sample from 58 | params = [0, 0.001] # loc, scale 59 | mmc.freeze(params) 60 | 61 | # mix the dist for multiple correlated rvs 62 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 63 | scale = np.linalg.cholesky(C) 64 | shift = np.asarray([0.1, 0.2, 0.3]) 65 | mmc.mix(scale, shift) 66 | 67 | # generate samples and check return shape is correct 68 | nd_samples = mmc.sims() 69 | assert isinstance(nd_samples, np.ndarray) 70 | assert nd_samples.shape == (mmc.num_points, mmc.num_sims, mmc.num_rvs) 71 | 72 | # check shift of all samples (assumes iid) 73 | # flatten nd array 74 | size = mmc.num_points * mmc.num_sims 75 | nd_samples_reshaped = np.reshape(nd_samples, (size, mmc.num_rvs)) 76 | 77 | # check mean for shift (since norm) 78 | # TODO: update when non-norm allowed 79 | fit_shift = np.mean(nd_samples_reshaped, axis=0) 80 | np.testing.assert_allclose(fit_shift, mmc.shift, rtol=1e-2) 81 | 82 | # check covariance for scale (since norm) 83 | # TODO: update when non-norm allowed 84 | fit_C = np.cov(nd_samples_reshaped.T) / params[1] ** 2 85 | fit_scale = np.linalg.cholesky(fit_C) 86 | np.testing.assert_allclose(fit_scale, mmc.scale, rtol=5e-2) 87 | 88 | 89 | def test_fit(mmc): 90 | # freeze the dist for a rv to sample from 91 | params = [0, 0.001] # loc, scale 92 | mmc.freeze(params) 93 | 94 | # mix the dist for multiple correlated rvs 95 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 96 | scale = np.linalg.cholesky(C) 97 | shift = np.asarray([0.1, 0.2, 0.3]) 98 | mmc.mix(scale, shift) 99 | 100 | new_params = list(np.asarray(params) * 0.5) # new params 101 | new_scale = scale * 0.5 102 | new_shift = shift * 0.5 103 | 104 | # generate data 105 | size = mmc.num_points * mmc.num_sims 106 | x = mmc.dist.rvs(*new_params, size=(size, mmc.num_rvs)) 107 | 108 | # affine transform rvs with new scale and shift 109 | nd_samples = np.einsum("ij,nj->ni", new_scale, x) + new_shift 110 | 111 | # fit shift, scale to data 112 | mmc.fit(nd_samples) 113 | 114 | # check mmc.params refit to standard 115 | # TODO: update when non-norm allowed 116 | np.testing.assert_equal([0, 1], mmc.params) 117 | 118 | # adjust expected scale for initial new_params[1] scale 119 | new_scale_mixed = new_scale * new_params[1] 120 | 121 | # check mmc.scale, mmc.shift refit to close to new shift, scale 122 | np.testing.assert_allclose(new_scale_mixed, mmc.scale, rtol=5e-2) 123 | np.testing.assert_allclose(new_shift, mmc.shift, rtol=1e-2) 124 | -------------------------------------------------------------------------------- /ape_risk/stats/montecarlo.py: -------------------------------------------------------------------------------- 1 | from typing import ClassVar, Optional, Tuple, Union 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | from pydantic import BaseModel, validator 6 | from scipy import stats # type: ignore 7 | 8 | 9 | class MonteCarlo(BaseModel): 10 | """ 11 | Monte carlo simulator. 12 | 13 | Attrs: 14 | dist_type (str): The continuous distribution to sample from. 15 | num_points (int): The number of points to generate for each sim. 16 | num_sims (int): The number of sims. 17 | """ 18 | 19 | dist_type: str 20 | num_points: int 21 | num_sims: int 22 | supported_dist_types: ClassVar[Tuple] = (stats.rv_continuous, stats.rv_discrete) 23 | 24 | _rv: Optional[Union[stats.rv_continuous, stats.rv_discrete]] = None 25 | 26 | class Config: 27 | underscore_attrs_are_private = True 28 | 29 | @validator("dist_type") 30 | def dist_type_supported(cls, v): 31 | d = getattr(stats, v) 32 | assert d is not None and isinstance( 33 | d, cls.supported_dist_types 34 | ), f"dist_type {v} not supported" 35 | return v 36 | 37 | @property 38 | def dist(self) -> Union[stats.rv_continuous, stats.rv_discrete]: 39 | """ 40 | The distribution class of the random variable to sample from. 41 | """ 42 | return getattr(stats, self.dist_type) 43 | 44 | @property 45 | def rv(self) -> Union[stats.rv_continuous, stats.rv_discrete]: 46 | """ 47 | The random variable to sample from. 48 | """ 49 | if self._rv is None: 50 | raise Exception("dist not frozen with rv params") 51 | return self._rv 52 | 53 | @property 54 | def params(self) -> np.ndarray: 55 | """ 56 | The distributional parameters of the random variable to sample from. 57 | """ 58 | return np.asarray(self.rv.args) 59 | 60 | def freeze(self, params: npt.ArrayLike): 61 | """ 62 | Freezes the distribution as a random variable using the given params. 63 | 64 | Args: 65 | params (npt.ArrayLike): The parameter arguments (e.g. loc, scale) of `rv`. 66 | """ 67 | self._rv = self.dist(*params) 68 | 69 | def sims(self) -> npt.ArrayLike: 70 | """ 71 | Generates iid samples from given distribution for size = (num_points, num_sims). 72 | 73 | Returns: 74 | numpy.typing.ArrayLike 75 | """ 76 | return self.rv.rvs(size=(self.num_points, self.num_sims)) 77 | 78 | def fit(self, data: np.ndarray): 79 | """ 80 | Fits distribution params then freezes as random variable using the given data. 81 | 82 | Args: 83 | data (np.ndarray): The data to fit the random variable params from. 84 | """ 85 | params = self.dist.fit(data) 86 | self.freeze(params) 87 | 88 | 89 | class MultivariateMonteCarlo(MonteCarlo): 90 | """ 91 | Multivariate monte carlo simulator. 92 | 93 | Attrs: 94 | dist_type (str): The continuous distribution to sample from. 95 | num_points (int): The number of points to generate for each sim. 96 | num_sims (int): The number of sims. 97 | num_rvs (int): The number of rvs to use for sims. 98 | """ 99 | 100 | num_rvs: int 101 | 102 | _scale: Optional[np.ndarray] = None 103 | _shift: Optional[np.ndarray] = None 104 | 105 | @validator("dist_type") 106 | def dist_type_supported(cls, v): 107 | # TODO: support more dists 108 | assert v == "norm", f"dist_type {v} not supported" 109 | return v 110 | 111 | @property 112 | def scale(self) -> np.ndarray: 113 | if self._scale is None: 114 | raise Exception("dist not mixed with transform properties") 115 | return self._scale 116 | 117 | @property 118 | def shift(self) -> np.ndarray: 119 | if self._shift is None: 120 | raise Exception("dist not mixed with transform properties") 121 | return self._shift 122 | 123 | def mix(self, scale: np.ndarray, shift: np.ndarray): 124 | """ 125 | Sets the mixing transformation properties to use in generating correlated 126 | sims from iid random variables. 127 | 128 | Args: 129 | scale (npt.ArrayLike): The scale matrix. 130 | shift (npt.ArrayLike): The shift vector. 131 | """ 132 | # check shapes 133 | if scale.shape != (self.num_rvs, self.num_rvs): 134 | raise ValueError(f"Scale matrix is not shape of ({self.num_rvs}, {self.num_rvs})") 135 | if shift.shape != (self.num_rvs,): 136 | raise ValueError(f"Shift vector is not shape of (1, {self.num_rvs})") 137 | 138 | self._scale = scale 139 | self._shift = shift 140 | 141 | def sims(self) -> npt.ArrayLike: 142 | """ 143 | Generates correlated samples from given distribution for 144 | size = (num_points, num_sims, num_rvs). 145 | 146 | Returns: 147 | numpy.typing.ArrayLike 148 | """ 149 | x = self.rv.rvs(size=(self.num_points, self.num_sims, self.num_rvs)) 150 | return np.einsum("ij,nmj->nmi", self.scale, x) + self.shift 151 | 152 | def fit(self, data: np.ndarray): 153 | """ 154 | Fits distribution params then freezes as random variable and 155 | mixes with affine transform using the given data. 156 | 157 | Args: 158 | data (np.ndarray): The data to fit the random variable params from. 159 | """ 160 | if data.shape != (data.shape[0], self.num_rvs): 161 | raise ValueError(f"data is not shape of (_, {self.num_rvs})") 162 | 163 | # TODO: generalize to stable family 164 | shift = np.mean(data, axis=0) 165 | C = np.cov(data.T) 166 | scale = np.linalg.cholesky(C) 167 | params = (0, 1) # standard normal 168 | 169 | self.freeze(params) 170 | self.mix(scale, shift) 171 | -------------------------------------------------------------------------------- /tests/strategies/test_core.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from hypothesis import given 3 | from scipy import stats # type: ignore 4 | 5 | from ape_risk import strategies 6 | from ape_risk.stats import MonteCarlo, MultivariateMonteCarlo 7 | 8 | 9 | def hist_data(): 10 | dist_type = "norm" 11 | num_points = 200000 12 | params = [0.001, 0.005] # loc, scale 13 | 14 | mc = MonteCarlo(dist_type=dist_type, num_points=num_points, num_sims=1) 15 | mc.freeze(params) 16 | log_p = mc.sims() 17 | 18 | p0 = 1.0 19 | return (p0 * np.exp(np.cumsum(log_p))).tolist() 20 | 21 | 22 | def scale(): 23 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 24 | return np.linalg.cholesky(C).tolist() 25 | 26 | 27 | def multi_hist_data(): 28 | dist_type = "norm" 29 | num_points = 200000 30 | num_rvs = 3 31 | params = [0, 0.005] # loc, scale 32 | 33 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 34 | scale = np.linalg.cholesky(C) 35 | shift = np.asarray([0.001, 0.002, 0.003]) 36 | 37 | mmc = MultivariateMonteCarlo( 38 | dist_type=dist_type, num_points=num_points, num_rvs=num_rvs, num_sims=1 39 | ) 40 | mmc.freeze(params) 41 | mmc.mix(scale, shift) 42 | log_p = mmc.sims().reshape(num_points, num_rvs) 43 | 44 | p0 = 1.0 45 | return (p0 * np.exp(np.cumsum(log_p, axis=0))).tolist() 46 | 47 | 48 | @given(strategies.sims(dist_type="norm", num_points=100000, params=[0, 1])) 49 | def test_sims_fuzz(s): 50 | assert s.shape == (100000, 1) 51 | assert isinstance(s, np.ndarray) 52 | 53 | 54 | @given( 55 | strategies.multi_sims( 56 | dist_type="norm", 57 | num_points=100000, 58 | num_rvs=3, 59 | params=[0, 1], 60 | scale=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], 61 | shift=[0, 0, 0], 62 | ) 63 | ) 64 | def test_multi_sims_fuzz(s): 65 | assert s.shape == (100000, 1, 3) 66 | assert isinstance(s, np.ndarray) 67 | 68 | 69 | @given(strategies.gbms(initial_value=1.0, num_points=100000, params=[0.001, 0.005])) 70 | def test_gbms_param_fuzz(p): 71 | assert p.shape == (100000, 1) 72 | assert isinstance(p, np.ndarray) 73 | 74 | # check distr of p is close to log normal with params 75 | dlog_p = np.diff(np.log(p.T)) 76 | fit_params = stats.norm.fit(dlog_p) 77 | np.testing.assert_allclose(fit_params, [0.001, 0.005], rtol=2e-1) # mu tol is not great 78 | 79 | 80 | @given( 81 | strategies.gbms( 82 | initial_value=1.0, 83 | num_points=100000, 84 | params=[0.001, 0.005], 85 | r=0.0004, 86 | ) 87 | ) 88 | def test_gbms_param_risk_neutral_fuzz(p): 89 | assert p.shape == (100000, 1) 90 | assert isinstance(p, np.ndarray) 91 | 92 | # check distr of p is close to log normal with params 93 | dlog_p = np.diff(np.log(p.T)) 94 | fit_params = stats.norm.fit(dlog_p) 95 | np.testing.assert_allclose(fit_params, [0.0003875, 0.005], rtol=2e-1) # mu tol is not great 96 | 97 | 98 | @given(strategies.gbms(initial_value=1.0, num_points=100000, params=[0, 1], hist_data=hist_data())) 99 | def test_gbms_hist_fuzz(p): 100 | assert p.shape == (100000, 1) 101 | assert isinstance(p, np.ndarray) 102 | 103 | # check distr of p is close to log normal with params 104 | dlog_p = np.diff(np.log(p.T)) 105 | fit_params = stats.norm.fit(dlog_p) 106 | np.testing.assert_allclose(fit_params, [0.001, 0.005], rtol=2e-1) # mu tol is not great 107 | 108 | 109 | @given( 110 | strategies.multi_gbms( 111 | initial_values=[1.0, 0.9, 0.8], 112 | num_points=100000, 113 | num_rvs=3, 114 | params=[0, 0.005], 115 | scale=scale(), 116 | shift=[0.001, 0.002, 0.003], 117 | ) 118 | ) 119 | def test_multi_gbms_param_fuzz(p): 120 | assert p.shape == (100000, 1, 3) 121 | assert isinstance(p, np.ndarray) 122 | 123 | # check initial values of sim close to same as specified (since start at t=1) 124 | np.testing.assert_allclose(p.reshape((100000, 3))[0], [1.0, 0.9, 0.8], 0.005 * 10) 125 | 126 | # check distr of p is close to log normal with params 127 | dlog_p = np.diff(np.log(p.reshape(100000, 3)), axis=0) 128 | fit_shift = np.mean(dlog_p, axis=0) 129 | C = np.cov(dlog_p.T) 130 | fit_scale = np.linalg.cholesky(C) 131 | 132 | np.testing.assert_allclose( 133 | fit_scale, np.asarray(scale()) * 0.005, rtol=2e-1 134 | ) # cov tol is not great 135 | np.testing.assert_allclose(fit_shift, [0.001, 0.002, 0.003], 2e-1) # mu tol is not great 136 | 137 | 138 | @given( 139 | strategies.multi_gbms( 140 | initial_values=[1.0, 0.9, 0.8], 141 | num_points=100000, 142 | num_rvs=3, 143 | params=[0, 0.005], # TODO: check for non-zero params[0] 144 | scale=scale(), 145 | shift=[0.001, 0.002, 0.003], 146 | r=0.0004, 147 | ) 148 | ) 149 | def test_multi_gbms_param_risk_neutral_fuzz(p): 150 | assert p.shape == (100000, 1, 3) 151 | assert isinstance(p, np.ndarray) 152 | 153 | # check initial values of sim close to same as specified (since start at t=1) 154 | np.testing.assert_allclose(p.reshape((100000, 3))[0], [1.0, 0.9, 0.8], 0.005 * 10) 155 | 156 | # check distr of p is close to log normal with params 157 | dlog_p = np.diff(np.log(p.reshape(100000, 3)), axis=0) 158 | fit_shift = np.mean(dlog_p, axis=0) 159 | C = np.cov(dlog_p.T) 160 | fit_scale = np.linalg.cholesky(C) 161 | 162 | np.testing.assert_allclose( 163 | fit_scale, np.asarray(scale()) * 0.005, rtol=2e-1 164 | ) # cov tol is not great 165 | np.testing.assert_allclose( 166 | fit_shift, [0.0003875, 0.0003875, 0.0003875], 2e-1 167 | ) # mu tol is not great 168 | 169 | 170 | @given( 171 | strategies.multi_gbms( 172 | initial_values=[1.0, 0.9, 0.8], 173 | num_points=100000, 174 | num_rvs=3, 175 | params=[0, 1], 176 | scale=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], 177 | shift=[0, 0, 0], 178 | hist_data=multi_hist_data(), 179 | ) 180 | ) 181 | def test_multi_gbms_hist_fuzz(p): 182 | assert p.shape == (100000, 1, 3) 183 | assert isinstance(p, np.ndarray) 184 | 185 | # check initial values of sim close to same as specified (since start at t=1) 186 | np.testing.assert_allclose(p.reshape((100000, 3))[0], [1.0, 0.9, 0.8], 0.005 * 10) 187 | 188 | # check distr of p is close to log normal with params 189 | dlog_p = np.diff(np.log(p.reshape(100000, 3)), axis=0) 190 | fit_shift = np.mean(dlog_p, axis=0) 191 | C = np.cov(dlog_p.T) 192 | fit_scale = np.linalg.cholesky(C) 193 | 194 | np.testing.assert_allclose( 195 | fit_scale, np.asarray(scale()) * 0.005, rtol=2e-1 196 | ) # cov tol is not great 197 | np.testing.assert_allclose(fit_shift, [0.001, 0.002, 0.003], 2e-1) # mu tol is not great 198 | -------------------------------------------------------------------------------- /tests/strategies/test_simulation.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from hypothesis.internal.conjecture.data import ConjectureData 4 | 5 | from ape_risk.stats import MonteCarlo, MultivariateMonteCarlo 6 | from ape_risk.strategies import MultivariateSimulationStrategy, SimulationStrategy 7 | 8 | 9 | def test_init_without_hist_data(): 10 | dist_type = "norm" 11 | num_points = 100000 12 | params = [0.1, 0.001] # loc, scale 13 | sim_strat = SimulationStrategy( 14 | dist_type=dist_type, 15 | num_points=num_points, 16 | params=params, 17 | ) 18 | 19 | # check internal mc matches params to sim strat 20 | assert sim_strat._mc.dist_type == dist_type 21 | assert sim_strat._mc.num_points == num_points 22 | assert sim_strat._mc.num_sims == 1 23 | np.testing.assert_equal(sim_strat._mc.params, params) 24 | 25 | 26 | def test_init_with_hist_data(): 27 | dist_type = "norm" 28 | num_points = 100000 29 | params = [0.1, 0.001] # loc, scale 30 | 31 | mc = MonteCarlo(dist_type=dist_type, num_points=num_points, num_sims=1) 32 | mc.freeze(params) 33 | hist_data = mc.sims().tolist() 34 | 35 | sim_strat = SimulationStrategy( 36 | dist_type=dist_type, 37 | num_points=num_points, 38 | params=[0, 1], # initial loc, scale 39 | hist_data=hist_data, 40 | ) 41 | 42 | # check internal mc matches params to sim strat 43 | assert sim_strat._mc.dist_type == dist_type 44 | assert sim_strat._mc.num_points == num_points 45 | assert sim_strat._mc.num_sims == 1 46 | 47 | # check params fit from historical are same as norm rv fit 48 | np.testing.assert_allclose(sim_strat._mc.params, params, rtol=1e-2) 49 | 50 | 51 | def test_init_with_hist_data_raises_when_num_points_gt_len_hist(): 52 | dist_type = "norm" 53 | num_points = 100000 54 | params = [0.1, 0.001] # loc, scale 55 | 56 | mc = MonteCarlo(dist_type=dist_type, num_points=num_points, num_sims=1) 57 | mc.freeze(params) 58 | hist_data = mc.sims().tolist() 59 | 60 | with pytest.raises(ValueError): 61 | _ = SimulationStrategy( 62 | dist_type=dist_type, 63 | num_points=num_points + 1, 64 | params=[0, 1], # initial loc, scale 65 | hist_data=hist_data, 66 | ) 67 | 68 | 69 | def test_do_draw(): 70 | dist_type = "norm" 71 | num_points = 100000 72 | params = [0.1, 0.001] # loc, scale 73 | sim_strat = SimulationStrategy( 74 | dist_type=dist_type, 75 | num_points=num_points, 76 | params=params, 77 | ) 78 | 79 | # SEE:https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/conjecture/test_utils.py # noqa: E501 80 | data = ConjectureData.for_buffer(bytes(8)) 81 | draw = sim_strat.do_draw(data=data) 82 | assert draw.shape == (100000, 1) 83 | assert isinstance(draw, np.ndarray) 84 | 85 | # check draw has fit close to params 86 | sim_strat._mc.fit(draw) 87 | np.testing.assert_allclose(sim_strat._mc.params, params, rtol=1e-2) 88 | 89 | 90 | def test_multi_init_without_hist_data(): 91 | dist_type = "norm" 92 | num_points = 100000 93 | num_rvs = 3 94 | params = [0, 0.001] 95 | 96 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 97 | scale = np.linalg.cholesky(C).tolist() 98 | shift = [0.1, 0.2, 0.3] 99 | sim_strat = MultivariateSimulationStrategy( 100 | dist_type=dist_type, 101 | num_points=num_points, 102 | num_rvs=num_rvs, 103 | params=params, 104 | scale=scale, 105 | shift=shift, 106 | ) 107 | 108 | # check internal mmc matches params, scale, shift to sim strat 109 | assert sim_strat._mmc.dist_type == dist_type 110 | assert sim_strat._mmc.num_points == num_points 111 | assert sim_strat._mmc.num_sims == 1 112 | assert sim_strat._mmc.num_rvs == num_rvs 113 | np.testing.assert_equal(sim_strat._mmc.params, params) 114 | np.testing.assert_equal(sim_strat._mmc.scale, scale) 115 | np.testing.assert_equal(sim_strat._mmc.shift, shift) 116 | 117 | 118 | def test_multi_init_with_hist_data(): 119 | dist_type = "norm" 120 | num_points = 100000 121 | num_rvs = 3 122 | params = [0, 0.001] 123 | 124 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 125 | scale = np.linalg.cholesky(C) 126 | shift = np.asarray([0.1, 0.2, 0.3]) 127 | 128 | mmc = MultivariateMonteCarlo( 129 | dist_type=dist_type, 130 | num_points=num_points, 131 | num_sims=1, 132 | num_rvs=num_rvs, 133 | ) 134 | mmc.freeze(params) 135 | mmc.mix(scale, shift) 136 | hist_data = mmc.sims().reshape(num_points, num_rvs).tolist() 137 | 138 | sim_strat = MultivariateSimulationStrategy( 139 | dist_type=dist_type, 140 | num_points=num_points, 141 | num_rvs=num_rvs, 142 | params=[0, 1], 143 | scale=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], 144 | shift=[0, 0, 0], 145 | hist_data=hist_data, 146 | ) 147 | 148 | # check internal mmc matches params to sim strat 149 | np.testing.assert_equal(sim_strat._mmc.params, [0, 1]) 150 | np.testing.assert_allclose(sim_strat._mmc.scale, scale * params[1], rtol=5e-2) 151 | np.testing.assert_allclose(sim_strat._mmc.shift, shift, rtol=5e-2) 152 | 153 | 154 | def test_multi_init_with_hist_data_raises_when_num_points_gt_len_hist(): 155 | dist_type = "norm" 156 | num_points = 100000 157 | num_rvs = 3 158 | params = [0, 0.001] # loc, scale 159 | 160 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 161 | scale = np.linalg.cholesky(C) 162 | shift = np.asarray([0.1, 0.2, 0.3]) 163 | 164 | mmc = MultivariateMonteCarlo( 165 | dist_type=dist_type, 166 | num_points=num_points, 167 | num_sims=1, 168 | num_rvs=num_rvs, 169 | ) 170 | mmc.freeze(params) 171 | mmc.mix(scale, shift) 172 | hist_data = mmc.sims().reshape(num_points, num_rvs).tolist() 173 | 174 | with pytest.raises(ValueError): 175 | _ = MultivariateSimulationStrategy( 176 | dist_type=dist_type, 177 | num_points=num_points + 1, 178 | num_rvs=num_rvs, 179 | params=[0, 1], # initial loc, scale 180 | scale=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], # initial scale 181 | shift=[0, 0, 0], # initial shift 182 | hist_data=hist_data, 183 | ) 184 | 185 | 186 | def test_multi_do_draw(): 187 | dist_type = "norm" 188 | num_points = 100000 189 | num_rvs = 3 190 | params = [0, 0.001] 191 | 192 | C = np.asarray([[1, 0.1, 0.2], [0.1, 1, 0.1], [0.2, 0.1, 1]]) 193 | scale = np.linalg.cholesky(C).tolist() 194 | shift = [0.1, 0.2, 0.3] 195 | sim_strat = MultivariateSimulationStrategy( 196 | dist_type=dist_type, 197 | num_points=num_points, 198 | num_rvs=num_rvs, 199 | params=params, 200 | scale=scale, 201 | shift=shift, 202 | ) 203 | 204 | # SEE:https://github.com/HypothesisWorks/hypothesis/blob/master/hypothesis-python/tests/conjecture/test_utils.py # noqa: E501 205 | data = ConjectureData.for_buffer(bytes(8)) 206 | draw = sim_strat.do_draw(data=data) 207 | assert draw.shape == (100000, 1, 3) 208 | assert isinstance(draw, np.ndarray) 209 | 210 | # check draw has fit close to params 211 | sim_strat._mmc.fit(draw.reshape(100000, 3)) 212 | np.testing.assert_equal(sim_strat._mmc.params, [0, 1]) 213 | np.testing.assert_allclose(sim_strat._mmc.scale, np.asarray(scale) * params[1], rtol=5e-2) 214 | np.testing.assert_allclose(sim_strat._mmc.shift, np.asarray(shift), rtol=5e-2) 215 | -------------------------------------------------------------------------------- /ape_risk/strategies/core.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | import numpy as np 4 | import numpy.typing as npt 5 | from hypothesis import strategies as st 6 | from hypothesis.internal.validation import check_type 7 | from hypothesis.strategies._internal.utils import cacheable, defines_strategy 8 | 9 | 10 | @cacheable 11 | @defines_strategy() 12 | def sims( 13 | *, 14 | dist_type: str, 15 | num_points: int, 16 | params: List, 17 | hist_data: Optional[List] = None, 18 | ) -> st.SearchStrategy[npt.ArrayLike]: 19 | """ 20 | Generates instances of ``np.ndarray``. The generated random instances 21 | are individual runs of a Monte Carlo sim driven by the 22 | specified distribution. 23 | 24 | Args: 25 | dist_type (str): The continuous distribution to sample from. 26 | num_points (int): The number of points to generate for each sim. 27 | params (List): The parameter arguments (e.g. loc, scale) of the random variable. 28 | hist_data (Optional[List]): Historical data to fit the random variable params from. 29 | 30 | Returns: 31 | :class:`hypothesis.strategies.SearchStrategy[numpy.typing.ArrayLike]` 32 | """ 33 | check_type(str, dist_type, "dist_type") 34 | check_type(int, num_points, "num_points") 35 | check_type(list, params, "params") 36 | if hist_data is not None: 37 | check_type(list, hist_data, "hist_data") 38 | 39 | from ape_risk.strategies.simulation import SimulationStrategy 40 | 41 | return SimulationStrategy( 42 | dist_type=dist_type, num_points=num_points, params=params, hist_data=hist_data 43 | ) 44 | 45 | 46 | @cacheable 47 | @defines_strategy() 48 | def multi_sims( 49 | *, 50 | dist_type: str, 51 | num_points: int, 52 | num_rvs: int, 53 | params: List, 54 | scale: List[List], 55 | shift: List, 56 | hist_data: Optional[List[List]] = None, 57 | ) -> st.SearchStrategy[npt.ArrayLike]: 58 | """ 59 | Generates instances of ``np.ndarray``. The generated random instances 60 | are individual runs of a multivariate Monte Carlo sim driven by the 61 | specified distribution, with covariance determined by the given 62 | scale matrix (Cholesky decomposition of covariance matrix). 63 | 64 | Args: 65 | dist_type (str): The continuous distribution to sample from. 66 | num_points (int): The number of points to generate for each sim. 67 | params (List): The base parameter arguments (e.g. loc, scale) of the random variables. 68 | scale (List[List]): The scale matrix to mix random variables via affine transformation. 69 | shift (List): The shift vector to translate random variables via affine transformation. 70 | hist_data (Optional[List[List]]): Historical data to fit the random variable params from. 71 | 72 | Returns: 73 | :class:`hypothesis.strategies.SearchStrategy[numpy.typing.ArrayLike]` 74 | """ 75 | check_type(str, dist_type, "dist_type") 76 | check_type(int, num_points, "num_points") 77 | check_type(int, num_rvs, "num_rvs") 78 | check_type(list, params, "params") 79 | check_type(list, scale, "scale") 80 | check_type(list, shift, "shift") 81 | if hist_data is not None: 82 | check_type(list, hist_data, "hist_data") 83 | 84 | from ape_risk.strategies.simulation import MultivariateSimulationStrategy 85 | 86 | return MultivariateSimulationStrategy( 87 | dist_type=dist_type, 88 | num_points=num_points, 89 | num_rvs=num_rvs, 90 | params=params, 91 | scale=scale, 92 | shift=shift, 93 | hist_data=hist_data, 94 | ) 95 | 96 | 97 | @cacheable 98 | @defines_strategy() 99 | def gbms( 100 | *, 101 | initial_value: float, 102 | num_points: int, 103 | params: List, 104 | hist_data: Optional[List] = None, 105 | r: Optional[float] = None, 106 | ) -> st.SearchStrategy[npt.ArrayLike]: 107 | """ 108 | Generates instances of ``np.ndarray``. The generated random instances 109 | are individual runs of a Monte Carlo sim driven by Geometric Brownian motion. 110 | 111 | Args: 112 | initial_value (float): The initial value of each sim. 113 | num_points (int): The number of points to generate for each sim. 114 | params (List): The parameter arguments (e.g. loc, scale) of the random variable. 115 | hist_data (Optional[List]): Historical data to fit the random variable params from. 116 | r (Optional[float]): The risk-neutral interest rate. When not None, 117 | adjusts sims to risk-neutral measure. 118 | 119 | Returns: 120 | :class:`hypothesis.strategies.SearchStrategy[numpy.typing.ArrayLike]` 121 | """ 122 | check_type(float, initial_value, "initial_value") 123 | check_type(int, num_points, "num_points") 124 | check_type(list, params, "params") 125 | if hist_data is not None: 126 | check_type(list, hist_data, "hist_data") 127 | 128 | # fit to log differences: log(p[i+1] / p[i]) 129 | hist_data = np.diff(np.log(np.asarray(hist_data))).tolist() 130 | 131 | from ape_risk.strategies.simulation import SimulationStrategy 132 | 133 | strat = SimulationStrategy( 134 | dist_type="norm", num_points=num_points, params=params, hist_data=hist_data 135 | ) 136 | 137 | s = 0.0 138 | if r is not None: 139 | check_type(float, r, "r") 140 | 141 | # adjust for risk-neutral if given risk-free rate 142 | # S_t = S_0 * exp(mu_p * t + sigma * W_t); mu_p = mu - sigma**2 / 2 143 | # dS_t = mu * S_t * dt + sigma * S_t * dW_t 144 | [mu_p, sigma] = strat._mc.params.tolist() 145 | mu = mu_p + sigma**2 / 2 146 | s = r - mu 147 | 148 | def pack(x: npt.ArrayLike) -> npt.ArrayLike: 149 | return initial_value * np.exp(np.cumsum(np.add(x, s), axis=0)) # axis=0 sums over rows 150 | 151 | return strat.map(pack) 152 | 153 | 154 | @cacheable 155 | @defines_strategy() 156 | def multi_gbms( 157 | *, 158 | initial_values: List[float], 159 | num_points: int, 160 | num_rvs: int, 161 | params: List, 162 | scale: List[List], 163 | shift: List, 164 | hist_data: Optional[List[List]] = None, 165 | r: Optional[float] = None, 166 | ) -> st.SearchStrategy[npt.ArrayLike]: 167 | """ 168 | Generates instances of ``np.ndarray``. The generated random instances 169 | are individual runs of a multivariate Monte Carlo sim driven by 170 | correlated Geometric Brownian motions. 171 | 172 | Args: 173 | initial_value (List[float]): The initial values of the random variables for each sim. 174 | num_points (int): The number of points to generate for each sim. 175 | num_rvs (int): The number of random variables for each sim. 176 | params (List): The base parameter arguments (e.g. loc, scale) of the random variables. 177 | scale (List[List]): The scale matrix to mix random variables via affine transformation. 178 | shift (List): The shift vector to translate random variables via affine transformation. 179 | hist_data (Optional[List[List]]): Historical data to fit the random variable params from. 180 | r (Optional[float]): The risk-neutral interest rate. When not None, 181 | adjusts sims to risk-neutral measure. 182 | 183 | Returns: 184 | :class:`hypothesis.strategies.SearchStrategy[numpy.typing.ArrayLike]` 185 | """ 186 | check_type(list, initial_values, "initial_values") 187 | check_type(int, num_points, "num_points") 188 | check_type(int, num_rvs, "num_rvs") 189 | check_type(list, params, "params") 190 | check_type(list, scale, "scale") 191 | check_type(list, shift, "shift") 192 | 193 | if len(initial_values) != num_rvs: 194 | raise ValueError("Length of initial_values not same as num_rvs") 195 | 196 | if hist_data is not None: 197 | check_type(list, hist_data, "hist_data") 198 | 199 | # fit to log differences: log(p[i+1] / p[i]) 200 | hist_data = np.diff(np.log(np.asarray(hist_data)), axis=0).tolist() 201 | 202 | from ape_risk.strategies.simulation import MultivariateSimulationStrategy 203 | 204 | strat = MultivariateSimulationStrategy( 205 | dist_type="norm", 206 | num_points=num_points, 207 | num_rvs=num_rvs, 208 | params=params, 209 | scale=scale, 210 | shift=shift, 211 | hist_data=hist_data, 212 | ) 213 | 214 | s = np.zeros( 215 | shape=( 216 | len( 217 | shift, 218 | ) 219 | ) 220 | ) 221 | if r is not None: 222 | check_type(float, r, "r") 223 | 224 | # adjust for risk-neutral if given risk-free rate 225 | # S_t = S_0 * exp(mu_p * t + sigma * W_t); mu_p = mu - sigma**2 / 2 226 | # dS_t = mu * S_t * dt + sigma * S_t * dW_t 227 | # mu_p_i = shift_i + params.loc * sum_j scale_{ij} 228 | # sigma_i**2 = params.scale**2 * sum_j scale**2_{ij} 229 | [mu_p, sigma] = strat._mmc.params.tolist() 230 | 231 | mus_p = np.ones(shape=(len(shift),)) * mu_p 232 | sigmas = np.ones(shape=(len(shift),)) * sigma 233 | mmc_shift = strat._mmc.shift # transformed to np.ndarrays 234 | mmc_scale = strat._mmc.scale 235 | 236 | # elementwise squares 237 | sigmas_sqrd = np.square(sigmas) 238 | mmc_scale_sqrd = np.square(mmc_scale) 239 | 240 | mus = ( 241 | mmc_shift 242 | + np.einsum("j,ij", mus_p, mmc_scale) 243 | + np.einsum("j,ij", sigmas_sqrd, mmc_scale_sqrd) / 2.0 244 | ) 245 | s = r - mus 246 | 247 | def pack(x: npt.ArrayLike) -> npt.ArrayLike: 248 | return initial_values * np.exp(np.cumsum(np.add(x, s), axis=0)) # axis=0 sums over rows 249 | 250 | return strat.map(pack) 251 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 ApeWorX Ltd. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /notebook/example.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 1, 6 | "id": "47d49e4d", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import pandas as pd\n", 11 | "from ape_risk import strategies" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 2, 17 | "id": "4cf2a7c7", 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "strat = strategies.gbms(initial_value=1.0, num_points=10000, params=[0, 0.005])" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 3, 27 | "id": "28ae7f56", 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "data": { 32 | "text/plain": [ 33 | "array([[0.99394044],\n", 34 | " [0.9932313 ],\n", 35 | " [0.98968565],\n", 36 | " ...,\n", 37 | " [1.47617896],\n", 38 | " [1.4845554 ],\n", 39 | " [1.48961519]])" 40 | ] 41 | }, 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "output_type": "execute_result" 45 | } 46 | ], 47 | "source": [ 48 | "ex = strat.example()\n", 49 | "ex" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 4, 55 | "id": "61101357", 56 | "metadata": {}, 57 | "outputs": [], 58 | "source": [ 59 | "df = pd.DataFrame(ex, columns=['price'])" 60 | ] 61 | }, 62 | { 63 | "cell_type": "code", 64 | "execution_count": 5, 65 | "id": "f179f105", 66 | "metadata": {}, 67 | "outputs": [ 68 | { 69 | "data": { 70 | "text/html": [ 71 | "
| \n", 89 | " | price | \n", 90 | "
|---|---|
| 0 | \n", 95 | "0.993940 | \n", 96 | "
| 1 | \n", 99 | "0.993231 | \n", 100 | "
| 2 | \n", 103 | "0.989686 | \n", 104 | "
| 3 | \n", 107 | "0.989897 | \n", 108 | "
| 4 | \n", 111 | "0.985961 | \n", 112 | "
| ... | \n", 115 | "... | \n", 116 | "
| 9995 | \n", 119 | "1.459019 | \n", 120 | "
| 9996 | \n", 123 | "1.469879 | \n", 124 | "
| 9997 | \n", 127 | "1.476179 | \n", 128 | "
| 9998 | \n", 131 | "1.484555 | \n", 132 | "
| 9999 | \n", 135 | "1.489615 | \n", 136 | "
10000 rows × 1 columns
\n", 140 | "