├── 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 | ![](notebook/multi_example.png) 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", 72 | "\n", 85 | "\n", 86 | " \n", 87 | " \n", 88 | " \n", 89 | " \n", 90 | " \n", 91 | " \n", 92 | " \n", 93 | " \n", 94 | " \n", 95 | " \n", 96 | " \n", 97 | " \n", 98 | " \n", 99 | " \n", 100 | " \n", 101 | " \n", 102 | " \n", 103 | " \n", 104 | " \n", 105 | " \n", 106 | " \n", 107 | " \n", 108 | " \n", 109 | " \n", 110 | " \n", 111 | " \n", 112 | " \n", 113 | " \n", 114 | " \n", 115 | " \n", 116 | " \n", 117 | " \n", 118 | " \n", 119 | " \n", 120 | " \n", 121 | " \n", 122 | " \n", 123 | " \n", 124 | " \n", 125 | " \n", 126 | " \n", 127 | " \n", 128 | " \n", 129 | " \n", 130 | " \n", 131 | " \n", 132 | " \n", 133 | " \n", 134 | " \n", 135 | " \n", 136 | " \n", 137 | " \n", 138 | "
price
00.993940
10.993231
20.989686
30.989897
40.985961
......
99951.459019
99961.469879
99971.476179
99981.484555
99991.489615
\n", 139 | "

10000 rows × 1 columns

\n", 140 | "
" 141 | ], 142 | "text/plain": [ 143 | " price\n", 144 | "0 0.993940\n", 145 | "1 0.993231\n", 146 | "2 0.989686\n", 147 | "3 0.989897\n", 148 | "4 0.985961\n", 149 | "... ...\n", 150 | "9995 1.459019\n", 151 | "9996 1.469879\n", 152 | "9997 1.476179\n", 153 | "9998 1.484555\n", 154 | "9999 1.489615\n", 155 | "\n", 156 | "[10000 rows x 1 columns]" 157 | ] 158 | }, 159 | "execution_count": 5, 160 | "metadata": {}, 161 | "output_type": "execute_result" 162 | } 163 | ], 164 | "source": [ 165 | "df" 166 | ] 167 | }, 168 | { 169 | "cell_type": "code", 170 | "execution_count": 6, 171 | "id": "d8fc8bfb", 172 | "metadata": {}, 173 | "outputs": [ 174 | { 175 | "data": { 176 | "text/plain": [ 177 | "" 178 | ] 179 | }, 180 | "execution_count": 6, 181 | "metadata": {}, 182 | "output_type": "execute_result" 183 | }, 184 | { 185 | "data": { 186 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAiMAAAGdCAYAAADAAnMpAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjYuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8o6BhiAAAACXBIWXMAAA9hAAAPYQGoP6dpAABxHElEQVR4nO3dd3wUdfoH8M+WVEghQBICCb3XYACRIiiKgOjpeTY8sYvKCWL5iQW7ePZTUe9sWFBETtETBJEighRBQu8toYSeTurO749kd2dmZ2Zn62x2P+/XKy92d2Z3v5mwO898y/OYBEEQQERERGQQs9ENICIiosjGYISIiIgMxWCEiIiIDMVghIiIiAzFYISIiIgMxWCEiIiIDMVghIiIiAzFYISIiIgMZTW6AXrYbDYcPXoUCQkJMJlMRjeHiIiIdBAEASUlJcjIyIDZrN7/0SCCkaNHjyIzM9PoZhAREZEX8vPz0apVK9XtDSIYSUhIAFD3yyQmJhrcGiIiItKjuLgYmZmZjvO4mgYRjNiHZhITExmMEBERNTDuplhwAisREREZisEIERERGYrBCBERERmqQcwZISIi8idBEFBTU4Pa2lqjm9KgWSwWWK1Wn9NuMBghIqKIUlVVhWPHjqG8vNzopoSF+Ph4tGjRAtHR0V6/BoMRIiKKGDabDQcOHIDFYkFGRgaio6OZTNNLgiCgqqoKJ0+exIEDB9CxY0fNxGZaGIwQEVHEqKqqgs1mQ2ZmJuLj441uToMXFxeHqKgoHDp0CFVVVYiNjfXqdTiBlYiIIo63V/Dkyh/Hkn8NIiIiMhSDESIiojB18OBBmEwm5ObmGt0UTZwzQkREFKYyMzNx7NgxNGvWzOimaGIwQkREFIaqqqoQHR2N9PR0o5viFodpiIgC7OCpMvz7130oq6wxuinUgA0bNgwTJ07ExIkTkZSUhGbNmuHJJ5+EIAgAgDZt2uC5557DzTffjMTERNx1112KwzTbtm3D5ZdfjsTERCQkJGDIkCHYt2+fY/uHH36Irl27IjY2Fl26dMG7774b8N+NPSNERAF2yRu/orpWQEFxBZ4a293o5pCMIAg4V21MJta4KItHeU4+/fRT3H777Vi3bh3Wr1+Pu+66C1lZWbjzzjsBAK+++iqmTZuGp556SvH5R44cwdChQzFs2DAsXboUiYmJWLVqFWpq6gLlWbNmYdq0aXjnnXeQnZ2NjRs34s4770SjRo0wfvx4339hFQxGiCisvLt8L9buP4MPbs5BtDU0On+ra+uuXNcdOGNwS0jJuepadJu2yJD33v7sSMRH6z8VZ2Zm4o033oDJZELnzp2xZcsWvPHGG45g5KKLLsKDDz7o2P/gwYOS58+YMQNJSUmYPXs2oqKiAACdOnVybH/qqafw2muv4eqrrwYAtG3bFtu3b8e///3vgAYjofFJJSLyk5cX7sKvu0/if5uOGt0UF0z0Sb46//zzJT0pAwcOxJ49exw1dnJycjSfn5ubiyFDhjgCEbGysjLs27cPt99+Oxo3buz4ef755yXDOIHAnhEiChsfrzzguF1RE3oF0ExgNBKK4qIs2P7sSMPe258aNWqk/X5xcarbSktLAQAffPABBgwYINlmsfi3nXIMRogobDz743ajm+DidGml4zZ7RkKTyWTyaKjESGvXrpXcX7NmDTp27Kg7WOjVqxc+/fRTVFdXu/SOpKWlISMjA/v378e4ceP81mY9OExDRBRAH69y9tYwFiFf5eXlYcqUKdi1axe++uorvP3225g0aZLu50+cOBHFxcW4/vrrsX79euzZsweff/45du3aBQB45plnMH36dLz11lvYvXs3tmzZgk8++QSvv/56oH4lAOwZIaIwZROMbkGdzYeLHLftE1mJvHXzzTfj3Llz6N+/PywWCyZNmoS77rpL9/ObNm2KpUuX4uGHH8aFF14Ii8WCPn36YNCgQQCAO+64A/Hx8XjllVfw8MMPo1GjRujZsycmT54coN+oDoMRIgpL9twLRvttzynH7e3Hig1sCYWDqKgovPnmm3jvvfdctslXzgB1uUfkn4VevXph0SL11UM33ngjbrzxRp/b6gkO0xBRWLKFSteISJ/MZKObQBSSGIwQEQVJQiw7o4mU8JNBRGGpJgR6RkoqqiX3WzWJN6glFA6WL19udBMChj0jRBSWakMgGDleXCm5X1NrM6glRKGNwQgRhaVQ6BkplvWMhEKbiEIRgxEiCktN4qONbgLmbjgsuV/NnpGQESqrrcKBP44lgxEiCgvy1TMxIVAkz2qWpjkLhaGjSGfPOlpeXm5wS8KH/Vgq1bvRixNYiSgsyIdEjpdUwGYTYDYbl/fULMv/zqRnxrNYLEhOTsaJEycAAPHx8ZLCc6SfIAgoLy/HiRMnkJyc7FP9GgYjRBQW5PMxXl64CxvzCvHBzdpVTAPpgvZNMfP3g477NTYO04SC9PR0AHAEJOSb5ORkxzH1FoMRIgoLp0urXB5bvP24AS1xssnG0mvYMxISTCYTWrRogdTUVFRXV7t/AqmKioryS0VfBiNERAFSJQs+Vu49hUOny9C6qXaZdwoOi8XilxMp+c74GV5ERH6w7sBpo5vgoqrGdVjmhv+sMaAlRKGNwQgRhYUnv99mdBNcPPTNJpfHjhZVGNASotDGYISIKAC4jJdIPwYjREQBwARnRPoxGCEi8gObTcD2o8WObJTsGSHSj6tpiIj8YMzbK7HjWDEAYO1jFyMumqs0iPRizwgRNXiF5a45RoLNHogAwGPfbkEtc4oQ6cZghIgavH98tdHoJkjsPVnKCr1EHmAwQkQN3m97ThndBIlDp8tdsq8SkToGI0REAcCeESL9GIwQUYNVWF6FT0WF6OzG9GwR1HYICr0gNlEw8tLVPR23mzaKDkqbiBoSBiNE1GDdPzsXT/0gzbx677D2eObK7o77czccDng7lu8+6fKYvWckIcaKK/pkOB7v0iIh4O0hamgYjBBRg7VCIQhIS4yF1Wxy3FdKye5vt37yh8tjtba6pGcWiwnx0VbcckGbuvtmfu0SyfFTQURhxWw2wSIKRoyQnhgLewJWe2CUnZUMwBmkEJETgxEiCitWswlWg3sfamw21NQHHWZTXTBiD5BqmH+EyAWDESIKK7sKSmD0SMip0ipc+c4qAM6eEUt9UMIlv0SuGIwQUVg5eLrM8J4RwDmB1Wqpa4ujZ4RLfolcGP+JJSLyoyiLGQZPGZGwWkySf1lAj8iVx8HIihUrMHbsWGRkZMBkMmHevHlunzNr1iz07t0b8fHxaNGiBW677TacPn3am/YSEWmKsphgMoVONLL/ZBkA5yoaBiNErjwORsrKytC7d2/MmDFD1/6rVq3CzTffjNtvvx3btm3DN998g3Xr1uHOO+/0uLFERO5EWUKzw9c+Z4TBCJErq6dPGDVqFEaNGqV7/9WrV6NNmza4//77AQBt27bF3XffjX/+85+evjURkVuhMF9ECeeMEKkL+Kd24MCByM/Px4IFCyAIAo4fP465c+di9OjRqs+prKxEcXGx5IeISI9oa2gGI5wzQqQu4J/aQYMGYdasWbjuuusQHR2N9PR0JCUlaQ7zTJ8+HUlJSY6fzMzMQDeTiBqg2CjXr7DmCTEAgGaN6/7tmNo44O3Qk2TNvg+DESJXAQ9Gtm/fjkmTJmHatGnYsGEDFi5ciIMHD2LChAmqz5k6dSqKioocP/n5+YFuJhE1QEoJxO4Y0hYAMG1sNwDOoCSQOjSvC3hSNIrg2eeM5J0pD3h7iBoaj+eMeGr69OkYNGgQHn74YQBAr1690KhRIwwZMgTPP/88WrRwra4ZExODmJjAf4EQUcMmD0UmDu+AxNgoAKIJowFOMlZrE7DreEnde2r0kFRU1zpuV9bUIsZqCWi7iBqSgPeMlJeXwyybUGax1H0IlcpuExHpJR7y6JTWGA+N7Oy4b19UYwvwsMgPm444bkdrrOQpqahx3GZKeCIpj3tGSktLsXfvXsf9AwcOIDc3FykpKcjKysLUqVNx5MgRfPbZZwCAsWPH4s4778R7772HkSNH4tixY5g8eTL69++PjIwMtbchIvLIFb2l3yfmAPeMnCmrwhPztuBkSaXjsSiLes+IoHKbiLwIRtavX4/hw4c77k+ZMgUAMH78eMycORPHjh1DXl6eY/stt9yCkpISvPPOO3jwwQeRnJyMiy66iEt7icivKqql1XDtwUhVTWCq5L4wfwcWbCmQPKaU4+Tuoe0AAPHRzmEZ1qchkvI4GBk2bJjm8MrMmTNdHvvHP/6Bf/zjH56+FRGRboKsv6HwXDUAYNtR19QANbU2R80Yb+WfdZ2IOrJ7Ovac2Ct57Ib+WQCA/m1TnG0NTHxE1GCF5oJ8IiId+mYlO27Lr5F+33tK8TkPztmEDo//hO9zjyhu10tpQOYv2S1dHouqz3tiEaWoD/SkWqKGhsEIETVYWnNTc9qkKD7+3z8PAwAmzc71e3uU5oxE1a+wMYtW2nCYhkiKwQgRNVjiIWP56X1g+6YBfW+zQjE+paEfpccCvcKHqKFhMEJEDcrTP2xDm0fnY1N+oeZwh7W+JyIuyn/5PPYcL8G1/16N3/edglJh4CizCaN7pqNxjHM6nlWht2ThtgKXx4giGYMRImpQZv5+EABw5YxVsIkmgsrjEqX062fLqnx677s/34B1B87gxg/WKm63mE14d9x5WDxlqOMxpR6Uad9v86kdROEm4BlYiYgCRWvuhSMYEe2z4dBZn95PnFNEKciwT1ZNT4zFVdktEWM1S3pJ7C7v5Zp5miiSMRghogbLJpkzIg1MxD0jgiDAZDJhwdZjPr1fSaUzi6rSME1CfeBhMpnwxnV9XLZHW82oqrFhdE8GI0RiHKYhogZDnuNo74lS1X3FS2l3H6/b77Lu6YFpWD2TUoQiElM/mfV0aaXmfkSRhsEIETUYtbJVKDaNHOsW0cTRGz5YAwCI9eNkVm/Ye1ae/H4bV9QQiTAYIaIGQzNZmKxTQtwzcqasChe/thzHiyv81hZ3vSDufLH2kJ9aQtTwMRghopCgp4q3diwiDQ4sZun9fSfL8PDczV61TcmK3Sd9ev4bi3f7qSVEDR+DESIy3H2z/sSof/2G6lrtoi3yYRoxeUeFPBiRi7b69+uvY2pjt/uIm1ReVevX9ydqyBiMEJHh5m85hp0FJfjj4BnN/bSW8spjD4ubYRR/z9mYe88FbvdpnhDjuM2M8ERODEaIqMGwaXScyIdpzG56RvxdrC4pLsrtPk+M6ea47eOUE6KwwmCEiAy153iJ7n3VAojYKDNuHJDl0fsKQvBrxLRIinXcrqzRHpIiiiRMekZEhpoyZ5PjtrvOCrU5I5ufGunVHJBaQYBZvgwngNzNYyGKVOwZISJDbTlS5Lh99+cbNFfVKE1wnXZ5N9VAZEjHZprvrTUhNhB6tUqW3K+o5iRWIoDBCBEZ6FjROcn90soa5J85p7K3cjDSpUWC6v5Pje2u+f4lFTWa2/Vq0zRe137ynpH7Zv3pl/cnaugYjBCRYR4UDdHYaa2YqVHoydDKqmp1MyzibimxXlMu7ezV85bsPOGX9ydq6BiMEIWgeRuPYMEW34q6NQQHTpW5PKZUDddOaVilrFK9d8PdHA1Pg5FmjaNdHruyTwbGsgovkU8YjBCFmDNlVZj8dS7unfWn367cQ5VSqKC15FUpGNE6Ru6Ckfd/3a+5Xc/7d2uR6HNqeKJIx2CEKMSIr/TDPRjxdM6GUjBgNat/jbkbpvlqXZ7P71+q0TNDRPowGCEKMeL8E9U14Z2ms0ThRO7pnJEoi/rXmL+X0iotvpn9R75vr8nqvUQMRohCzTtL9zhuC4i8E5XWclulbR3T1GvC+DsYUXr/fm2aePQar1zTS/qazAtPxGCEKNTMyz3quB3sPBihQKtnRH48urVIRLPGMSp7ByAYUWjbyO7pHr3G33Iypa8ZgX9jIjkGI0QhLBLPU1rTZOQn7iv7ZGi+ltZ8Ek/V1NpQJUvh/vI1vTC2l3Yb3HGX6I0oEjAYIQph3pykKqprsfeE/novADBnfT7+MmMVTpRUePx+/ubJMI27ng93291NcBVbte+05P7cCQNxbU6m24J87vy6+yTOlFX59BpEDR2DEaIQpqdnpLiiGltFKdVv+GANRry+Aou3H9f9Po/M3Yzc/EK8umiXN830yNmyKs0ga9ku9URg+06WSu67W1KrFox0a5EIAEjUUWnXrlZUMnjtYxcjp02K7ucSkTYGI0Qhpn9b50lOa/6E3YjXfsXlb6/E73tPAQA25hUCAL72YpVHoJepLthyDNnPLUa/F35RTer2ikZA9NQP2yT33U0eVeu02H6sGAA86pEQBz4xXhTlIyJ1/EQRhZhzVc7iaXqCkRMllQCARdsKJI+HYh6ue+trsZwqrXLc9tbXd53vUnhOTq3nxN4z4gmL6LW8qRCshStqKNIxGCEKIeeqaiVVbG0e5DyTn3i9iUVMXj1LH3/n0xjQrqnXz42N8vyrL+9MueN2fLTV6/dW4snfmSgcMRgh8tDWI0XIF52Y/Ol0WaXkvp6eEbuZvx+U3Neq8WKE2z79Q/e+//hqY0BXmHjzyu8u2+v3dtjVMBqhCMdghMgDRwvP4fK3V2LIy8uC8n6/7NA/CRWAZDVMiMUiWL7rpO59/7fpqGP4KRD+0qel4/bYt1di+9Fit88J5FDKnhOl7nciCmMMRog8sP+ka5VZb50orsCgl5bi9Z/VJ2w+P3+HR685+l8rfWtUiAUwcvbJva2axPn0Ohd1SXXc3nKkCHd+tt5ln/wz5bjwlWWYueoAAOB4sf+Co98fvUhy/9ZP/kBlTa3K3kThj8EIkQesFufZeuWeU6iutWHO+nyvhm2+XJeHI4Xn8NZSZ/e/r9VfT5U6T5g/bS3Q2DP06JlUuu7AGQDAdbIspp5IjLW6LPlVyq/yzP+249Dpcjz9v+1ev5eajGTXYMr+uxFFIv/OwiIKc+J5GDd9tBaxUWZUVNtgMgEHpo/x6LVKFSrWejpPQrzyxlvFFdWO24u3eTYs5E8xskml8gRn4mXHSgX29Hj5ml4Y3KGZSzBSXet63D0dIvNVJGbbJbJjzwiRB44WnpPcr6ium3jozXQC8cnnbH2+C0/nMZZUVrvfyY1v1h923K7SysUeYJfL0qrLg5EFm515Sbyd3HptTiYykuMUJ/de+c5KlAU4z4oWP5fRIWpQGIwQeWDy17l+ey3xSpns5xZjZ0Gxx6sq3vxlj/ud3Fi4VTn5mL+UVdbgpJvJqD9NGoIL2kuX6lbLAqNH/rvZcduTFOyz7hiAAW1TsOmpSzX323S4CF+ty9P9uv4WyGXVRKGOwzREIeL5H3cgyYP05IB/rqYDPTxwwUtLUXROuwena4tEl1TvOwtK0K55Y+UneNDmQR2aYVCHZrKnK7+AkT1DfqzpR9Tg8L8/kUHkQw0r957CfJUU6WoGd2juczsCXcLeXSBiF22Rfh1pZWj9cbNvvTlxURbFx19euMttivghHZtpbtfr0m5pkvv+rDBM1NDwfz+RQSprfL8K10qKpndhTm5+oeP2Zd3TfWyRlCeBjif1Xo7I5u54KiFWvQfqgpeWuDwmzh7rrhKwXvIjw8RnFMkYjBAZxB9F6ZbuVK9w680p09+JvQ6e1p+XJcoSGl9HFdU2l16rGnEw4qdscvJD/fyPO/DkvK1+eW2ihiY0Pv1EESg+WnmowBNzNxxW3eZNzhJ/D9m8qJK0rZHC7+5J8Tl/9+DIyY+DuNciVmWIx3PS99h+rBifrzmEraLaRESRgsEIkUHSE2MD+vpRFs+DkRo/ByNLVHpulN7Hk2BkWGff58pokbfvun+vcdz2RxAJqAd+075n7whFHgYjRDrV+HmlhTjzqpou6Qma21sqZPK082YKwpr9pz1/kheyUuJdHrN6MBcjq6nr8/1JHoyIKymP7tXCL+9x2+C2io+zTg1FIgYjRDrdoVC/JNDcTeG4ok+G6jZvJkRW+WFSrTuz7zpfsbaMu2Glsb2dv+vAdk019tTniTFdVbdpBZ792qT4/N4AMKRjc6yeehHOa91E8niJQmZeonDHYIRIJ0+qzuqhZxjF3YRSm8awik3Q3u5OYbn2Eldvne9lIGGfVPr02G4+1/ABgPapKjlMoD1c5UkPjjstkuJcljQTRSJ+Coj8wJtz47DOqW73cRdMuJvj4e0ckDd/2Y0+zy7WnCDri2WiwG7uhIGO22NkQyDbjxY7btvnWPhraa3Wqpgth9UnkfozGAFcf5+URtF+fX2ihoDBCJEfCILnQxx6Vq7sP1WGkgr1pGHuXsPb1TH2NPNPzNvi1fM9kSMa9phxY1/JttFv/eZYAu0MRvzztaV1bG6d+YfqNn8FQ2qv1yTesyy8ROGAwQiRn5z1cFhDb6/Fo9+qBwTu5oW42y7Pp5GWGCO5by8EaKTN+YUor6rBz9vrquj6KxeKt5V//TFEJCbvaWH1XopEDEaIdPrHRR00t+8sKPHo9Wp1TjCdr5H6XHx1f++w9prblcgDIn/mGfG2su62Z0ZK7ptMJnSbtshx/9s//TN0FCpl6eRF/7pnJBrUEiLjMBgh0ik5Xnss/+7PPVtt48mJf8Ohs4qP19TWvcYjl3XGI5d1cd3uLhiplW4/Veq/SataschN52epbmsUI63fKR8VKdZZ68YdrVT6wSTvGcnQWK5NFK4YjBDp5O5K39MhDU+CkVlrDmm+htqkSvc9I65tPllSqbtdWrRO9iM9yKAq7znwV+9NoAsE6iWfMyIPEIkigdX9LkQE+P/kZe+1eO4vPXBZ93TYBAEDXnQt0gao17FZd/AMAPVJndVuErUpnfiOFflWhM7x2hrHa0jH5vjg5hx01Fheq8aTTK1aQiUYkQeStTYbDp4qQ7OEGDSO4Vc0RQb2jBDp9Nri3S6P9WqV5PXr2U+GLZNj0TwhBmka6eGVKvyeKK7A4bN1gcOK3co5UDydMwL4bwKlu2GQS7qloU2zRm5fRx4wPTzSdTjKG6lepON/YEQnv7y3mLzn59PVhzDs1eUYqBKYEoUjBiNEOikt3e2b1URhT33sJ1k9S1Wv6O2aaXX7MWcOjhV7lIMR93lIXH+ndQf8kxLelzo3j492ZkeVt3Fge9+zrwLA0I7NMOnijh49Z9IIz/bXIze/UPFxb1f7EDVEDEaIfOBLV7+7+R5iSsXZzKIlps0bx7hsF7+HGqVhmhcX7HTbHj1qfZj7IM7SKh9q8tfQhclkwgOXdFJMTR9M+0+WGfr+RKGAwQiRDyqqa71+rv2KX08SrWpZUFFda8NeUUG1v+W0AuCaMMvdZEg9vRfeTmgVv7anqTl6tkpyPKc6wBM6L+qingl3/4ujA/reRFSHwQiRhy7tlua4fc6HYMRdz0hb0XyKatkQ0YTPN+DZH7c77l/dty4Y+f6+wXjo0k6O3gP3GVrrXjdZI+vnkULvJrTa54xYzCavcmf0r8/MKg66Pr4lx6u2aHl0VBfcPbSd4jb5fA4iCgyPg5EVK1Zg7NixyMjIgMlkwrx589w+p7KyEo8//jhat26NmJgYtGnTBh9//LE37SUyzIC2dSdHcf2UHzUSkrlj7zlQ6xmZd+8gNKofnhHPm6i1CViy84Rk35j6FSZZTeMx8aKOaNKoLriodpNYzd7rYDWb0L658mTSp3/Y5u5XUST+/bq38Hyib1R9AblXFu1yPNa+ueerb9yJj7Zi6uiu2CpLtkZEweNxMFJWVobevXtjxowZup9z7bXXYsmSJfjoo4+wa9cufPXVV+jcubOnb01kmL0nSrH2QN0y2ihRlVVfsmUqFX67vD7QSYqLQlJ8FIZ0bA4AqBINVcxZn+/yWvKFK9b6SbF6a9dYzWbHc+Ry8wtRUFSh+TqKry0KdB4b0xV3DG6LH/8xWPfzlaoam/2cil2skcK8nGCIi1J/3415ysnuiMKNxzPBRo0ahVGjRunef+HChfj111+xf/9+pKTUXVm2adPG07clMtT0BTsct61mEz6+JQfvLd+HGeP64pLXV6DoXDUSYj37OCkFIy9f0wtDOzbHRV3r5jFE1fd4iIdp/qjPLSImX0Zrf029c0YsZhN2Hfcsnb07FTV1Q1jlVbVIiovCE5d38+j54qDPzt9F6sT8XXNGr/f/fh7Gf7xOcdvELzdi1aMXBblFRMEX8DkjP/zwA3JycvDyyy+jZcuW6NSpEx566CGcO6c+Dl1ZWYni4mLJD5GRxBNIrRYTLuqShm8mXIDUhFgU1acnL6nwbCmmuFfCLj7aimv7ZaJZ/eoYe+/Asz9ud+z/255TLq8l7wCxz0Nxv5rGJnkfNWv2e77c96PfDnj8HLFgByNGubBTc2x5+lLF2kLe1vchamgCHozs378fK1euxNatW/Hdd9/hzTffxNy5c3HvvfeqPmf69OlISkpy/GRmZga6mUSaxEnF5HlBrspu6dVr2pesap1go0Tv9fu+uiBEaXVL65R4yX1rfXDhvqqv9rwVux3HPL8g2Jjv2xBDsIdpjJQQG6U4XMNQhCJFwIMRm80Gk8mEWbNmoX///hg9ejRef/11fPrpp6q9I1OnTkVRUZHjJz/fdYycyCjy5byX9airs9I3K1nzeftOluK/Gw47rnbtgYBWr0SU1bntg/qeBvlS1J4tk1xWfVh0zhmxD+Mo9UKIffL7Qc3tSnxNt55/1vX7wV16+4ZMaeUOO0YoUgQ8GGnRogVatmyJpCTnbPquXbtCEAQcPqxcCjwmJgaJiYmSH6JQYZOdZKMcvRDaZ46LX/sVD36zCTnP/yLZ36oRCIiHcA6fKQfgOmnWqhDM2IdpqmsFLN91Ah/+tl+xy19vrhOl7LPuFPlYXXePwhwWf9WlCUVKS7wLij2fOEzhoefTizDopaVGNyNoAv7JHjRoEI4ePYrSUmeugN27d8NsNqNVq1aBfnsiv6sV5MFI3cdI64QtDmBOl1UBcM7X0MrAKh5mianvxpfvrfR8e3Dxw6YjuOWTP/D8/B1Ys9914qu9Z0QrIPLWqdIqn55frDAHp5lKptlgCHSmVk/nwyzcWoAH52zyKfEehaadBcUoqajBkcJzyK+/CAl3Hn8DlZaWIjc3F7m5uQCAAwcOIDc3F3l5eQDqhlhuvvlmx/433ngjmjZtiltvvRXbt2/HihUr8PDDD+O2225DXJyxaZiJvCEffrAHIzU2AR/+th9tHp2Px77bIumJWLitQPKcZTtPOCadagUjpaITsn3ehvxqWekkZn9owRbn+yolL6vxICV9pPnk1n6S+wmx6onh/MHTv8GELzbgv38exserfJsoTKHneLFzXlhlTWQEmx4HI+vXr0d2djays7MBAFOmTEF2djamTZsGADh27JgjMAGAxo0bY/HixSgsLEROTg7GjRuHsWPH4q233vLTr0AUXM0TpFfn9mGa6lobnp9ftwT4y7V5WCFa9TLte2nisFtn/uG4rdUrobQyd8566fCmUn4QpV4QrWEaq9mELNkkWD3PDzdf33U+Lu6Sit8eGY7hnevm5rSrz4R7uSjZXSD0zkz26nnepuun0CUfCo4EHucZGTZsmOaX0syZM10e69KlCxYvXuzpWxGFpIHtpFVj7T0j8pTtj327xZEj4lSp+gnD3bJad3xZ7upYXqyjDSdKKpGWGKvrdStratE3Kxl/5hV6fZKNtpolQ18ZSfre2xcD2jXFANnfd+49F+CPg2c0a9j4Q7ZCBWhxSQA1JpeBO2ro8kRDM/tPlqFDaoKBrQmO8J0NRhQAn9/e3yU5lj0YOSrLUmofFnE3pq+W+VQvvcGM0iWEMx28GbFR0nbI69XoKapnN+Sfy/BnXiEA4IL2TbV3ViGvRPzuTed59Tq+SmkUjZHd092uOAqEvgoBihxH2MJLrU3AU6ISDHd9vsHnyeANAYMRIjfEOUY6p7teoWgFAzabgOv+s0bz9bXmCug7z+g8GynEErWiYZqc+sJ0dvKTryfDNCdEQwez1hzS/TwxeXAUifNa9PzKYZp6JWIpTYQ/HgGrqhiMELnx2WrnyVSpF0MrEdeaA6exKb9Q8/W1KsPqOdH8suO4+50ACArRiKNnxGLC7YPbSrZ1kQVe2456lwlZaVWMHk+N7S65b0TPRLA9Prorumck4r7hddlY9eRqCddEcJFKqbglgxEigrhLQWluhdZJMhCTC89vl+J+JwVK7RSnpJdXxH31b70l9+/+fINX7+utoZ2aS+6HYyp4uTuHtsP8+4egaaO6ISpdQ2Phf1giSlG565DMF172LjYkDEaI3BCPTigNFSTFqy/5nDQ716f3VjrPKK2U0UPpxCZPSd9ONGEyLTEW91/c0eP38TXzqppIGqaxB73sGYk8Ly/aZXQTDMFghMgN8elA6eo8McD5JyRt8WF5rdJYtHw1zSv1vSH23pd0jdUzlTW1WLbzBMoqpcMwH/y23+s2aomEnhE7+99lnUKFZkD7/8HsdXlYtbduWfmqvafwyNxN2HuiVHV/Ci0r95x0eSwCVtV7vrSXKNLYM6YC6itfmjaKluznL8nx0ZL7vvQ6KNV1kSc9O691E/z2yHDHEl55tlmxzk8sBAAM79wcn9za3/H4Sz/tlOzXOMY/XzORMGfEbmZ9LaCTJZUQBMFlBVdFtfNvWSP6u245XIRHv90CANj+7EiM+3AtgLrcNAdfGhPgVpM/NGkUjbOyoZpISDsSOZ9uIi+JJ6CqXZ1rnbR9MXlER0mejhqb4PVwhWIwopAOPjMl3lEDpkeG+7pQy3a5XsmJPf+XHp40U1Uk9YyIe7HGf/KHy/blu044brdIcmayPnzWmZ+itNK7icNkrKsVqoDrnaTekDEYIfKDQoVJZ/6QHB+Nr+8633G/xia4zZQqd11OJgC1YRrt+jhKibgAz4aLWvpQ0+W/91zguB1Jc0YeH9PVcVu8tByoO/b3zPrTeV+0Lba+fhEgLSVADUckDMkoYTBChmoIJeHbNa+b1Dl+YGtD3l98Ei4sr0KVh8csylr3/CqF3PLVotU07jQRTdT1ZLjIlxgiXZR1VWsJdLiRJ3wTpwf/+o98ybZa0VJQcSHBaqVaAhTyqiNhTEYBgxEKmgOnyvDEvC2OruSftxWg0xM/Ye6Gw26eaSz7ktfO6e6HLLQ8dGknr54nHp4Y/M9lugK4pDhn4OBIV6/wPD3p4G+5oA0AYHTPutosryzaiTd+2e2+4fVK/HSFHkGxiEu9ouIKZ8+bfU6I3YsLdmLh1mMApLlk/rVE/9/Indz8Qgz+51L8tOWY316TlNWofL7DvTYUgxEKmhv+swZfrMnDHZ+uB1CX5lgQgIe+2WRwy7TZT+K+1pC5ZVBbl8e0VqvYyScvyq94X79Wmg8EqEtbn52VjG8mDHTM/5DXzql7Le1hGqAuHTpQN4nuZEklZizbhxnL9kn20fqirFR4X70SY52TX+2/RySQr9Z9Yt5WbMovlMwJEZvwRd2wjfiiWlyx2Vd3fLoeh8+ekwwPkX/ZbALyTpc7JpW3k9Ulajt1AX7YdNSIpgVF5Hy6yXAF9VkEdxaUGNwSzxwrrGu3rydDi8mECRe2lzzmTYoIeVBxcdc0l316tUrGd/cOQr82KYiuv8pWGt6pla2mUZJfX7Trq3V5jiq/cj9uVr9i9mU1TUJsFGbfdT6+mTAQMVaL+yeEicpq6XH+cfMxXDljFQb/c5nm8wKV40VcX+lESfhnAzXC5K9zMfSVZfjPirql8Zf1SHfZ5/6vNup6rXNVtbjszRV4+odt7ncOEQxGiDSsP3gGu47XBU/RPi4ttZhNLid9bzK0lshWScRFaZ+k7cM08gmsgiA4Ut3LhwXEvhENo6kl2PpclDJfrpUPE1gB4Px2TdGvjXdZZxsqb+dSafVQlVR4P8la/LqXvL7C69chdfJeD63PpDv/23QUOwtKHEvEGwIGI0QaPl51wHHb1zwXFrPJZRKmJ5Vw1bgbPrIv8Zwtm/hoD7LsbdPDpnKyq6xRr0zcumkj1W2kzJuVQ3tPlGjmo/jfJu/ne5RVOf++kVBBNhTIV1F5wh/fK8HGYIQCrqyyBt/+GdqTVNWI52vERfs2TGA2KWdX9JV8TomcvdtXrrrG+YWld7ioRmWFhtpX35d3DtD3wiTRv63nPUHX/2eN5jCNUqFEPZSWhFPg5bopsKnldKn/a2IFGoMRCrjHvtuCKXNCe5KqmgrRFWF5lfrV/wMjpCtlPrutv8s+JpMJ2495V/k2ENR6ObSIk22JKb1UtMWMC9o38/g9yLsu+lOlVVi0TX3SartmjVW3KamptWFXQQle+9m1Vop4Dgn5Tul4ZqbE4f2b+nr8Wt/nHsFri/23kipYGIxQwH2f23BngFeIhh/EKzvkhnWWVphVm1sRH+2/CgzdMxJx5xDXFTpy/71noOP2/pPOGiXimjImjdKvD4/s7Lh9pFB58qI3gQ1pa54Q436nALpn1p8Y+eYK/FuhZ225m6y75BmlFWf/uj4bcQrfF20ena96UQAAz/5vu1/bFiwMRog0iFc1aF2tyueTyIf8O6bWXZXKT9rTLu/mddt+/MdgPD7G/fPFV8TvLNvruP32UuftPcfVVzj1bpXsuG0vwCa37ahrj89TV3j/uxEw5+6B7neS6aaRvt/TgHHxdvUU5OVVzO7qT5UKPSPtmjVSza1zi0KJALuGmhyQwQiRhp6tkhy3tVatyFc/yOdx2CeIytPG3zbYfc+GGndzRexiopwfc3FwtflwoeP2H4eUq8MCQEayMxfKliNFbt+vbX1+hM5pCbraR8raNvN84q/anB7Av8t+/9tA54CFquv+s8blMW8nzHuzQi8UMBghQ2xXuJIORY1E3aRdW6ifXFuITthv35DtckWjNmwTDOIlyafLnF9U4mDGotG+ds09m2tgvwLXGyyRuonDO3i0f27+WdVtZZU1OOWniY2r9p72y+tQnQOnylwecxeMlFbWYMxbvynO6WmIGIyQIUa/9Zvk/tmyKoNaos2eKOzOIW01T66Jsc7064lxUTh0Rpops0yhW/vXh4fpboc4vbunxMNLa/afQUFR3bwPcTIyf1bEtQcjDbS3OKQ86GEJgTnrpT0WcVEW9K7v3btn1p/Ief4XxROfp7JS4sM+PbnRoiwmzblcX/+Rj21HiyXDrUpsDWSZL4MRCglrD6gPExhpy+G6YQl32VfFJ3MTgGJZLgb7ZNF3xzlnx6fpSAVvt2DSEMn9xQ8M1f1cudl/5AFwZsQF6qoD++ruz9fjz7yzsCdpNbI3KFyYTCY8Prqr+x1VRFvNLnMI/FF+Ie9MOfq98AuOFp7z+bUI6NkyyeUxk8mEfm2Vq2YD+pdcN5QU8gxGKOCaNXa/KsDXui/+tPdECa58ZyXeXb4Xq/fXdUdHW7RzjIiHOcwmE8b2zpBsP1Va1/PTXjTk4cmYcMtkaRbTjj7Mx7D3jIi9eV0fr1/PbtG247j63d8dV8wMRvxDvlLLbsMTI9w+12I2uQzB+Wvl06nSKnz42wH3O5JbfTKTFR/XKoGw90Spy2NvLdnj8tjkr3MbRC8WgxEKqLzT5brGqX1Jfexv983aiE2Hi/DyQudY7Nly7WEk8dWn2QwkqCwDbt00HkBdhk1/Do14wv69ZK/Ge1V2S2SmxPvt9e29woxF/EPt/0nTxjE4MH205nPNJtesv3qCYL1d+/wb+0d6kv5eUjulScSvq+QX8cfQXKCFzhmAwtLQV7QLe9lFhdAEA6Xg6fBZ/d3RJphUewVioyzY/PSl2PL0SK/b543BHZzJxzJT6npZ7KsrsvwYiADiCax+fdmIZTWrf02bTCZ8cms/1e0Ws+vkZHmvnZJpP2x1eWxwh2YuK6SMCqjDhSAImLFsL97/dZ/qPhd2Uu4Z80RDSA/PYIRCQwh9p9UqdGlq1V6Ra9Y4WrO2SGJslM+p5X1hXx1jX47sazViOfv3Hodp/MPiZggzRqOnw2Jy7YGLd1NYEQA25hW6PPbatb1drrD5N/ZN72d+xiuLdqGkQj1vyye39MPyh4Zpvs6RMJi7w2CEQoJWfoRgEgRBMe27nhwN747ri2eu6I6OaQkhd8Uorktiv0qyrxTSM1/n5Wt66X4ve88ST1T+odRrKD60URrBpFlhOFDPn2VwR9c0/jFWs+P/jF0Ija42SMUaQYid2WxCk0baE8zzZav3GiL+VyK/KCr3rZKnPxMy+eLvH61TnKWuZ0x3dM8WGF8/DyOUc2zc/9VGnCmrQnV9AKhnDkG6wsqfH/8xGHcNbaf6nBCLxxospflUP4lWV2n9/SwKwYiez1qUwtCQUoDNgDM43F0wFJZXYeFW9bpEDaFcA4MR8tnf3v8dvZ/9Gd9tdJ+V8aIuqYqPaxX4CqaVKunO2zb1PBummL+HQjwl/y56d9leVNfYe0bct03pRNSjZZJm8rpQDsgaEqVj3yXdmfZda0jQYnKdv/S/zcfcvqdShV+lwIN/4eDQmjcEAP9ashcTvtigut3mZhXw0p3HsVujJEQwMBghn/1xsC7r4wNfS/MXyGfkTx3VBe/cmK34GrP/yA9M4/zkTo0eAF0MvjCRXxlV1NQ654x4GYwA2lds7BnxD3EMcFGXVPzr+j6S7VqBrtlswi87pDVmVux2X+RO6UI6PtqCx0Z3cftc8j93PSM73FQD13r+hkNncdvM9bj0jRVYf9C4fE/+KyFKYU8QBMz+Ix+9WiWhe4Zrkh45+Qxuq8XsNsIPRcM7N0esjkl/DUlNrYAlO+sqf0ZZ3UcNB1WWBmr1qrBnxD8SYqwY2qk5qmts+Gh8jstx1Rym8fJvIB/Jade8EUwmE5o2kuYM8rZ+Cmm7WNaD7O6z1DI5TnMSq9bI3NwNzh7tYwo5iIKF/5NIt5+2FmDqt1sw5q2VuvaXX43bbIJml3KosngZQK38v+F+bYda8is95Fe64p4oPQHiCVnxrYz6OTRd0tWTr8UYPDQVLkwmEz67rT++vHOA4klJ6zPlbQXXClkV2f0nlYNRI1eFhZK9J0o052zoFWM145Nb+uGjW9SXaytRCkQuaN/UcXvDIfWaRV+ty3PcNvL7md8WpJu4K9D+ZaWV2U/eM9ItI7FBlrf2Njtsqyb+zd/x3JU90LtVEt6+QXmoy1vyE48S+TDNP+tX19wwIEv1Ocnx3tfTIVdqV8daK7e87bhQS9onj1ufn7/DuzcII0t3HseI11dgwhcb8LvKnDO9po7qguEq8+p+f/QivPa33rpe55HLOuMKUT6ZVTrbZWTySQYjpFueaPlYt2kLUVNrwz6VKybAdda+OFIPRWqrDHz5gPbNSgagL9GUO5kp8fh+4mCvXktrysq3fx5x+3x5QJaaUNczEquSrjorJR7x0RwFDgatHny1YZqRb6zQzMqpNiyZ0zrFo7ZFgttmrnfczj1c6NNr3XR+a9VtGclxuj/79w7rgGtzMh33L+2eput57BmhkFdVY8P3uc6CSzahrkLozN/Va1PIT+72Kzt7FVF3+wdbda3ylHNfPqAfje+HV//WG89e2d3r1/AHrV+he0ai+sZ6V/ZpKblvvxq3qvQa5bRRL/BF/qUWEALApvpCj3K7jpdg6rebVZ+nlvfHn2UDwlFltb7idUo6pDZ2e+HjSS+t2WxyXAzpnfNmZM81gxHS5fBZ16Q6j323BV+syVPYu45acKEWc6gFA8GyXWVGui/BSJNG0bjmvFZoFGNsL4FWKfIr+ri/2kpNkE5ctB8TtQmMnlQkJt/IE2KlJUr/ViO6Kl8Vn9M4cdZqrAWV/18gp/eWq6d1d0fPt4yeSeFXij7P9pVWahV+5d/RRuYjYTBCunjzn1Q9GFF+vFJnSexAWLnnFK5+93fFbUb32PjD9f0zVbfpqVsh/xK0P0MtULtnWHvdbSPfdW2RqHgbAHq0VO750rrItlerVhLPSauq5BlqtZyW1cDyVwI58cTx6PpeM7ULPZcCoAZ+1TEYIV1GvL7C4+eUVChnZVU795VWuk+NHAiF5VW46aO1qtu/3eh+TkWou0JjrLnaiyDQPnFZbfJkYiwnrwbTP//a03H77qHSQHDtfuXcEVqfN3vuIDtxMcV3buwr2dYQytMHivx3752ZrPu5B09Le5v9NUQiHmKLro841XpG1h2Q/t9gzwiFhTjZuKSeRGZz7h7ouD3opaV+b5MeD32jPnYeLkwmE764fYDitgQvAgf7V5ZSt7F8mIACr3N6ApLjo9AxtTFaNYmTbFPr5dh9vFT19eSfZXFvSI+WSXhVtKrjRx0ZXcPVK4t2Se6ne/B/X77CxV/TNVJFQ6SOYRqVnhF5r6iRncAMRshv5NVus+snT8mJryb6tzV+dv6yXSeMbkJQKE0qHdKxGbrpmMAKAJumXeq4namybNlqNmGZmwqj5H8xVgvWPTYCCycPdVmW27qp55NOR3STzjORzytKaeQMYLVyWPiiutaG1ftO61p6bpR3ZXNEFm07jorqWhw6rb5SCaj7Dnx98W7JY3qHaR4Y0Ulz+33DnT1j9jld6nNGpI+zZ4TCQlWNDX//aK2jaF5DSXDmrp1tvPgyD0XyGfVN4qPwuUpviZKk+CjseWEUdj1/mWoK8g1PXMIlvQaJtpphMZuQHC+d0Hrf8A4ev5b9JDXlkk748OYc3DVEWg5B3CNWfM63IplqXvppJ274YA0e/W/D6rn863u/48JXluMPjdTqe0649krp/bp0d/Eg7um0l3pQ6xmRxx5GDrkxGCG/+m3PKfR+9mcA6hNSQy1NuLuJs4+OCp96HH87r5XjdlKc58MzURYzYjSWkqot9SXjXJXdEpd0S8M4jQR1cvZJ200bR2NEtzSXJacdUxs7bhdX1M092XuiFJU1/uvF+GhlXdqAeblHUV5lzHwyb2yrLx45d7164dDyKtfjdOugtrpef0RX5aRoSqLqLxqqa5SDjA6iv6PZBAxs30z3a/sbgxEKiBPFFZg0O9dxX7y6oiFMeBMPH+lZbdJQ9G3tHKqRT6DzB6OXMJOrKIsZH9ycg9sG6zvZAc5gRC1pmji78C87juPrP/Iw4vVfccen6xX399U7S/d6tH91rQ01BqQKaNfcWd1ba2VNU9ly7Feu6SVZkqvFk4s5e8/I+kNn8OFv+12Kl9rvZiTFYvuzl3l1geIvDEYoIMSBCAD832XO3oXRPVsAcEblfUQz0M8pXDEYobVo9UA4FQMb1SPd6CZQEKWITnqeXATYA3CtVPNi//ffLQDqekYD4dAZ/YGzzSbgktd/xfDXlgd9Wb64ho/W8Za3q1tGYkB6jO3Dqb/tOYXn5+/AvFzpykB7O6wWs+HFQMPnW5YM8/TYbi6PaU1qu2toO7x5XR98fdf5AIDn/9LDsW3OevcrcIJB/L3QMjlOfccGJpBXPmoTlin4vrxzAHq1SsJnt/V3PNamaSONZ0jVehiMhJKic9U4eLoc+WfOueTyCBXyyf5NZPN8vDXhQumy7mjZhZR8BZXNzRL9YGKfKvksSaEgmvzDJhYbZcFfsp3pxcUF1fw55qxXtNUsmW0+rHNzScZS8bhqQxfI+TryLz4yzgXtm+GHiYMlj3lSY6khByPib54KH9Kz+0rrsybvGcnw0wXPkI7SOR/uenXtk4+16hQFC789SKKiuha9n/kZLy/cqfs5SkvSPOkeFX9gLDrK2fvbNaJJnYBrhsmG+IWshz+K94mprbCh0KX2ObVP6g6VIcr5m4/pXuIrniuydOfxQDUJgDO7rdLyaa1vDfFx11MbSu7N6/qgeUIMuomy7V7cJdWlGKn8M3m2TJpx9Z8efM8HWmj8T6OQMeGLDSg6Vy1ZPy+uSzPp4o4uz/H1ZC1+/k9bgp9ASd6JE20x42vRcJHaJL6GTj6ZzVd685VQ6NirsMQUADbXV5+V5ywRa6wyWfnD3/ZjU36hr01zKaj51pI9up4n7g1p3Uz/0JQelTW1eO3nXY5haHu200u7udb/+XbjESxQ+T7bVVDiuN2theefm79kt8S6xy7GA5c4c458dEs/l94YeTAin1Srlfgu2BiMkENFdS2W7zrp8vjxYue461XZLV22u0vWM9FNngNxno/1AUqgpEV+Um7XXDosY2Qly0Ca76fAb959g3D3he0UA1UKbUo1S95dvhfV9SdZrZ4RtSWmz8/fgStnrPK5ba1kFYL1JFd7a8keDH1lmeP+kbPnfG6H2CerDuLtpXvx1/fq6ljZj9/wzsrH4t5Zfyo+PvnrXMdtb3soTSYT0t0UpIxuQEvtGYwQgLr6LF2eXKi4TTwr3GI2SVJBA8Dgjtpr091dMYt7RgYYkJHVPomrX5smuC4nE3cNbefmGSTWJzMZU0d1ZbKzMPHyQmeKc62S9Zd0825l1s6CYtUeGTH5RU6Zjlwj8qym+QrVxn2xR9STcKq0EvvqV8/4MkQ5tFNzr5/bs1USnrmiOz4an6O4Xd6u3/dJVzv1b2N8Bmw7fnsQAGDxdvWxVXGejSiLGdec1wpX9M6A1WxCVa3N7ZIwdxMbxV86wZ4seuBUGb7ZUJecaETXNNxdPxv9/HYpWKNSYIwoXLib26XVM+LN8GxJRTUue/M3AMC+F0drvoZ809YjxR6/n79XwonblPP8L47beufW/HfDYfy2x7X32RfjL2ijuk3eLnEvNwBc1DUV6w6ewUVd9CdSCxT2jBAA5aEWe4+IeBglNaGuEFS01Qyz2aRrbbq7qwZxUa6URv5Z4qbX/811ppoWfzEObGdcJkKiYHHX22DVnFDu+ZyjkyXOk+HEL5WHMOz8MXFcXMHWH9RGpPVmHn7wm02Yl3vUjy3S5m6VjH2ybygUt2QwQgAApe+cGpuAiupaXPP+agBAemKsV/Mn3H1QzWYTxvSqS4S2IkBJk9SsE9WPEE/+mjCsHZ4e2y2si771bJnkficKKw9dKi2y9rWssvYZ2WoLLe6qPcuTfp0oqcBFr/3quP/T1gL8+9d98qeJXkB3U1T5u/DbHJUU76Gy6khurZve3apa+wWn8e03vgUUErYcdu0Cra61ScZIo6zeXano+Y8+v74MuT9m4XtLHDPFWC24ZVBbtPXzbPxQotW9S+HpnmEdMOfugY7738uu0uVDCFrDOAPbNVXdBgC/7JBWwx75xgqXfab/pL60VCtXkV7BKuWgJ8fO8eKKILREqnem9gWHfQJuKARTxreAQsLHqw64PFYt6+K0eZk/qKEUTwvXVTNyH9ycgzsGt1VcGUXhzWI2SeouAdIejBjZkGpslPopwt3nRT5J9Wy5Z9V9/RFHBCsdvJ7Mxq//vNvtPv4mH36/LidTct8+TOPthaY/MRghVfJlf0cKvVsmFxWELsCTJZUNqrKnkS7ploYnLu8WtsncyDO5ot7I93/dL9nWMS0hqG2ptQl4/sftWLj1mMuSe0+qDotfLxj0zJ372oBSF/L5evI2OJZwc5iGQtlPWwtQo7M7RCvVSGaK+xnt4iqWv+72bLb5qdJK9HvhF/R/YYlHz5PLNXCIiMgopZXOIF78GRBXoPWGN/M1Zv5+EB+uPIAJX/zpeH6zxnXfDbPW5nmUGRoIXjASqtmHmydoT0xdve80AA7TUIh7ct5WzFqbp2tftcRnNw7IQrKOIlAv/bWX4/ZT32/V18B6f9YnQxJ/qXrj8vpJtESRRC1m8LXWkDeBwHM/bnd5fozV2esgzgyth78nsKqxmE24Y3BbxW0nSyoxZU6u4rZ///28ALYKuL6fem/SpvxC7Dpelwm2QQ7TrFixAmPHjkVGRgZMJhPmzZun+7mrVq2C1WpFnz59PH1bCpBDp8vwyNxNqtvV0hnLqX3xDHWTEM1OvM794GnPEhW5ywCrl7vVAUSRZOJF2pmTAdfVOWLyuWKtmniW88P+lRIjm7eiVsZAvnoHcE5gXb7rBK54Z6UkDbu/qfVCPDFvC77984jitpHdvUscp1e01ayauO6/fzpXBllDYMjW42CkrKwMvXv3xowZMzx6XmFhIW6++WZcfPHFnr4lBdCFryxXXa4GSOeNdPZw/HhIx2YYppImWc6X+QviXAmeXo2Jh4dYdZYikfgzI87zk+Ym1TgAzV7P1ATp8z3tpPijftm9vHDl2XLl5cclCj2j9sDljcW7sflwEe6ZtUHUHv/0mlzdt24i+A0qc1oWbQtssT53bhuk3GPjr4s4f/H423fUqFF4/vnncdVVV3n0vAkTJuDGG2/EwIED3e9MhpF/8MUrarKzkj16rc9vH6BrYpevJs3Oddz2dN7HGdEXW6KOGfFE4UAchG8/Vres32YTJHlG2jT1bc6IvJCep9lQi+rL28tXqpwnynwqtkeh6Ju9Z2TT4SIAziRga/efRp9nF2PeRuUeCyXihG12WSnxeP3aPgCAxNgoHHxpDAZ10F7yHGwPjeyMR0d1cdz/Prfud+4lKkQYpKk1moJyKfjJJ59g//79eOqpp3TtX1lZieLiYskPBYdWhdqqGi/X9gbRPh01L8TEF0f+ulIiCnXfTxzkuP3Kol3YVVAiqeh6Qfumbic/uqNn8rueDlGLzpUeSqvp5D2l9o/4bTP/QNG5aknBOneU6tzcOqiNy2Or9p7W/ZrBEGUxY4houHzS7Fx8t/EwpsxxDs+HwldfwIORPXv24NFHH8UXX3wBq1VfKZzp06cjKSnJ8ZOZmen+SeQXsdHqPRnf6ryKWP7QMKx9zJjhOPn4shb52HMIfB6JgqJVE2lF3C/XHnL0RAB1Bdj0OK91E9Vt8lTslQoVgm2C+6HV1fvUszJvPlyIs/W9OUorQtRe29NkaPlnyvHZ7wddHg/VVTRy8r/FA1+rzxM0SkCPZG1tLW688UY888wz6NRJfaKT3NSpU1FUVOT4yc8P/vrsSPX+TX1VtzXSCFTE2jRrpGu8Wc4fxZpirBbFsuhK5Bke2TNCkerT1Ycw4EXn0nitHlKxri0S8d29F2D11ItctslP+NUqPavuPq+xVuXvnd/3nsIV76xC9nOLseVwEZbscJ2bUVKhnGjNk4963ulyDHl5mWJNmRiFtk25RP+5LliKVY6DnXx43ggBDUZKSkqwfv16TJw4EVarFVarFc8++yw2bdoEq9WKpUuXKj4vJiYGiYmJkh8Kjj6Z6lc6vVolB/S9tcqV6zXhiw1uC3DZya+aUhoZXyyKKFjaa+QR8aTjIDurCVokxWFwB+nKuRpZkFGlEnTYgxG1VTLTxnZTfPz/vnUWuRz7zkp88JtrFmm1onRqbVGySqNnRqlnJBRydshlynrC5JLjjZ8vp2/cxEuJiYnYsmWL5LF3330XS5cuxdy5c9G2rfIsXzKO1hiuVvRsNZt8rgPhr4ygemevi3MQfDQ+J+gVg4mM1LVFIvadVK7q6k1+DnlnikvPiEoAYB9C2K9SYTanTYri432zmiD/jGtWaLNJGkzN3aC+WtBXSr09entm5an3A6mNmxpbl/UI7BJjPTwORkpLS7F3717H/QMHDiA3NxcpKSnIysrC1KlTceTIEXz22Wcwm83o0aOH5PmpqamIjY11eZxCg0mje/bpK7qrbtvx3GX49PeDuKJPhtfvHeylZuIvy8E686EQhQutK/hYL06UT43thr++t9ox98SlZ6T+xP3yNb3w/q/7sL8+ELKfvE+Vuq5WAZTr49TaBDRRWVbcMTUBvTOTHCkLHvrGt/kRyRqr7JSK+em9qBrWubnXbfI3peGmYPP4f9z69euRnZ2N7OxsAMCUKVOQnZ2NadOmAQCOHTuGvDx9WTup4bg2pxUyU9S7+qIsZtwxpJ1LbgFPpHsxz+R3jS5Ud8TdwqFQQpsomLROmreq5KbQ0iE1ARufvARX1l+QqPWM9GqVhKUPDnP0DNiHTNQ6Y+KiLLjmvFYur6WWqMsmCMhprdyb4o34GPVrdqW5Ndf107fgQi2YilQefwMPGzYMgiC4/MycORMAMHPmTCxfvlz1+U8//TRyc3O9bC4ZJRhF1W4RLZMrVElsJLZm/2nc+MFar99P/GUZAgkIiYJKa45WEy+HLM1mkyOwlwcjlTXScvX2JINllbX4M++s6jyO2CgLXrmml+SxGpsAi0r795wo1awovLPAs1QR8h4eMaXvxWaNY/AXHT3E8jwsweRpNtxg4OUg6VJSEfiKuHGiBGkfKkxGk/tGI3OsHvaeEbNJe3iKKByVV9UG5HXtoz9r90vzbdh7RuxBSFR9z8j9X23E1e/+jreW7FF8vRir2eXzufdEKf4tqzAspnVxcdmbv2m2X666Vn3+jFrQ8+b12ejWQnvhxYUGDtOIK7AnxhoXFImFRisopFzYqblL5dwTCtkH/U08VLKhvvidFnFtBW/Yr9w4REORaGNeYUBe94dNdStYlu2SfofY54zYV6DYh1nsxdrUPvNKFwqz12lPBfBnT648eVtmSpxj4mzH1MYev95zV3ZHm2aNMKSjccGIeEjMGiKrfxiMRDC17sd/Xd8Hf33vd8lM+4wk7+eC6CWOCVbv9y2LYWVNrdtJWfalvYxFKBLltGmCvDOuWUUfHtnZp9etqHb9Xqm1CY4VLo6eEQ9Pgtf3y8TsP+pyTq13c7HiyWR4QRA0e0blCcO+vWcQTpZUoqD4HLpq9H7Y0+zL3XR+65DqiQ2FInkAh2ki2tajyh+W5PhozL9/iOSx1j7WqdDDn1czv2w/4XYf+/JF9oxQJFLLPXHfcPfVej0lLiVhH54RDxWo+fbeCxy3p1/d03F7r5uyD558lygFT2LypbrNE2LQLSMRF3VJ0/0eYqEUiAAMRigErJANxXRJd1bllV+1BOM/rD+X9v6wyX3q+oKiCgBAqUK1T6JwF8yTkHhyqp7khm2axuPru85H3yxnEka9J/FhnZu7nZDes6Uz3f07y5Tnqtj5mj8pVMy8tZ/i41qVl4OJwUgEe33xbsftD27OwcLJQx335VcWajPX/cmTYOTD39QnrwF1M/Td+efCnbrfjyjcBHOugLhnJNrN+w7r3BzLHx6OAe28q34ba7W4/S4Rf799suqg5r5K1YAbomGdUyXVe+3euiHbgNa4YjBCAOomrWoJTs+Ivv1sNgHPz9/h8ri4aNfQTu6TmP0ZoAl8RA2B0mf6hv6+FyWVL8MFnEMdURaTo4ejd2aySrt8Oy2ZTO6HaSqqnRcrWquKzpZV4eNVzpV94wZk+dQ2oyn9zTt4MQk3EBiMEAD3wUaglgGKia9mWiarr4OvqHFtyz3D2uO/91yAy3u1ACAdZiquqMYjczfh973eJ0gjCjdWhd5Of5REsK8SEX+nOFbSiD6XR84qzxnx9cKnV6tkzTwjgHMFjzs7ZJNQX7iqp8qeDUMw8kV5i8FIBGue4CwM5+7Dq5YDwJ/EPatak9vOKQRG9qyP9hU0u0Vdq//6ZQ/mrD+MGz/0PkEaUbhJUkhzHm3xPS24/YRXYxMclbAdPSM60syfKKnw+r2njuqC2we3dVt1WG/pnTU+rOpTuqDq1SpJYc/gkQd6V2e3NKglrhiMRLAb6tMWD3UzRAN4VsXTW3onqCn10ti/fOw5DL5al4cTxXVfakd1zNonijSX98pAZor0hKlUhdZT4hOePWFYpULPCKD8peLt8Gm01Yy7L2yPaKtZcc6IN70+v+xwvypPzQ8TBzlu/6VPBj64OQef3dbf69fzB4tsCMxdAb1gYjBCaNtUu7w0AOSI5mMEktbwjN25avUho/Iq58qY5fVJl0KxpDeR0aKtZnw8vp/LY74SD/98/UddcjLnnJHAfRZfFA2hyKedPDCiE9650fOJmrWiqzA9301iTRs7e55NJhMu6ZZm+MoVeV4ZT3+nQOK3dASrrv+gyaNlJY9c5joLO9AElb5Urfkrq0TzQoor6qqH2quIilUplP4mijTyDoRoP6yaE09A/XzNIQDOz1uMKNhpJjpZ6zWmfk6Yku4ZzgRk8mGahFgrTPD8dxNX1p08oqPHz7eT5yoxyirZvDmt4xlsDEYimD0Dq9JENrlGMcEpMd2vjbMHRm19v7j3wy4pvm78+1Sps8De6bK62/LU9oB03sldQ9t511iiBk/62fdHz4h4kqR97pZ9uEZPz4hWL+xTY7tJ7v/8wFDF/eQTNWOjLJLvFr06pjlzL8VHe5+wXJ7F1SjyHC8xfvh7+0votISCzlmbxX0w4i61ur/Eiorl7SpQnvH+6qJdkvvvjeuL1ATXdPXHi1wnwlXX2jBw+hL0fvZnx2OPje7qbXOJwoo/hlHkAc2cP/Jx00drFbcpGdk9XXWb/HPevrlzWar4e0w+/6yyplYzr8r9X210FM4UqxXVpUnwoaCcvL6NUW46v7Xkfihlg2UwEsHs0bpaMPLLlKGIjTJjVI90tG8enIlOo3s6uw2f+mGby/YFW465THAb1VO5qzExLsolu+rnqw/hmEKQQhSJ5BNYA7H085H/bnbc1pN9VW8bbuifBYvZhCt6Z2Bop+aSfBnyId4tR4o0X+uHTUcl+UTsxBV7+2Ql62qXklDJ4nqxlynsg4HBSARz9IyoXDF0SE3AzudG4b2bzgtaBD2kozNZmbyK557jJbh31p+6X2vm7wfR46lFksf2nwqPbIpE/hBjtQS1hHxJhXLphUu7OU+SeoMR+0XUWzdk47Pb+ku+o+SF9K7sU7eEVasI4EcrXYMR+wTWwR2aITHWdSm0XqEyTCPumfo/A+YBamEwEsHsc0ZCKRGOVtDz7vJ9bp//wIhOmttPlVRpbieKNN9PHOy47c/6UEr2iArciTsvXr22t6gN2q9h/74a1EE9y3JqgnRybGF53efeno9IiT1gEbNfsDVt7N0qmD71WWav7ed7Zlt/iI1ynvKHd3Gf0iGYghcSU8ixR/16uk6NIE9T/N1G98XvJgxrhzd+2a26feG2Ap/bRRRO2oiW9hs1hUCcf8RdAsZV/3cRdhQUY5hGfqSrsltiypxNjvv2uTBaS2szkl3nnfl6wTbrjgHYc6IUvQ1OdmZnMpkw775BOF5cgS7pie6fEETsGYlgniztDSb7Erp+bVLc7itPIhRjteCK3upXP0QkJe6N9NeSd0+XjIpP9u6yp6YnxWJ451TNXlT5tpz6lTSNY5zX33df2E71OZsPF+La91c7hoq9TVHfKMaKPpnJITVRtE9msuYkYaOE1lmIgqqqvsaLP3IL+JP9KqbGzdr8oZ2aK2aPrVSoXUNE7nmT+0PJ5SqTysVai3pkxAGIv4aKxEM1Sqvtqmuk8zi2iSa5XvHOKqw7eAY/bz8OILgVjiMVj3AEO1ddd7KP82H9fCDYh43czUBv1UQ5e+Cibcd1v1daon++fIkasjG9WqBzWgLOb9fUL6+nZ4nw81f1wNjeGfj6rvMlQzP+6kQ4W649P2xge+nvOvuPfNV93fXWkO9C6yxEQVVRn/grPjo4OUT0smdwdJe1MD7K93a/8JeGXYWTyB9m3NgXgiD4bThBXu1WSWpCLN6+wTVF+6bDhfhbju8TPqtVVrD8/uhFWLn3FC7ukqr7tQ6cKvO5PaSNPSMRrLy6bpldnB9O6v5k7xn5cfMxlNSndFdKDX99/yyf3ysmih8BIsC/CbC0qm670yIpsPVSMpLjcG1OptuJsmIrZWnUyf/4TRyhBEHA1iN1Vy+xIRaMiMdnH/g6F4By1WD5ahtvBCuzLFEkuc6HpawjuvonMZee4p73DGvvl/ci3zEYiVC7jjtTrYfeMI3zisVewtuTQlOeJPPxRy0OIpKK8+I7Zc7dA/HWDdnonJ7gfmcd9CzHDaEUSxGPc0Yi1OEzzm5UPYXygkkpQPAknbI84ZGWUCoURRQuar1If96/rful/J7QM4lWqTC4Uo0aCjx+E0eoOz5bb3QTVFlleU8EQUDRuWr9z/cguGLPCJH/nS6VrmS5qEsqUhNiMOfugUFrg7eJyqoUemEfulQ7szP5jt/EEeoC0bK2riGWiU8eTFTV2nDnp/qDJ/kVUVyUBY+NDq06DEThTD4P7YreGVj72MV+7/3Q4m1m6UqFxG93X8i5JYHGYCRC9WzpTE/syazyYCiVFdOqtQnYrmOpoJ38imjHc5ehZXK84r5pia7JkIjIN/KvlCiLOehZSOU9rO50qZ+ropSFVs+QD/mGRzhCbT1al22wrw9lsQPlws7SrKoPf7NZZU9lxaIhHXuhqku6uc7QT2kULUkPTUT+Ie8ZMaL+1dj6shBqyREBQDw7pEl93RqlYRoKPH4TR6hVe08DAM6UhV4VW3lK6vlbjnn0/HPVznTw19cvMZTPDfn4lhyc1zp4XcZEkaR7hnToN8qAuVmje6bjmwkD0SlV3+qc1ftPY9vRIjQKsYzUkYI9IxFo30lnGe+Dp8sNbIn3XrpaPXOquMDeUZXkSxd1SUNSXJTf20VErgnUYgwY5jCZTOjXJgVJ8eqf8wtlta3GvLXSpWfkpvN9T65I7jEYiUD2RGIN0Z1D2mLnc5dpZl8V5yT5dffJYDSLiLSE1rQ0h/PbNcX9F3WQPGafM5KeGIvfHhmOZ6/oYUTTIg6DkQi0+bCzOqW4cmZD8NueU24zxoonpdaKEgmM0VFJlIj8o1OaM0NyKGc6zmraSHK/uL4ERZTVhMyU+JCb4B+uGIxEuFAdqshKUQ6Sjpx1X/NCHIyIx4tvG9wGgPaENiLyj0aiyeHRIbwa5URJheR+Sf1qvlBuczji0Y5woRrzq2VG1TsR7t5h7dEo2oInLu/meOy81in4ZcpQLJo81C9tJCJ14kmsQV7V65EaWXVfRzASwr054YjBSBg6U1aFoS8vw6uLdrnfOUS/JWqV8jRD/9XKI5d1wbZnL0NKo2jJ4x1SEyRXbEQUGI+IakQlxIbuZ04+idVeKZzZmYOLRzsMffr7QeSdKcc7y/a63Tc0QxEgs4nyMM0UpmUmahASY6Pw6t964/HRXdFaNi8jlPQQJYAExMM0ofrtGJ4YjES4EO0YwT//2kvx8f5tmBuEqKG45rxWuHNoO6OboUmesTn/TF26A/aMBBePdhjyJNvhfcM6uN/JAOlJymnavS1+RUSkx+7jJQA4gTXYeLTDkNXNh+janFaO2yMU0qSHMtaIIKJA2lSf+oA9I8HFox2G3J2w7RM47x3W8CpRsmeEiIKBFz7BxaMdhsTDNDUKRZ9strqVKtYGeGI3ouAWEYW30T3TXR47XRp6dbvCGYORMCReN1+hUA67pj4YsXhYYjvY4hQyrbobgiIi8tSrf+vt8hh7YYOL3+xh6KWFOx23dx4rdtlea+8ZCfFehh8mDsL4ga3xwlXO2hANsTeHiEJbvEKl3rPl7BkJJgYjYahK1BtyzfurJdvOllVh0bYCAKEf+XdMS8AzV/ZA1xbOTI4MRogoGLYddb2Qo8BhMBJhbvhgDc6W12UYbCgn9maNYhy3Qz2AIiIizzEYCUN/7dtKddvOghLHbXOoZjyTyWoajyfGdMXLf+0FUwNpMxE1LEM6NjO6CRGNwUgYSozTVwdiwZZjAW6J/9wxpB2u7ZdpdDOIKEzJa9R8ND7HoJZEptCtXkRes09QdWf9obMBbgkRUcPQTTQ3beHkIeiSnqixN/kbg5EwVC0rif197hFc2aclBFkl3P8TVdUkIopkF3Rohpev6YUOqY0ZiBiAwzRhSJ7obNLsXABA0blqyeN9s5KD1CIiotB3bU4m+mY1MboZEYnBSBhSG6b5cbN0jggTiBERUSjg2SgMVasEI/KFKNUKqeKJiIiCjcFIGKpWSAEPQJI8DACOF1cEozlERESaGIyEobKqGsXHZfNXkdIoOgitISIi0sZgJAxVqfSM2GTRSKe0hGA0h4iISBODkTAknwsSG1X3ZxZX8507YSDSEmOD2i4iIiIlDEbC0J95hZL70fWrZuw9I53TEpDTJiXYzSIiIlLkcTCyYsUKjB07FhkZGTCZTJg3b57m/t9++y0uueQSNG/eHImJiRg4cCAWLVrkbXvJCwmxUQCcS37NLDZHREQhxONgpKysDL1798aMGTN07b9ixQpccsklWLBgATZs2IDhw4dj7Nix2Lhxo8eNJffkWVYB5zBNbf02phchIqJQ4nE6+FGjRmHUqFG693/zzTcl91988UV8//33+N///ofs7GxP357cOFla6bj98S05uG3mekePSG2tPRhhNEJERKEj6LVpbDYbSkpKkJKiPmehsrISlZXOk2pxcXEwmhYWNucXOW4nx9ct3T14uhyF5VXOnhGO0hARUQgJ+iXyq6++itLSUlx77bWq+0yfPh1JSUmOn8xMlo7X647P1jtuW0VzQ6Z+uwU2m71nhNEIERGFjqAGI19++SWeeeYZzJkzB6mpqar7TZ06FUVFRY6f/Pz8ILYyfJhF+d9/2lrg6Bkxy/PCExERGShowzSzZ8/GHXfcgW+++QYjRozQ3DcmJgYxMTFBaln4Eic565yW4Jg7wp4RIiIKJUHpGfnqq69w66234quvvsKYMWOC8ZaGUVrNYtR7izOxjuyehur6Cays1ktERKHE47NSaWkpcnNzkZubCwA4cOAAcnNzkZeXB6BuiOXmm2927P/ll1/i5ptvxmuvvYYBAwagoKAABQUFKCoqUnr5BuVEcQU25Rc67r+1ZA/6vbAEh8+WG9KeWlm13sax0o6vuRvy6/djtV4iIgodHgcj69evR3Z2tmNZ7pQpU5CdnY1p06YBAI4dO+YITADgP//5D2pqanDfffehRYsWjp9Jkyb56VcwTv8Xl+DKGauw7WhdYPX64t04VVqJ13/erbj/iZIKvP/rPpwWLb/1pxpRMHLH4Lbokp4I+4hMZY0Na/afAQCs2ns6IO9PRETkDY/njAwbNkxzKGLmzJmS+8uXL/f0LRqE/DPO3o8Nh86ie0aS4/7aA2cUn3PHp+ux+XARlu86gdl3DfR7m8Q1ac5v1xQAcM+w9pixbB8qVYrnERERGY2TB7y0ThRwTPt+m2SI5EjhOcXnbD5c14Ni76HwN3EhvGGdmwMAoi0WAMDM3w86tl3dt2VA3p+IiMgbDEa8VCvrHfrgt/0GtcRJPExjXzETbXX9E1+bw7wtREQUOhiMeCmlPrup3Us/7TSoJU419RNToywmmEzqwUgUV9MQEVEI4VnJS1p5wxJig55lH4BzmMYqqj2jFIwUn6sOWpuIiIjcYTDipRqb+iTev/ZtFcSW1DleXIEfNx8DAFhFxWeiFQrRlFXVBK1dRERE7jAY8ZJ4sqhcTJTzsJ4qrZRMdvWn73OPoM2j87F2/2lc+sYK/HNh3VCRuCaNUs9I22aNAtIeIiIibxgznhAGajQSh2057EzoNvTlZSivqsUnt/RT3Le8qgYxVotXKdonzc4FAFz3nzWSx8UZVpXmh3RMTfD4vYiIiAKFPSNe0lo98/u+0zhWdA6CIKC8qhYA8PDcTS77HS08h+xnF+MfX/3p17ZFiQIbeTDSKa2xYm8JERGRUXhW8tLWI8Wa2wdOX4qy+kAEAE6VVrns88qiXaissWHBlgJMX7BD93ufKKnAj5uPqm4X94xEi273aJmI+fcP0f0+REREwcBhmgCqqVUeymkcU3fYv9t4xPHYv1fsx2erD2HHc5e5fd2xb6/E8WL1lPKSCayiXpA2TRtxWS8REYUcnpk8IAgCzpa59nCoEVfN7ZuV7LhdWVOLyppal/3PVbs+pkQrEAGA/SfLnK8p6p05rdA7Q0REZDT2jHig7dQFAICbzs/Stb+4HkyeqJZNda2Ank//7FUbiis8yxGyIe+s4/bmw4VevScREVEgsWdEJ3GPyBdrnFWJF00eio/G5yg+Z2N+oeO2fM5IlUrhuhnL9mq2o6jcs2BkTM8WjtsslkdERKGIwYhOB06XSe7bJ4YmxFpxcdc0vH9TX5fn3P/VRo/f55VFu1Ci0fuhUTBZUeum8Y7bWonaiIiIjMJgxEv2PCP2yaKX9WiB/m1S/PLaxRX+y5AaF2Xx22sREREFAoMRnQRZl4S9k0FcB+btG7P98l4VOieyqhEHRVauniEiohDHM5VuyhlSxcto0xJj/fJOr/28S3WbVuZXu5E90hUff/Lybl63iYiIKFC4mkanapWcIVYv0ri7s2BLgeo2PfM+kuKiJPf3vDAKe0+Uoks608ATEVHoYTCik9rqF/EwTTCoBUV2Y3q2wJV9MiSPRVnM6NoiMZDNIiIi8hqHaXTadlQ5/bu8Z+SN63oHtB21Gj0jY3tnYMa4vsyySkREDQrPWjp9t/Gwy2NmE2CWBSNXZbdSfY3/3jPQ53ZU16oHI1NHdfH59YmIiIKNwYhOxedcl9t6mrYjO7OJ6raxvaVDK9W1NlRU1+Kb9fk4UVwheVxN41iOuhERUcPDYESnAlFA4M6+F0fjjsFtXR43m024tFua4nP6t5EGKjd9uBZvLN6Nh+duxlXv/u54fO4G1x4au8TYKNVtREREoYrBSABYzCY8obKM9uVrerk89uTl3XBD/yxJhd21B85g8fbjAIAjheccuUfUgpH7L+7oa7OJiIgMwWBEp4u7pAIAHh7Z2afXSY6PlqxsWf7QMNw+uC2sFjPGDVAvwNflyYVYtE19ya+Nqd6JiKiBYjCik/1U37xxjO7n/DJlqOLj4sUubZo1ctyOj5ambt9/SloP5+7PN6i+F+vOEBFRQ8VgRKdzVXXDJLHR+mu9dEhNcElABgAmlWyu0Rbv68gMaOefujhERETBxmBEp8qaumAk2sMcHt/dewEA4M4hzgmtJpWkreLU8notefBCfHpbfwzr1Nzj5xIREYUCrgXVqap+SW2M1bNgpF3zxjgwfTRMahGISCMPel0AoEt6Ato1a4T2zRt79DwiIqJQwp4Rnezp4KM9DEYAuAQik+pXvlyd3VLyuKcVdhfcP0RXkENERBTK2DOikz0Y8bRnRMnFXdOw7vGLXSbDdm3hWSE7efZXIiKihog9IzqJe0b+8/fzEB9twfs39fX69VITYl16NfpmNcGjOlO692qV5PV7ExERhRIGIzqV1a+mibaacWn3dGx5eiQu69HCr+9hMpkw4cL2uvbNTIn363sTEREZhcM0OtTaBBSdqwYAR0Vci4FDJFf2ycCTKhleiYiIGhoGIzrsPl4S1PeLsZpRWaNeEO9f12cHsTVERESBxWEaHd5assdxu50oY2qgpCbqz/JKRETU0DEY0eGPg2cct4OxlLZRNDusiIgocjAY0eFUaVVQ3++Fq3o4bv/9/NaSbfPvHxzUthAREQUag5EQdF5rZ50ZeQG87hlc0ktEROGFwUiI21VQbHQTiIiIAorBSIjrlpFodBOIiIgCisFIiIu2WPD02LqcImN6+TfJGhERUSjgso0QF2UxYfwFbdCvbQo6pnpWu4aIiKghYDDigf+7TF/dGH9qHGOFyWTixFUiIgpbHKZxwyZazXJtTqugve+0y7uhX5smuHVw26C9JxERkRHYM+JGVa0zLXtMlCVo73vb4La4jYEIERFFAPaMuFFRXeu4HWvl4SIiIvI3nl3dsBess5hNsFp4uIiIiPyNZ1c3th+tSzpWK8uESkRERP4R8cHI1iNFaPPofMxae0hx+2PfbQlyi4iIiCJLxAcjl7+9EgDw+HdbFbeL54wQERGR/0V8MCImCK5DMRXVNoU9iYiIyF8YjIjYJ6uKtWveyICWEBERRQ4GIyLv/7rPpXdk+zFWzSUiIgokBiMib/6yB3M3HJY8pjByQ0RERH7EYERm+a6Tkvstk+MAAP3aNDGiOURERGGPwYhM+9TGkvtdWyQCAK7uG7y6NERERJEk4oORrJR4yf3UhBjJ/cqauqW9sVERf6iIiIgCIuLPsPI8IjW10hU19hU2MdbgFckjIiKKJB4HIytWrMDYsWORkZEBk8mEefPmuX3O8uXL0bdvX8TExKBDhw6YOXOmF00NDHkwIl/e6wxGIj5uIyIiCgiPz7BlZWXo3bs3ZsyYoWv/AwcOYMyYMRg+fDhyc3MxefJk3HHHHVi0aJHHjQ2Eksoayf3pP+2U3K+sD1bYM0JERBQYVk+fMGrUKIwaNUr3/u+//z7atm2L1157DQDQtWtXrFy5Em+88QZGjhzp6dv7VUV1rdulu/aeEc4ZISIiCoyAn2FXr16NESNGSB4bOXIkVq9erfqcyspKFBcXS34C4Y+DZ9zuw54RIiKiwAp4MFJQUIC0tDTJY2lpaSguLsa5c+cUnzN9+nQkJSU5fjIzMwPdTFWOOSPsGSEiIgqIkDzDTp06FUVFRY6f/Pz8gLzP3z9a57j94lU9FfepcPSMhOShIiIiavA8njPiqfT0dBw/flzy2PHjx5GYmIi4uDjF58TExCAmJkZxmz8lx0ehsLwaAHBx11TgO9d9nHNGOExDREQUCAG/3B84cCCWLFkieWzx4sUYOHBgoN/aLZvNOXtV3PORd7oc3+ceQWF5FWrq92HPCBERUWB43DNSWlqKvXv3Ou4fOHAAubm5SElJQVZWFqZOnYojR47gs88+AwBMmDAB77zzDh555BHcdtttWLp0KebMmYP58+f777fwUu/MZPy25xTioiySCapDX1nmsi8nsBIREQWGx8HI+vXrMXz4cMf9KVOmAADGjx+PmTNn4tixY8jLy3Nsb9u2LebPn48HHngA//rXv9CqVSt8+OGHhi/rBYBX/9Yb7yzdi5vOb41oNz0f7rYTERGRd0yC4C7ThvGKi4uRlJSEoqIiJCYmBux92jyq3ltz8KUxAXtfIiKicKT3/M3LfR3Ob5didBOIiIjCFoMRHVomx7vfiYiIiLzCYEQHzhchIiIKHJ5ldTCbjG4BERFR+GIwoiI9MdZx+6etBQa2hIiIKLwxGFHxw8RBjttnyqoMbAkREVF4YzCiIsrCQ0NERBQMPOOKvDeur+N2UlyUgS0hIiKKHAEvlNeQjOrZAruevwwWkwlm0azVG/pnGdgqIiKi8MZgREZcg2bJgxdizf7TuC4n08AWERERhTcGIxraN2+M9s0bG90MIiKisMY5I0RERGQoBiNERERkKAYjREREZCgGI0RERGQoBiNERERkKAYjREREZCgGI0RERGQoBiNERERkKAYjREREZCgGI0RERGQoBiNERERkKAYjREREZCgGI0RERGSoBlG1VxAEAEBxcbHBLSEiIiK97Odt+3lcTYMIRkpKSgAAmZmZBreEiIiIPFVSUoKkpCTV7SbBXbgSAmw2G44ePYqEhASYTCa/vW5xcTEyMzORn5+PxMREv70uueKxDg4e5+DgcQ4OHufgCORxFgQBJSUlyMjIgNmsPjOkQfSMmM1mtGrVKmCvn5iYyP/oQcJjHRw8zsHB4xwcPM7BEajjrNUjYscJrERERGQoBiNERERkqIgORmJiYvDUU08hJibG6KaEPR7r4OBxDg4e5+DgcQ6OUDjODWICKxEREYWviO4ZISIiIuMxGCEiIiJDMRghIiIiQzEYISIiIkNFdDAyY8YMtGnTBrGxsRgwYADWrVtndJNC1vTp09GvXz8kJCQgNTUVf/nLX7Br1y7JPhUVFbjvvvvQtGlTNG7cGH/9619x/PhxyT55eXkYM2YM4uPjkZqaiocffhg1NTWSfZYvX46+ffsiJiYGHTp0wMyZMwP964Wsl156CSaTCZMnT3Y8xuPsH0eOHMFNN92Epk2bIi4uDj179sT69esd2wVBwLRp09CiRQvExcVhxIgR2LNnj+Q1zpw5g3HjxiExMRHJycm4/fbbUVpaKtln8+bNGDJkCGJjY5GZmYmXX345KL9fqKitrcWTTz6Jtm3bIi4uDu3bt8dzzz0nqVXCY+25FStWYOzYscjIyIDJZMK8efMk24N5TL/55ht06dIFsbGx6NmzJxYsWOD5LyREqNmzZwvR0dHCxx9/LGzbtk248847heTkZOH48eNGNy0kjRw5Uvjkk0+ErVu3Crm5ucLo0aOFrKwsobS01LHPhAkThMzMTGHJkiXC+vXrhfPPP1+44IILHNtramqEHj16CCNGjBA2btwoLFiwQGjWrJkwdepUxz779+8X4uPjhSlTpgjbt28X3n77bcFisQgLFy4M6u8bCtatWye0adNG6NWrlzBp0iTH4zzOvjtz5ozQunVr4ZZbbhHWrl0r7N+/X1i0aJGwd+9exz4vvfSSkJSUJMybN0/YtGmTcMUVVwht27YVzp0759jnsssuE3r37i2sWbNG+O2334QOHToIN9xwg2N7UVGRkJaWJowbN07YunWr8NVXXwlxcXHCv//976D+vkZ64YUXhKZNmwo//vijcODAAeGbb74RGjduLPzrX/9y7MNj7bkFCxYIjz/+uPDtt98KAITvvvtOsj1Yx3TVqlWCxWIRXn75ZWH79u3CE088IURFRQlbtmzx6PeJ2GCkf//+wn333ee4X1tbK2RkZAjTp083sFUNx4kTJwQAwq+//ioIgiAUFhYKUVFRwjfffOPYZ8eOHQIAYfXq1YIg1H14zGazUFBQ4NjnvffeExITE4XKykpBEAThkUceEbp37y55r+uuu04YOXJkoH+lkFJSUiJ07NhRWLx4sXDhhRc6ghEeZ//4v//7P2Hw4MGq2202m5Ceni688sorjscKCwuFmJgY4auvvhIEQRC2b98uABD++OMPxz4//fSTYDKZhCNHjgiCIAjvvvuu0KRJE8dxt793586d/f0rhawxY8YIt912m+Sxq6++Whg3bpwgCDzW/iAPRoJ5TK+99lphzJgxkvYMGDBAuPvuuz36HSJymKaqqgobNmzAiBEjHI+ZzWaMGDECq1evNrBlDUdRUREAICUlBQCwYcMGVFdXS45ply5dkJWV5Timq1evRs+ePZGWlubYZ+TIkSguLsa2bdsc+4hfw75PpP1d7rvvPowZM8blWPA4+8cPP/yAnJwc/O1vf0Nqaiqys7PxwQcfOLYfOHAABQUFkmOUlJSEAQMGSI5zcnIycnJyHPuMGDECZrMZa9eudewzdOhQREdHO/YZOXIkdu3ahbNnzwb61wwJF1xwAZYsWYLdu3cDADZt2oSVK1di1KhRAHisAyGYx9Rf3yURGYycOnUKtbW1ki9rAEhLS0NBQYFBrWo4bDYbJk+ejEGDBqFHjx4AgIKCAkRHRyM5OVmyr/iYFhQUKB5z+zatfYqLi3Hu3LlA/DohZ/bs2fjzzz8xffp0l208zv6xf/9+vPfee+jYsSMWLVqEe+65B/fffz8+/fRTAM7jpPUdUVBQgNTUVMl2q9WKlJQUj/4W4e7RRx/F9ddfjy5duiAqKgrZ2dmYPHkyxo0bB4DHOhCCeUzV9vH0mDeIqr0UWu677z5s3boVK1euNLopYSc/Px+TJk3C4sWLERsba3RzwpbNZkNOTg5efPFFAEB2dja2bt2K999/H+PHjze4deFlzpw5mDVrFr788kt0794dubm5mDx5MjIyMnisySEie0aaNWsGi8XisgLh+PHjSE9PN6hVDcPEiRPx448/YtmyZWjVqpXj8fT0dFRVVaGwsFCyv/iYpqenKx5z+zatfRITExEXF+fvXyfkbNiwASdOnEDfvn1htVphtVrx66+/4q233oLVakVaWhqPsx+0aNEC3bp1kzzWtWtX5OXlAXAeJ63viPT0dJw4cUKyvaamBmfOnPHobxHuHn74YUfvSM+ePfH3v/8dDzzwgKPnj8fa/4J5TNX28fSYR2QwEh0djfPOOw9LlixxPGaz2bBkyRIMHDjQwJaFLkEQMHHiRHz33XdYunQp2rZtK9l+3nnnISoqSnJMd+3ahby8PMcxHThwILZs2SL5ACxevBiJiYmOE8PAgQMlr2HfJ1L+LhdffDG2bNmC3Nxcx09OTg7GjRvnuM3j7LtBgwa5LE3fvXs3WrduDQBo27Yt0tPTJceouLgYa9eulRznwsJCbNiwwbHP0qVLYbPZMGDAAMc+K1asQHV1tWOfxYsXo3PnzmjSpEnAfr9QUl5eDrNZeqqxWCyw2WwAeKwDIZjH1G/fJR5Ndw0js2fPFmJiYoSZM2cK27dvF+666y4hOTlZsgKBnO655x4hKSlJWL58uXDs2DHHT3l5uWOfCRMmCFlZWcLSpUuF9evXCwMHDhQGDhzo2G5fcnrppZcKubm5wsKFC4XmzZsrLjl9+OGHhR07dggzZsyIqCWnSsSraQSBx9kf1q1bJ1itVuGFF14Q9uzZI8yaNUuIj48XvvjiC8c+L730kpCcnCx8//33wubNm4Urr7xScWlkdna2sHbtWmHlypVCx44dJUsjCwsLhbS0NOHvf/+7sHXrVmH27NlCfHx82C43VTJ+/HihZcuWjqW93377rdCsWTPhkUcecezDY+25kpISYePGjcLGjRsFAMLrr78ubNy4UTh06JAgCME7pqtWrRKsVqvw6quvCjt27BCeeuopLu311Ntvvy1kZWUJ0dHRQv/+/YU1a9YY3aSQBUDx55NPPnHsc+7cOeHee+8VmjRpIsTHxwtXXXWVcOzYMcnrHDx4UBg1apQQFxcnNGvWTHjwwQeF6upqyT7Lli0T+vTpI0RHRwvt2rWTvEckkgcjPM7+8b///U/o0aOHEBMTI3Tp0kX4z3/+I9lus9mEJ598UkhLSxNiYmKEiy++WNi1a5dkn9OnTws33HCD0LhxYyExMVG49dZbhZKSEsk+mzZtEgYPHizExMQILVu2FF566aWA/26hpLi4WJg0aZKQlZUlxMbGCu3atRMef/xxyXJRHmvPLVu2TPE7efz48YIgBPeYzpkzR+jUqZMQHR0tdO/eXZg/f77Hv49JEERp8IiIiIiCLCLnjBAREVHoYDBCREREhmIwQkRERIZiMEJERESGYjBCREREhmIwQkRERIZiMEJERESGYjBCREREhmIwQkRERIZiMEJERESGYjBCREREhmIwQkRERIb6fxZHd2/VCYXFAAAAAElFTkSuQmCC\n", 187 | "text/plain": [ 188 | "
" 189 | ] 190 | }, 191 | "metadata": {}, 192 | "output_type": "display_data" 193 | } 194 | ], 195 | "source": [ 196 | "df.plot()" 197 | ] 198 | }, 199 | { 200 | "cell_type": "code", 201 | "execution_count": 7, 202 | "id": "09644bd2", 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "df.to_csv(\"../tests/helpers/example.csv\", index=False)" 207 | ] 208 | }, 209 | { 210 | "cell_type": "code", 211 | "execution_count": null, 212 | "id": "1b7287a9", 213 | "metadata": {}, 214 | "outputs": [], 215 | "source": [] 216 | } 217 | ], 218 | "metadata": { 219 | "kernelspec": { 220 | "display_name": "Python 3 (ipykernel)", 221 | "language": "python", 222 | "name": "python3" 223 | }, 224 | "language_info": { 225 | "codemirror_mode": { 226 | "name": "ipython", 227 | "version": 3 228 | }, 229 | "file_extension": ".py", 230 | "mimetype": "text/x-python", 231 | "name": "python", 232 | "nbconvert_exporter": "python", 233 | "pygments_lexer": "ipython3", 234 | "version": "3.9.6" 235 | } 236 | }, 237 | "nbformat": 4, 238 | "nbformat_minor": 5 239 | } 240 | --------------------------------------------------------------------------------