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.
Files changed (66) hide show
  1. {quantplus-0.0.4 → quantplus-0.0.5}/PKG-INFO +1 -1
  2. quantplus-0.0.5/docs/source/api.rst +45 -0
  3. quantplus-0.0.5/docs/source/architecture.rst +103 -0
  4. quantplus-0.0.5/docs/source/development.rst +93 -0
  5. quantplus-0.0.5/docs/source/examples.rst +168 -0
  6. quantplus-0.0.5/docs/source/finance.rst +249 -0
  7. quantplus-0.0.5/docs/source/index.rst +60 -0
  8. quantplus-0.0.5/docs/source/installation.rst +94 -0
  9. quantplus-0.0.5/docs/source/min_versions.rst +58 -0
  10. quantplus-0.0.5/docs/source/release-history.rst +40 -0
  11. quantplus-0.0.5/docs/source/usage.rst +220 -0
  12. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_version.py +3 -3
  13. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/PKG-INFO +1 -1
  14. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/SOURCES.txt +5 -0
  15. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/scm_file_list.json +5 -0
  16. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/scm_version.json +2 -2
  17. quantplus-0.0.4/docs/source/index.rst +0 -15
  18. quantplus-0.0.4/docs/source/installation.rst +0 -7
  19. quantplus-0.0.4/docs/source/min_versions.rst +0 -28
  20. quantplus-0.0.4/docs/source/release-history.rst +0 -6
  21. quantplus-0.0.4/docs/source/usage.rst +0 -9
  22. {quantplus-0.0.4 → quantplus-0.0.5}/.codecov.yml +0 -0
  23. {quantplus-0.0.4 → quantplus-0.0.5}/.coveragerc +0 -0
  24. {quantplus-0.0.4 → quantplus-0.0.5}/.flake8 +0 -0
  25. {quantplus-0.0.4 → quantplus-0.0.5}/.gitattributes +0 -0
  26. {quantplus-0.0.4 → quantplus-0.0.5}/.github/CONTRIBUTING.md +0 -0
  27. {quantplus-0.0.4 → quantplus-0.0.5}/.github/ISSUE_TEMPLATE.md +0 -0
  28. {quantplus-0.0.4 → quantplus-0.0.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  29. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/ci.yml +0 -0
  30. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/cibuildwheel.yml +0 -0
  31. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/docs.yml +0 -0
  32. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/flake8.yml +0 -0
  33. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/publish-pypi.yml +0 -0
  34. {quantplus-0.0.4 → quantplus-0.0.5}/.github/workflows/testing.yml +0 -0
  35. {quantplus-0.0.4 → quantplus-0.0.5}/.gitignore +0 -0
  36. {quantplus-0.0.4 → quantplus-0.0.5}/.isort.cfg +0 -0
  37. {quantplus-0.0.4 → quantplus-0.0.5}/.pre-commit-config.yaml +0 -0
  38. {quantplus-0.0.4 → quantplus-0.0.5}/AUTHORS.rst +0 -0
  39. {quantplus-0.0.4 → quantplus-0.0.5}/CONTRIBUTING.rst +0 -0
  40. {quantplus-0.0.4 → quantplus-0.0.5}/LICENSE +0 -0
  41. {quantplus-0.0.4 → quantplus-0.0.5}/MANIFEST.in +0 -0
  42. {quantplus-0.0.4 → quantplus-0.0.5}/README.rst +0 -0
  43. {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/editable_install.sh +0 -0
  44. {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/install.sh +0 -0
  45. {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/macos_install_eigen.sh +0 -0
  46. {quantplus-0.0.4 → quantplus-0.0.5}/continuous_integration/scripts/pre_build.sh +0 -0
  47. {quantplus-0.0.4 → quantplus-0.0.5}/docs/Makefile +0 -0
  48. {quantplus-0.0.4 → quantplus-0.0.5}/docs/make.bat +0 -0
  49. {quantplus-0.0.4 → quantplus-0.0.5}/docs/source/_static/.placeholder +0 -0
  50. {quantplus-0.0.4 → quantplus-0.0.5}/docs/source/conf.py +0 -0
  51. {quantplus-0.0.4 → quantplus-0.0.5}/examples/example.py +0 -0
  52. {quantplus-0.0.4 → quantplus-0.0.5}/pyproject.toml +0 -0
  53. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/__init__.py +0 -0
  54. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_crr_pricer.cpp +0 -0
  55. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/_crr_pricer.pyx +0 -0
  56. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/csrc/crr_core.cpp +0 -0
  57. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/csrc/crr_core.h +0 -0
  58. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/tests/__init__.py +0 -0
  59. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus/tests/test_quantplus.py +0 -0
  60. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/dependency_links.txt +0 -0
  61. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/requires.txt +0 -0
  62. {quantplus-0.0.4 → quantplus-0.0.5}/quantplus.egg-info/top_level.txt +0 -0
  63. {quantplus-0.0.4 → quantplus-0.0.5}/requirements-dev.txt +0 -0
  64. {quantplus-0.0.4 → quantplus-0.0.5}/requirements.txt +0 -0
  65. {quantplus-0.0.4 → quantplus-0.0.5}/setup.cfg +0 -0
  66. {quantplus-0.0.4 → quantplus-0.0.5}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantplus
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Summary: Cox-Ross-Rubinstein binomial option pricer (C++ core, Cython bindings)
5
5
  Author-email: Your Name <you@example.com>
6
6
  License: MIT
@@ -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.