quantplus 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {quantplus-0.0.4 → quantplus-0.0.5}/PKG-INFO +1 -1
- quantplus-0.0.5/docs/source/api.rst +45 -0
- quantplus-0.0.5/docs/source/architecture.rst +103 -0
- quantplus-0.0.5/docs/source/development.rst +93 -0
- quantplus-0.0.5/docs/source/examples.rst +168 -0
- quantplus-0.0.5/docs/source/finance.rst +249 -0
- quantplus-0.0.5/docs/source/index.rst +60 -0
- quantplus-0.0.5/docs/source/installation.rst +94 -0
- quantplus-0.0.5/docs/source/min_versions.rst +58 -0
- quantplus-0.0.5/docs/source/release-history.rst +40 -0
- quantplus-0.0.5/docs/source/usage.rst +220 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_version.py +3 -3
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/PKG-INFO +1 -1
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/SOURCES.txt +5 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/scm_file_list.json +5 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/scm_version.json +2 -2
- quantplus-0.0.4/docs/source/index.rst +0 -15
- quantplus-0.0.4/docs/source/installation.rst +0 -7
- quantplus-0.0.4/docs/source/min_versions.rst +0 -28
- quantplus-0.0.4/docs/source/release-history.rst +0 -6
- quantplus-0.0.4/docs/source/usage.rst +0 -9
- {quantplus-0.0.4 → quantplus-0.0.5}/.codecov.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.coveragerc +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.flake8 +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.gitattributes +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/CONTRIBUTING.md +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/ISSUE_TEMPLATE.md +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/ci.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/cibuildwheel.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/docs.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/flake8.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/publish-pypi.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/testing.yml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.gitignore +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.isort.cfg +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/.pre-commit-config.yaml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/AUTHORS.rst +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/CONTRIBUTING.rst +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/LICENSE +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/MANIFEST.in +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/README.rst +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/editable_install.sh +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/install.sh +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/macos_install_eigen.sh +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/pre_build.sh +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/docs/Makefile +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/docs/make.bat +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/docs/source/_static/.placeholder +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/docs/source/conf.py +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/examples/example.py +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/pyproject.toml +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/__init__.py +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_crr_pricer.cpp +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_crr_pricer.pyx +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/csrc/crr_core.cpp +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/csrc/crr_core.h +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/tests/__init__.py +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/tests/test_quantplus.py +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/dependency_links.txt +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/requires.txt +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/top_level.txt +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/requirements-dev.txt +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/requirements.txt +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/setup.cfg +0 -0
- {quantplus-0.0.4 → quantplus-0.0.5}/setup.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
===
|
|
2
|
+
API
|
|
3
|
+
===
|
|
4
|
+
|
|
5
|
+
This page summarizes the public API exposed by the package.
|
|
6
|
+
|
|
7
|
+
Top-level functions
|
|
8
|
+
-------------------
|
|
9
|
+
|
|
10
|
+
The package exports the following functions at the top level:
|
|
11
|
+
|
|
12
|
+
.. autofunction:: quantplus.crr_price
|
|
13
|
+
|
|
14
|
+
.. autofunction:: quantplus.black_scholes_price
|
|
15
|
+
|
|
16
|
+
.. autofunction:: quantplus.crr_tree
|
|
17
|
+
|
|
18
|
+
Module overview
|
|
19
|
+
---------------
|
|
20
|
+
|
|
21
|
+
The public package is intentionally lightweight and exposes only the functions
|
|
22
|
+
that users are expected to call directly. The numerical implementation itself is
|
|
23
|
+
kept in the compiled extension and C++ source files.
|
|
24
|
+
|
|
25
|
+
You can inspect the runtime package directly in Python:
|
|
26
|
+
|
|
27
|
+
.. code-block:: python
|
|
28
|
+
|
|
29
|
+
import quantplus as qp
|
|
30
|
+
|
|
31
|
+
print(qp.__all__)
|
|
32
|
+
print(qp.__version__)
|
|
33
|
+
|
|
34
|
+
This helps developers confirm which functions are part of the supported public
|
|
35
|
+
surface.
|
|
36
|
+
|
|
37
|
+
Other useful considerations
|
|
38
|
+
---------------------------
|
|
39
|
+
|
|
40
|
+
- ``crr_price`` is the main pricing function for binomial-tree valuation.
|
|
41
|
+
- ``black_scholes_price`` is the analytic benchmark for comparison.
|
|
42
|
+
- ``crr_tree`` is useful when you want the tree terminal nodes or payoffs.
|
|
43
|
+
|
|
44
|
+
The package may evolve over time, but the public interface remains intentionally
|
|
45
|
+
stable and easy to inspect.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
============
|
|
2
|
+
Architecture
|
|
3
|
+
============
|
|
4
|
+
|
|
5
|
+
``quantplus`` is intentionally small and modular. The project separates the
|
|
6
|
+
public Python API from the performance-critical numerical implementation so that
|
|
7
|
+
users interact with a clean interface while the expensive work runs in compiled
|
|
8
|
+
code.
|
|
9
|
+
|
|
10
|
+
High-level layout
|
|
11
|
+
-----------------
|
|
12
|
+
|
|
13
|
+
.. code-block:: text
|
|
14
|
+
|
|
15
|
+
quantplus/
|
|
16
|
+
├── __init__.py # public exports
|
|
17
|
+
├── _crr_pricer.pyx # Cython bridge to native C++
|
|
18
|
+
├── csrc/
|
|
19
|
+
│ ├── crr_core.cpp # CRR and Black-Scholes math
|
|
20
|
+
│ └── crr_core.h # C interface declarations
|
|
21
|
+
└── tests/
|
|
22
|
+
└── test_quantplus.py # regression and validation tests
|
|
23
|
+
|
|
24
|
+
Public Python API
|
|
25
|
+
------------------
|
|
26
|
+
|
|
27
|
+
The package user interface lives in ``quantplus/__init__.py``. This file imports
|
|
28
|
+
and re-exports the functions that are intended for normal use:
|
|
29
|
+
|
|
30
|
+
- ``crr_price``
|
|
31
|
+
- ``black_scholes_price``
|
|
32
|
+
- ``crr_tree``
|
|
33
|
+
|
|
34
|
+
This makes the API easy to discover and keeps the import path simple:
|
|
35
|
+
|
|
36
|
+
.. code-block:: python
|
|
37
|
+
|
|
38
|
+
import quantplus as qp
|
|
39
|
+
|
|
40
|
+
price = qp.crr_price(...)
|
|
41
|
+
|
|
42
|
+
Cython bridge layer
|
|
43
|
+
-------------------
|
|
44
|
+
|
|
45
|
+
The ``_crr_pricer.pyx`` file is a thin wrapper around the compiled C++ core. It
|
|
46
|
+
is written in Cython so that Python can call native functions while preserving a
|
|
47
|
+
Pythonic API.
|
|
48
|
+
|
|
49
|
+
This is where the package handles several concerns:
|
|
50
|
+
|
|
51
|
+
- argument validation
|
|
52
|
+
- conversion from Python values to native types
|
|
53
|
+
- calling the C++ implementation
|
|
54
|
+
- packaging the result back into native Python objects
|
|
55
|
+
|
|
56
|
+
The bridge is deliberately lightweight. It does not duplicate the numerical logic;
|
|
57
|
+
it mostly delegates to the native implementation and enforces sensible input
|
|
58
|
+
checks.
|
|
59
|
+
|
|
60
|
+
Native C++ implementation
|
|
61
|
+
-------------------------
|
|
62
|
+
|
|
63
|
+
The actual model logic is implemented in ``quantplus/csrc/crr_core.cpp``.
|
|
64
|
+
It exposes plain C-linkage functions through ``crr_core.h``. The functions are:
|
|
65
|
+
|
|
66
|
+
- ``crr_price``
|
|
67
|
+
- ``black_scholes_price``
|
|
68
|
+
- ``crr_price_with_terminal_nodes``
|
|
69
|
+
|
|
70
|
+
These functions are defined using a simple C ABI so Cython can call them without
|
|
71
|
+
relying on C++ name mangling or class-based object models.
|
|
72
|
+
|
|
73
|
+
This design matters because it keeps the compiled part:
|
|
74
|
+
|
|
75
|
+
- fast
|
|
76
|
+
- easy to call from Cython
|
|
77
|
+
- independent of Python object management
|
|
78
|
+
|
|
79
|
+
Build pipeline
|
|
80
|
+
--------------
|
|
81
|
+
|
|
82
|
+
The package is built through ``setup.py`` and the metadata in ``pyproject.toml``.
|
|
83
|
+
The build process does the following:
|
|
84
|
+
|
|
85
|
+
1. Cython compiles ``_crr_pricer.pyx`` into a Python extension.
|
|
86
|
+
2. The native C++ source is compiled and linked into the same extension module.
|
|
87
|
+
3. Python imports the resulting ``quantplus._crr_pricer`` extension at runtime.
|
|
88
|
+
|
|
89
|
+
This hybrid approach is common in numerical libraries: Python handles the public
|
|
90
|
+
interface and automation, while the inner loop runs in compiled code.
|
|
91
|
+
|
|
92
|
+
Why this architecture works well
|
|
93
|
+
--------------------------------
|
|
94
|
+
|
|
95
|
+
The package stays easy to understand because each layer has a clear responsibility:
|
|
96
|
+
|
|
97
|
+
- Python: user-facing API and validation
|
|
98
|
+
- Cython: interop glue and conversion
|
|
99
|
+
- C++: performance-critical option-pricing logic
|
|
100
|
+
- tests: numerical correctness and regression protection
|
|
101
|
+
|
|
102
|
+
This separation keeps the code readable while still giving the package the speed of
|
|
103
|
+
native execution for repeated computations and lattice evaluations.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
===========
|
|
2
|
+
Development
|
|
3
|
+
===========
|
|
4
|
+
|
|
5
|
+
This project is small enough that development remains lightweight, but it still
|
|
6
|
+
benefits from a clear workflow for building, testing, and documenting the package.
|
|
7
|
+
|
|
8
|
+
Recommended environment
|
|
9
|
+
------------------------
|
|
10
|
+
|
|
11
|
+
Use a virtual environment for local development:
|
|
12
|
+
|
|
13
|
+
.. code-block:: bash
|
|
14
|
+
|
|
15
|
+
python -m venv .venv
|
|
16
|
+
source .venv/bin/activate
|
|
17
|
+
python -m pip install -U pip
|
|
18
|
+
python -m pip install -r requirements-dev.txt
|
|
19
|
+
|
|
20
|
+
Then install the project itself in editable mode:
|
|
21
|
+
|
|
22
|
+
.. code-block:: bash
|
|
23
|
+
|
|
24
|
+
python -m pip install -e .
|
|
25
|
+
|
|
26
|
+
This allows changes to the Python files and compiled extension to be picked up
|
|
27
|
+
without reinstalling the package repeatedly.
|
|
28
|
+
|
|
29
|
+
Running tests
|
|
30
|
+
-------------
|
|
31
|
+
|
|
32
|
+
The project uses ``pytest`` for regression and validation testing. Run the suite
|
|
33
|
+
with:
|
|
34
|
+
|
|
35
|
+
.. code-block:: bash
|
|
36
|
+
|
|
37
|
+
pytest
|
|
38
|
+
|
|
39
|
+
This checks the pricing functions against known values and ensures that numerical
|
|
40
|
+
behavior stays consistent after changes.
|
|
41
|
+
|
|
42
|
+
Building the extension
|
|
43
|
+
----------------------
|
|
44
|
+
|
|
45
|
+
The package compiles the native C++ and Cython extension as part of installation.
|
|
46
|
+
If you want to trigger a fresh build manually, use:
|
|
47
|
+
|
|
48
|
+
.. code-block:: bash
|
|
49
|
+
|
|
50
|
+
python setup.py build_ext --inplace
|
|
51
|
+
|
|
52
|
+
This builds the extension in place so it can be imported directly from the source
|
|
53
|
+
checkout during testing and debugging.
|
|
54
|
+
|
|
55
|
+
Building the docs
|
|
56
|
+
-----------------
|
|
57
|
+
|
|
58
|
+
The documentation is built with Sphinx. From the project root, run:
|
|
59
|
+
|
|
60
|
+
.. code-block:: bash
|
|
61
|
+
|
|
62
|
+
python -m sphinx -b html docs/source docs/build/html
|
|
63
|
+
|
|
64
|
+
This creates a local HTML build that can be reviewed in the browser.
|
|
65
|
+
|
|
66
|
+
Project conventions
|
|
67
|
+
-------------------
|
|
68
|
+
|
|
69
|
+
A few conventions help keep the package maintainable:
|
|
70
|
+
|
|
71
|
+
- keep the public API small and deliberate
|
|
72
|
+
- prefer simple, explicit function signatures
|
|
73
|
+
- validate common inputs early
|
|
74
|
+
- add regression tests when changing pricing math
|
|
75
|
+
- document numerical assumptions and edge cases clearly
|
|
76
|
+
|
|
77
|
+
When the pricing logic is modified, it is important to validate the change against
|
|
78
|
+
known examples and the existing test suite.
|
|
79
|
+
|
|
80
|
+
Contribution workflow
|
|
81
|
+
---------------------
|
|
82
|
+
|
|
83
|
+
A typical contribution flow looks like this:
|
|
84
|
+
|
|
85
|
+
1. create a feature branch
|
|
86
|
+
2. make a focused change
|
|
87
|
+
3. add or update tests
|
|
88
|
+
4. run the relevant test subset
|
|
89
|
+
5. update documentation if behavior or usage changes
|
|
90
|
+
6. submit a pull request with a clear summary
|
|
91
|
+
|
|
92
|
+
This keeps the project coherent and makes it easier to review numerical changes
|
|
93
|
+
that may affect pricing outcomes.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
========
|
|
2
|
+
Examples
|
|
3
|
+
========
|
|
4
|
+
|
|
5
|
+
This page shows a few representative ways to use ``quantplus`` in practice.
|
|
6
|
+
|
|
7
|
+
European call valuation
|
|
8
|
+
-----------------------
|
|
9
|
+
|
|
10
|
+
.. code-block:: python
|
|
11
|
+
|
|
12
|
+
import quantplus as qp
|
|
13
|
+
|
|
14
|
+
price = qp.crr_price(
|
|
15
|
+
S0=100.0,
|
|
16
|
+
K=100.0,
|
|
17
|
+
r=0.05,
|
|
18
|
+
sigma=0.20,
|
|
19
|
+
T=1.0,
|
|
20
|
+
N=200,
|
|
21
|
+
q=0.01,
|
|
22
|
+
is_call=True,
|
|
23
|
+
american=False,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
print(f"European call price: {price:.6f}")
|
|
27
|
+
|
|
28
|
+
European put valuation
|
|
29
|
+
----------------------
|
|
30
|
+
|
|
31
|
+
.. code-block:: python
|
|
32
|
+
|
|
33
|
+
import quantplus as qp
|
|
34
|
+
|
|
35
|
+
put_price = qp.crr_price(
|
|
36
|
+
S0=95.0,
|
|
37
|
+
K=100.0,
|
|
38
|
+
r=0.04,
|
|
39
|
+
sigma=0.25,
|
|
40
|
+
T=2.0,
|
|
41
|
+
N=500,
|
|
42
|
+
q=0.02,
|
|
43
|
+
is_call=False,
|
|
44
|
+
american=False,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
print(f"European put price: {put_price:.6f}")
|
|
48
|
+
|
|
49
|
+
American option valuation
|
|
50
|
+
-------------------------
|
|
51
|
+
|
|
52
|
+
.. code-block:: python
|
|
53
|
+
|
|
54
|
+
import quantplus as qp
|
|
55
|
+
|
|
56
|
+
american_put = qp.crr_price(
|
|
57
|
+
S0=90.0,
|
|
58
|
+
K=100.0,
|
|
59
|
+
r=0.03,
|
|
60
|
+
sigma=0.30,
|
|
61
|
+
T=1.5,
|
|
62
|
+
N=300,
|
|
63
|
+
q=0.0,
|
|
64
|
+
is_call=False,
|
|
65
|
+
american=True,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
print(f"American put price: {american_put:.6f}")
|
|
69
|
+
|
|
70
|
+
Comparing CRR and Black-Scholes
|
|
71
|
+
-------------------------------
|
|
72
|
+
|
|
73
|
+
.. code-block:: python
|
|
74
|
+
|
|
75
|
+
import quantplus as qp
|
|
76
|
+
|
|
77
|
+
s0, k, r, sigma, t, q = 100.0, 100.0, 0.05, 0.20, 1.0, 0.01
|
|
78
|
+
|
|
79
|
+
crr_value = qp.crr_price(
|
|
80
|
+
S0=s0,
|
|
81
|
+
K=k,
|
|
82
|
+
r=r,
|
|
83
|
+
sigma=sigma,
|
|
84
|
+
T=t,
|
|
85
|
+
N=500,
|
|
86
|
+
q=q,
|
|
87
|
+
is_call=True,
|
|
88
|
+
american=False,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
bs_value = qp.black_scholes_price(
|
|
92
|
+
S0=s0,
|
|
93
|
+
K=k,
|
|
94
|
+
r=r,
|
|
95
|
+
sigma=sigma,
|
|
96
|
+
T=t,
|
|
97
|
+
q=q,
|
|
98
|
+
is_call=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
print(f"CRR price: {crr_value:.6f}")
|
|
102
|
+
print(f"Black-Scholes price: {bs_value:.6f}")
|
|
103
|
+
|
|
104
|
+
Inspecting the terminal tree
|
|
105
|
+
----------------------------
|
|
106
|
+
|
|
107
|
+
The ``crr_tree`` function is useful when you want to inspect the terminal node
|
|
108
|
+
values or build custom visualizations.
|
|
109
|
+
|
|
110
|
+
.. code-block:: python
|
|
111
|
+
|
|
112
|
+
import quantplus as qp
|
|
113
|
+
|
|
114
|
+
root, stock_prices, payoffs = qp.crr_tree(
|
|
115
|
+
S0=100.0,
|
|
116
|
+
K=100.0,
|
|
117
|
+
r=0.05,
|
|
118
|
+
sigma=0.20,
|
|
119
|
+
T=1.0,
|
|
120
|
+
N=5,
|
|
121
|
+
q=0.01,
|
|
122
|
+
is_call=True,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
print(root)
|
|
126
|
+
print(stock_prices)
|
|
127
|
+
print(payoffs)
|
|
128
|
+
|
|
129
|
+
This pattern is especially useful when you are debugging the model or comparing
|
|
130
|
+
values across a lattice.
|
|
131
|
+
|
|
132
|
+
.. plot::
|
|
133
|
+
:caption: Terminal call and put payoffs at the stock-price nodes returned by ``crr_tree``.
|
|
134
|
+
|
|
135
|
+
import matplotlib.pyplot as plt
|
|
136
|
+
import quantplus as qp
|
|
137
|
+
|
|
138
|
+
parameters = dict(S0=100.0, K=100.0, r=0.05, sigma=0.20, T=1.0, N=8, q=0.01)
|
|
139
|
+
_, call_stocks, call_payoffs = qp.crr_tree(**parameters, is_call=True)
|
|
140
|
+
_, put_stocks, put_payoffs = qp.crr_tree(**parameters, is_call=False)
|
|
141
|
+
|
|
142
|
+
fig, ax = plt.subplots(figsize=(8, 4.5))
|
|
143
|
+
ax.scatter(call_stocks, call_payoffs, color="#147d78", s=42, label="Call")
|
|
144
|
+
ax.scatter(put_stocks, put_payoffs, color="#c05a36", s=42, label="Put")
|
|
145
|
+
ax.axvline(parameters["K"], color="#667780", linestyle="--", linewidth=1,
|
|
146
|
+
label="Strike")
|
|
147
|
+
ax.set_xlabel("Stock price at expiry")
|
|
148
|
+
ax.set_ylabel("Option payoff at expiry")
|
|
149
|
+
ax.set_title("Terminal payoffs on the CRR lattice")
|
|
150
|
+
ax.grid(alpha=0.25)
|
|
151
|
+
ax.legend(frameon=False)
|
|
152
|
+
fig.tight_layout()
|
|
153
|
+
|
|
154
|
+
Running a small sensitivity check
|
|
155
|
+
---------------------------------
|
|
156
|
+
|
|
157
|
+
.. code-block:: python
|
|
158
|
+
|
|
159
|
+
import quantplus as qp
|
|
160
|
+
|
|
161
|
+
params = dict(S0=100.0, K=100.0, r=0.05, T=1.0, N=200, q=0.01)
|
|
162
|
+
|
|
163
|
+
for sigma in [0.10, 0.20, 0.30, 0.40]:
|
|
164
|
+
price = qp.crr_price(sigma=sigma, is_call=True, **params)
|
|
165
|
+
print(f"sigma={sigma:.2f}, price={price:.6f}")
|
|
166
|
+
|
|
167
|
+
As volatility increases, option prices generally increase for standard long
|
|
168
|
+
positions, which is consistent with the intuition behind the model.
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
==========================================
|
|
2
|
+
Finance and CRR(Cox-Ross-Rubinstein) model
|
|
3
|
+
==========================================
|
|
4
|
+
|
|
5
|
+
This page explains the pricing model used by ``quantplus`` and the assumptions
|
|
6
|
+
behind it. The package implements the Cox-Ross-Rubinstein (CRR) binomial tree
|
|
7
|
+
model for option pricing, along with the closed-form Black-Scholes benchmark.
|
|
8
|
+
|
|
9
|
+
Option pricing problem
|
|
10
|
+
----------------------
|
|
11
|
+
|
|
12
|
+
An option is a derivative whose value depends on the future behavior of an
|
|
13
|
+
underlying asset. At a high level, the task is to estimate the present value of
|
|
14
|
+
a future payoff under a model for the underlying price process.
|
|
15
|
+
|
|
16
|
+
For a European option, the value at maturity is known from the payoff function:
|
|
17
|
+
|
|
18
|
+
- call payoff: max(S_T - K, 0)
|
|
19
|
+
- put payoff: max(K - S_T, 0)
|
|
20
|
+
|
|
21
|
+
where:
|
|
22
|
+
|
|
23
|
+
- ``S_T`` is the underlying price at maturity
|
|
24
|
+
- ``K`` is the strike price
|
|
25
|
+
|
|
26
|
+
The CRR model provides a discrete-time approximation to the risky asset dynamics.
|
|
27
|
+
|
|
28
|
+
CRR binomial tree model
|
|
29
|
+
-----------------------
|
|
30
|
+
|
|
31
|
+
The Cox-Ross-Rubinstein framework assumes that the underlying price moves through
|
|
32
|
+
a recombining binomial tree over discrete time steps. At each step, the price can
|
|
33
|
+
move up or down by multiplicative factors:
|
|
34
|
+
|
|
35
|
+
- ``u``: up factor
|
|
36
|
+
- ``d``: down factor
|
|
37
|
+
|
|
38
|
+
For a standard CRR construction:
|
|
39
|
+
|
|
40
|
+
- ``u = exp(sigma * sqrt(dt))``
|
|
41
|
+
- ``d = 1 / u``
|
|
42
|
+
|
|
43
|
+
where ``dt = T / N`` and ``N`` is the number of time steps.
|
|
44
|
+
|
|
45
|
+
The risk-neutral up probability is:
|
|
46
|
+
|
|
47
|
+
.. math::
|
|
48
|
+
|
|
49
|
+
p = \frac{e^{(r-q)dt} - d}{u - d}
|
|
50
|
+
|
|
51
|
+
where:
|
|
52
|
+
|
|
53
|
+
- ``r`` is the risk-free rate
|
|
54
|
+
- ``q`` is the dividend yield or carry adjustment
|
|
55
|
+
- ``sigma`` is the volatility
|
|
56
|
+
|
|
57
|
+
This probability is chosen so that the expected discounted stock return matches
|
|
58
|
+
the risk-free rate under the risk-neutral measure.
|
|
59
|
+
|
|
60
|
+
.. plot::
|
|
61
|
+
:caption: A four-step CRR stock-price lattice. Different paths recombine at shared nodes.
|
|
62
|
+
|
|
63
|
+
from math import exp, sqrt
|
|
64
|
+
|
|
65
|
+
import matplotlib.pyplot as plt
|
|
66
|
+
|
|
67
|
+
initial_price = 100.0
|
|
68
|
+
volatility = 0.25
|
|
69
|
+
expiry = 1.0
|
|
70
|
+
steps = 4
|
|
71
|
+
up = exp(volatility * sqrt(expiry / steps))
|
|
72
|
+
down = 1.0 / up
|
|
73
|
+
|
|
74
|
+
fig, ax = plt.subplots(figsize=(9, 4.5))
|
|
75
|
+
for level in range(steps):
|
|
76
|
+
for up_moves in range(level + 1):
|
|
77
|
+
x = level
|
|
78
|
+
y = 2 * up_moves - level
|
|
79
|
+
for next_up_moves in (up_moves, up_moves + 1):
|
|
80
|
+
next_x = level + 1
|
|
81
|
+
next_y = 2 * next_up_moves - next_x
|
|
82
|
+
ax.plot([x, next_x], [y, next_y], color="#91a4ad", linewidth=1.2, zorder=1)
|
|
83
|
+
|
|
84
|
+
for level in range(steps + 1):
|
|
85
|
+
for up_moves in range(level + 1):
|
|
86
|
+
y = 2 * up_moves - level
|
|
87
|
+
stock_price = initial_price * up**up_moves * down ** (level - up_moves)
|
|
88
|
+
ax.scatter(level, y, s=90, color="#147d78", edgecolor="white", zorder=2)
|
|
89
|
+
ax.annotate(f"{stock_price:.0f}", (level, y), xytext=(0, 10),
|
|
90
|
+
textcoords="offset points", ha="center", fontsize=8)
|
|
91
|
+
|
|
92
|
+
ax.set_xticks(range(steps + 1), [f"Step {step}" for step in range(steps + 1)])
|
|
93
|
+
ax.set_ylabel("Up/down state")
|
|
94
|
+
ax.set_title("Recombining CRR stock-price lattice")
|
|
95
|
+
ax.set_ylim(-steps - 0.8, steps + 0.8)
|
|
96
|
+
ax.grid(axis="x", alpha=0.2)
|
|
97
|
+
ax.spines[["top", "right", "left"]].set_visible(False)
|
|
98
|
+
ax.tick_params(axis="y", left=False, labelleft=False)
|
|
99
|
+
fig.tight_layout()
|
|
100
|
+
|
|
101
|
+
Backward induction
|
|
102
|
+
------------------
|
|
103
|
+
|
|
104
|
+
The option value is computed recursively from maturity back to the present. At the
|
|
105
|
+
terminal nodes, the value is the payoff:
|
|
106
|
+
|
|
107
|
+
- call: ``max(S - K, 0)``
|
|
108
|
+
- put: ``max(K - S, 0)``
|
|
109
|
+
|
|
110
|
+
Then at each earlier node, the continuation value is computed as the discounted
|
|
111
|
+
expected value under the risk-neutral probabilities:
|
|
112
|
+
|
|
113
|
+
.. math::
|
|
114
|
+
|
|
115
|
+
V_{i,j} = e^{-r dt} \left( p V_{i+1,j+1} + (1-p) V_{i+1,j} \right)
|
|
116
|
+
|
|
117
|
+
For American options, the holder may exercise early, so the value is compared
|
|
118
|
+
against the immediate exercise payoff and the larger of the two is chosen:
|
|
119
|
+
|
|
120
|
+
.. math::
|
|
121
|
+
|
|
122
|
+
V_{i,j} = \max\left(V_{i,j}^{continuation}, V_{i,j}^{exercise}\right)
|
|
123
|
+
|
|
124
|
+
This is exactly the logic implemented in the C++ core of the package.
|
|
125
|
+
|
|
126
|
+
Assumptions of the CRR model
|
|
127
|
+
----------------------------
|
|
128
|
+
|
|
129
|
+
The CRR model is based on a set of simplifying assumptions:
|
|
130
|
+
|
|
131
|
+
- the underlying follows a discrete binomial process over time
|
|
132
|
+
- time is divided into a finite number of steps
|
|
133
|
+
- volatility is constant over the option life
|
|
134
|
+
- the risk-free rate is constant
|
|
135
|
+
- the dividend yield or carry rate is constant
|
|
136
|
+
- there are no arbitrage opportunities in the risk-neutral pricing setup
|
|
137
|
+
- the tree is recombining, so the number of states remains manageable
|
|
138
|
+
|
|
139
|
+
These assumptions are standard in introductory option-pricing models and are
|
|
140
|
+
well suited for educational and benchmark applications.
|
|
141
|
+
|
|
142
|
+
European vs American options
|
|
143
|
+
----------------------------
|
|
144
|
+
|
|
145
|
+
The package distinguishes between:
|
|
146
|
+
|
|
147
|
+
- European options: exercise only at maturity
|
|
148
|
+
- American options: exercise at any earlier node
|
|
149
|
+
|
|
150
|
+
In the pricing function, this is controlled by the ``american`` flag:
|
|
151
|
+
|
|
152
|
+
.. code-block:: python
|
|
153
|
+
|
|
154
|
+
price = qp.crr_price(
|
|
155
|
+
S0=100.0,
|
|
156
|
+
K=100.0,
|
|
157
|
+
r=0.05,
|
|
158
|
+
sigma=0.20,
|
|
159
|
+
T=1.0,
|
|
160
|
+
N=200,
|
|
161
|
+
q=0.01,
|
|
162
|
+
is_call=True,
|
|
163
|
+
american=False,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
For a European option, the code follows the standard backward-induction valuation
|
|
167
|
+
without early exercise checks. For an American option, it compares continuation
|
|
168
|
+
value against exercise value at each time step.
|
|
169
|
+
|
|
170
|
+
Black-Scholes benchmark
|
|
171
|
+
-----------------------
|
|
172
|
+
|
|
173
|
+
The package also exposes ``black_scholes_price``, which gives the closed-form
|
|
174
|
+
solution to the same problem under the continuous-time geometric Brownian motion
|
|
175
|
+
model. This is valuable for two reasons:
|
|
176
|
+
|
|
177
|
+
- it provides a familiar benchmark
|
|
178
|
+
- it shows how the binomial model approaches continuous-time pricing as ``N`` grows
|
|
179
|
+
|
|
180
|
+
The CRR tree becomes more accurate as the number of steps increases because the
|
|
181
|
+
lattice approximates the continuous diffusion more finely.
|
|
182
|
+
|
|
183
|
+
.. plot::
|
|
184
|
+
:caption: European call prices from the CRR tree approach the Black-Scholes benchmark as the number of steps increases.
|
|
185
|
+
|
|
186
|
+
import matplotlib.pyplot as plt
|
|
187
|
+
import quantplus as qp
|
|
188
|
+
|
|
189
|
+
spot = 100.0
|
|
190
|
+
strike = 100.0
|
|
191
|
+
rate = 0.05
|
|
192
|
+
volatility = 0.20
|
|
193
|
+
expiry = 1.0
|
|
194
|
+
dividend_yield = 0.01
|
|
195
|
+
step_counts = [5, 10, 20, 40, 80, 160, 320]
|
|
196
|
+
crr_prices = [
|
|
197
|
+
qp.crr_price(spot, strike, rate, volatility, expiry, count,
|
|
198
|
+
q=dividend_yield, is_call=True)
|
|
199
|
+
for count in step_counts
|
|
200
|
+
]
|
|
201
|
+
benchmark = qp.black_scholes_price(
|
|
202
|
+
spot, strike, rate, volatility, expiry, q=dividend_yield, is_call=True
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
fig, ax = plt.subplots(figsize=(8, 4.5))
|
|
206
|
+
ax.plot(step_counts, crr_prices, marker="o", color="#147d78", label="CRR")
|
|
207
|
+
ax.axhline(benchmark, color="#c05a36", linestyle="--", label="Black-Scholes")
|
|
208
|
+
ax.set_xscale("log", base=2)
|
|
209
|
+
ax.set_xticks(step_counts, [str(count) for count in step_counts])
|
|
210
|
+
ax.set_xlabel("Number of CRR steps")
|
|
211
|
+
ax.set_ylabel("European call price")
|
|
212
|
+
ax.set_title("CRR convergence to Black-Scholes")
|
|
213
|
+
ax.grid(alpha=0.25)
|
|
214
|
+
ax.legend(frameon=False)
|
|
215
|
+
fig.tight_layout()
|
|
216
|
+
|
|
217
|
+
How the code implements this
|
|
218
|
+
----------------------------
|
|
219
|
+
|
|
220
|
+
In the C++ core, the process is implemented as a nested loop over time steps and
|
|
221
|
+
stock states. The numerical sequence is:
|
|
222
|
+
|
|
223
|
+
1. compute the terminal payoffs
|
|
224
|
+
2. iterate backwards through time
|
|
225
|
+
3. compute continuation values using risk-neutral probabilities
|
|
226
|
+
4. compare with early exercise when required
|
|
227
|
+
5. return the root node value
|
|
228
|
+
|
|
229
|
+
In the Python layer, the function only validates user input and then calls the
|
|
230
|
+
compiled native implementation. This keeps the public interface simple while
|
|
231
|
+
preserving efficient numerical execution.
|
|
232
|
+
|
|
233
|
+
This means the model logic is not hidden in Python loops; the expensive
|
|
234
|
+
calculation runs inside the compiled C++ implementation, while user code remains
|
|
235
|
+
clean and readable.
|
|
236
|
+
|
|
237
|
+
Practical interpretation
|
|
238
|
+
------------------------
|
|
239
|
+
|
|
240
|
+
The CRR model is especially useful for:
|
|
241
|
+
|
|
242
|
+
- teaching option-pricing mechanics
|
|
243
|
+
- benchmarking numerical methods
|
|
244
|
+
- approximating option values when closed-form formulas are not available
|
|
245
|
+
- exploring early-exercise behavior for American options
|
|
246
|
+
|
|
247
|
+
It is not a complete market model for all exotic derivatives, but it is a clean,
|
|
248
|
+
transparent framework for vanilla options and for understanding how discrete-time
|
|
249
|
+
pricing works in practice.
|