aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShivesh Mandalia <shivesh.mandalia@outlook.com>2020-03-21 17:30:06 +0000
committerShivesh Mandalia <shivesh.mandalia@outlook.com>2020-03-21 17:30:06 +0000
commitc5df1cb77e6e40f701ecf002687d7b3932b28d8f (patch)
tree03535770c6510eb22230049403daf6a41c5cc392
downloadMCOptionPricing-c5df1cb77e6e40f701ecf002687d7b3932b28d8f.tar.gz
MCOptionPricing-c5df1cb77e6e40f701ecf002687d7b3932b28d8f.zip
Initial Commit
-rw-r--r--.gitignore4
-rw-r--r--.pylintrc583
-rw-r--r--README.md22
-rw-r--r--output.txt71
-rwxr-xr-xrun.py234
-rw-r--r--utils/__init__.py0
-rw-r--r--utils/engine.py106
-rw-r--r--utils/enums.py40
-rw-r--r--utils/misc.py65
-rw-r--r--utils/path.py135
-rw-r--r--utils/payoff.py265
11 files changed, 1525 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..749ccda
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 0000000..abc3182
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,583 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-whitelist=
+
+# Add files or directories to the blacklist. They should be base names, not
+# paths.
+ignore=CVS
+
+# Add files or directories matching the regex patterns to the blacklist. The
+# regex matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Specify a configuration file.
+#rcfile=
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=invalid-name,
+ import-outside-toplevel,
+ print-statement,
+ parameter-unpacking,
+ unpacking-in-except,
+ old-raise-syntax,
+ backtick,
+ long-suffix,
+ old-ne-operator,
+ old-octal-literal,
+ import-star-module-level,
+ non-ascii-bytes-literal,
+ raw-checker-failed,
+ bad-inline-option,
+ locally-disabled,
+ file-ignored,
+ suppressed-message,
+ useless-suppression,
+ deprecated-pragma,
+ use-symbolic-message-instead,
+ apply-builtin,
+ basestring-builtin,
+ buffer-builtin,
+ cmp-builtin,
+ coerce-builtin,
+ execfile-builtin,
+ file-builtin,
+ long-builtin,
+ raw_input-builtin,
+ reduce-builtin,
+ standarderror-builtin,
+ unicode-builtin,
+ xrange-builtin,
+ coerce-method,
+ delslice-method,
+ getslice-method,
+ setslice-method,
+ no-absolute-import,
+ old-division,
+ dict-iter-method,
+ dict-view-method,
+ next-method-called,
+ metaclass-assignment,
+ indexing-exception,
+ raising-string,
+ reload-builtin,
+ oct-method,
+ hex-method,
+ nonzero-method,
+ cmp-method,
+ input-builtin,
+ round-builtin,
+ intern-builtin,
+ unichr-builtin,
+ map-builtin-not-iterating,
+ zip-builtin-not-iterating,
+ range-builtin-not-iterating,
+ filter-builtin-not-iterating,
+ using-cmp-argument,
+ eq-without-hash,
+ div-method,
+ idiv-method,
+ rdiv-method,
+ exception-message-attribute,
+ invalid-str-codec,
+ sys-max-int,
+ bad-python3-import,
+ deprecated-string-function,
+ deprecated-str-translate-call,
+ deprecated-itertools-function,
+ deprecated-types-field,
+ next-method-defined,
+ dict-items-not-iterating,
+ dict-keys-not-iterating,
+ dict-values-not-iterating,
+ deprecated-operator-function,
+ deprecated-urllib-function,
+ xreadlines-attribute,
+ deprecated-sys-function,
+ exception-escape,
+ comprehension-escape
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+ XXX,
+ TODO
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+ bar,
+ baz,
+ toto,
+ tutu,
+ tata
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+ j,
+ k,
+ ex,
+ Run,
+ _
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the python-enchant package.
+spelling-dict=
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[STRING]
+
+# This flag controls whether the implicit-str-concat-in-sequence should
+# generate a warning on implicit string concatenation in sequences defined over
+# several lines.
+check-str-concat-over-line-jumps=no
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=no
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[LOGGING]
+
+# Format style used to check logging format string. `old` means using %
+# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+ _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
+# tab).
+indent-string=' '
+
+# Maximum number of characters on a single line.
+max-line-length=100
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# List of optional constructs for which whitespace checking is disabled. `dict-
+# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
+# `trailing-comma` allows a space between comma and closing bracket: (a, ).
+# `empty-line` allows space-only lines.
+no-space-check=trailing-comma,
+ dict-separator
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Create a graph of external dependencies in the given file (report RP0402 must
+# not be disabled).
+ext-import-graph=
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report RP0402 must not be disabled).
+import-graph=
+
+# Create a graph of internal dependencies in the given file (report RP0402 must
+# not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[CLASSES]
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+ __new__,
+ setUp,
+ __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+ _fields,
+ _replace,
+ _source,
+ _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+ Exception
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0fe26cc
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# Exotic options by Monte Carlo
+
+![Python Version](https://img.shields.io/badge/python-3.7+-blue.svg)
+
+B.7 Project 5 from Mark Joshi's "The Concepts and practice of mathematical finance", published by Cambridge University Press.
+
+### Features
+* **Arithmetic Asian options**
+* **Discrete barrier options**
+* **Antithetic variates**
+
+### Dependencies
+* [`Python`](https://www.python.org/) >= 3.7
+
+### Usage
+```
+python run.py
+```
+
+To see an example of the output see `output.txt`.
+
+By Shivesh Mandalia https://shivesh.org
diff --git a/output.txt b/output.txt
new file mode 100644
index 0000000..b3739b0
--- /dev/null
+++ b/output.txt
@@ -0,0 +1,71 @@
+==================
+Pricing Asian options
+==================
+(i) = 1.4101 +- 0.0569 with 10000 trials
+(i) = 1.4339 +- 0.0191 with 100000 trials
+(i) = 1.4292 +- 0.0060 with 1000000 trials
+==================
+(ii) = 1.4344 +- 0.0381 with 10000 trials
+(ii) = 1.3561 +- 0.0114 with 100000 trials
+(ii) = 1.3582 +- 0.0036 with 1000000 trials
+==================
+(iii) = 1.3944 +- 0.1291 with 10000 trials
+(iii) = 1.4562 +- 0.0388 with 100000 trials
+(iii) = 1.4790 +- 0.0124 with 1000000 trials
+==================
+How do the prices compare?
+We see that Asian options with more frequent setting dates are more expensive.
+This is because the averaging is less pronounced with more setting dates,
+making the Asian option more volatile.
+==================
+(vanilla) = 3.5564 +- 0.0471 with 10000 trials
+(vanilla) = 3.5470 +- 0.0148 with 100000 trials
+(vanilla) = 3.5491 +- 0.0047 with 1000000 trials
+==================
+How do the prices compare with a vanilla option?
+We see that Asian options are cheaper than vanilla options.
+This is because Asian options are less volatile, due to the averaging feature -
+the volatility of the averaged price is less volatile than the spot price.
+==================
+How does the speed of convergence vary?
+The rate of convergence is faster for sparser date settings, as seen by the
+standard error. More dense date settings converge slower as the timing
+evolution needs to be simulated. For the same reason, vanilla options converge
+the fastest.
+==================
+Pricing discrete barrier options
+==================
+(i) = 3.5064 +- 0.1214 with 10000 trials
+(i) = 3.5232 +- 0.0376 with 100000 trials
+(i) = 3.5354 +- 0.0119 with 1000000 trials
+==================
+(ii) = 0.0028 +- 0.0025 with 10000 trials
+(ii) = 0.0021 +- 0.0007 with 100000 trials
+(ii) = 0.0017 +- 0.0002 with 1000000 trials
+==================
+(iii) = 4.3125 +- 0.0856 with 10000 trials
+(iii) = 4.1930 +- 0.0282 with 100000 trials
+(iii) = 4.2188 +- 0.0089 with 1000000 trials
+==================
+(iv) = 0.0000 +- 0.0000 with 10000 trials
+(iv) = 0.0000 +- 0.0000 with 100000 trials
+(iv) = 0.0000 +- 0.0000 with 1000000 trials
+==================
+(vanilla call) = 3.5461 +- 0.0469 with 10000 trials
+(vanilla call) = 3.5543 +- 0.0148 with 100000 trials
+(vanilla call) = 3.5452 +- 0.0047 with 1000000 trials
+==================
+(vanilla put) = 4.5581 +- 0.0381 with 10000 trials
+(vanilla put) = 4.5026 +- 0.0118 with 100000 trials
+(vanilla put) = 4.5036 +- 0.0037 with 1000000 trials
+==================
+Compare prices and speed of convergence. Also compare prices with
+the vanilla option.
+Similar to Asian options, the rate of convergence is faster for sparser date
+settings. Vanilla options converge the fastest.
+Discrete barrier options can be much cheaper than vanilla options. This is
+because these type of options offer less flexibility compared to vanilla
+options, and thus this is priced in.
+==================
+Shivesh Mandalia https://shivesh.org/
+==================
diff --git a/run.py b/run.py
new file mode 100755
index 0000000..6a280e8
--- /dev/null
+++ b/run.py
@@ -0,0 +1,234 @@
+#! /usr/bin/env python3
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+
+"""
+Exotic options by Monte Carlo.
+
+B.7 Project 5 from Mark Joshi's "The Concepts and practice of mathematical
+finance", published by Cambridge University Press.
+
+"""
+
+import random
+from typing import List
+
+from utils.engine import PricingEngine
+from utils.path import PathGenerator
+from utils.payoff import AsianArithmeticPayOff, DiscreteBarrierPayOff
+from utils.payoff import VanillaPayOff
+
+
+__all__ = ['asian_options', 'discrete_barrier']
+
+
+def asian_options(ntrials_arr: List[int]) -> None:
+ """Pricing Asian options."""
+ # Having implemented the engine, price the following with
+ # S0=100, σ=0.1, r=0.05, d=0.03, and strike=103
+ print('==================')
+ print('Pricing Asian options')
+ print('==================')
+
+ path = PathGenerator(S=100, r=0.05, div=0.03, vol=0.1)
+ payoff = AsianArithmeticPayOff(K=103, option_right='Call')
+ engine = PricingEngine(payoff=payoff, path=path)
+
+ # (i) an Asian call option with maturity in one year and monthly setting
+ # dates.
+ T = [x / 12 for x in range(12 + 1)]
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(i) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # (ii) an Asian call option with maturity in one year and three month
+ # setting dates.
+ T = [x / 4 for x in range(4 + 1)]
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(ii) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # (iii) an Asian call option with maturity in one year and weekly setting
+ # dates.
+ T = [x / 52 for x in range(52 + 1)]
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(iii) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ print('''How do the prices compare?
+We see that Asian options with more frequent setting dates are more expensive.
+This is because the averaging is less pronounced with more setting dates,
+making the Asian option more volatile.''')
+ print('==================')
+
+ # Vanilla option
+ T = [0, 1]
+ engine.payoff = VanillaPayOff(K=103, option_right='Call')
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(vanilla) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ print('''How do the prices compare with a vanilla option?
+We see that Asian options are cheaper than vanilla options.
+This is because Asian options are less volatile, due to the averaging feature -
+the volatility of the averaged price is less volatile than the spot price.''')
+ print('==================')
+
+ print('''How does the speed of convergence vary?
+The rate of convergence is faster for sparser date settings, as seen by the
+standard error. More dense date settings converge slower as the timing
+evolution needs to be simulated. For the same reason, vanilla options converge
+the fastest.''')
+
+
+def discrete_barrier(ntrials_arr: List[int]) -> None:
+ """Pricing discrete barrier options."""
+ # Price some discrete barrier options, all with maturity one year and
+ # struck at 103.
+ print('==================')
+ print('Pricing discrete barrier options')
+ print('==================')
+
+ path = PathGenerator(S=100, r=0.05, div=0.03, vol=0.1)
+
+ # (i) a down-and-out call with barrier at 80 and monthly barrier dates.
+ payoff = DiscreteBarrierPayOff(
+ K=103, option_right='Call', B=80, barrier_updown='Down',
+ barrier_inout='Out'
+ )
+ engine = PricingEngine(payoff=payoff, path=path)
+ T = [x / 12 for x in range(12 + 1)]
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(i) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # (ii) a down-and-in call with barrier at 80 and monthly barrier dates.
+ engine.path = PathGenerator(S=84, r=0.05, div=0.03, vol=0.1)
+ engine.payoff = DiscreteBarrierPayOff(
+ K=103, option_right='Call', B=80, barrier_updown='Down',
+ barrier_inout='In'
+ )
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(ii) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # (iii) a down-and-out put with barrier at 80 and monthly barrier dates.
+ engine.path = path
+ engine.payoff = DiscreteBarrierPayOff(
+ K=103, option_right='Put', B=80, barrier_updown='Down',
+ barrier_inout='Out'
+ )
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(iii) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # (iv) a down-and-out put with barrier at 120 and barrier dates at
+ # 0.05, 0.15, ..., 0.95
+ engine.payoff = DiscreteBarrierPayOff(
+ K=103, option_right='Put', B=120, barrier_updown='Down',
+ barrier_inout='Out'
+ )
+ T = [x / 20 for x in range(20)]
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(iv) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # Vanilla call option
+ T = [0, 1]
+ engine.payoff = VanillaPayOff(K=103, option_right='Call')
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(vanilla call) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ # Vanilla put option
+ T = [0, 1]
+ engine.payoff = VanillaPayOff(K=103, option_right='Put')
+
+ # Price
+ for ntrials in ntrials_arr:
+ result = engine.price(T=T, ntrials=ntrials)
+ print('(vanilla put) = {0:.4f} +- {1:.4f} with {2} trials'.format(
+ result.price, result.stderr, int(ntrials)
+ ))
+ print('==================')
+
+ print('''Compare prices and speed of convergence. Also compare prices with
+the vanilla option.
+Similar to Asian options, the rate of convergence is faster for sparser date
+settings. Vanilla options converge the fastest.
+Discrete barrier options can be much cheaper than vanilla options. This is
+because these type of options offer less flexibility compared to vanilla
+options, and thus this is priced in.''')
+
+
+def main() -> None:
+ """Main function."""
+ random.seed(1)
+
+ # Define number of trials to run
+ ntrials_arr = [1E4, 1E5, 1E6]
+
+ # Pricing Asian options
+ asian_options(ntrials_arr)
+
+ # Pricing discrete barrier options
+ discrete_barrier(ntrials_arr)
+
+ print('==================')
+ print('Shivesh Mandalia https://shivesh.org/')
+ print('==================')
+
+
+main.__doc__ = __doc__
+
+
+if __name__ == '__main__':
+ main()
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/utils/__init__.py
diff --git a/utils/engine.py b/utils/engine.py
new file mode 100644
index 0000000..e8d2b1f
--- /dev/null
+++ b/utils/engine.py
@@ -0,0 +1,106 @@
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+"""
+Pricing engine for exotic options.
+"""
+
+import math
+from statistics import mean, stdev
+from dataclasses import dataclass
+from typing import List
+
+from utils.path import PathGenerator
+from utils.payoff import BasePayoff
+
+
+__all__ = ['MCResult', 'PricingEngine']
+
+
+@dataclass
+class MCResult:
+ """
+ Price of option along with its MC error.
+ """
+ price: float
+ stderr: float
+
+
+@dataclass
+class PricingEngine:
+ """
+ Class for generating underlying prices using MC techniques.
+
+ Attributes
+ ----------
+ payoff : Payoff object for calculating the options payoff.
+ path : PathGenerator object for generating the evolution of the underlying.
+
+ Methods
+ ----------
+ price()
+
+ Examples
+ ----------
+ >>> from utils.engine import PricingEngine
+ >>> from utils.path import PathGenerator
+ >>> from utils.payoff import AsianArithmeticPayOff
+ >>> path = PathGenerator(S=100., r=0.1, div=0.01, vol=0.3)
+ >>> payoff = AsianArithmeticPayOff(option_right='Call', K=110)
+ >>> engine = PricingEngine(payoff=payoff, path=path)
+ >>> print(engine.price(T=range(4)))
+ 2.1462567745518335
+
+ """
+ payoff: BasePayoff
+ path: PathGenerator
+
+ def price(self, T: List[float], ntrials: int = 1E4,
+ antithetic: bool = True) -> MCResult:
+ """
+ Price the option using MC techniques.
+
+ Parameters
+ ----------
+ T : Set of times {t1, t2, ..., tn} in years.
+ ntrials : Number of trials to simulate.
+ antithetic : Use antithetic variates technique.
+
+ Returns
+ ----------
+ MCResult : Price of the option.
+
+ """
+ if ntrials < len(T):
+ raise AssertionError('Number of trials cannot be less than the '
+ 'number of setting dates!')
+
+ # Generation start
+ ntrials = int(ntrials // len(T))
+ payoffs = [0] * ntrials
+ for idx in range(ntrials):
+ # Generate a random path
+ if not antithetic:
+ spot_prices = self.path.generate(T)
+ else:
+ prices_tuple = self.path.generate_antithetic(T)
+ spot_prices, a_spot_prices = prices_tuple
+
+ # Calculate the payoff
+ payoff = self.payoff.calculate(spot_prices)
+ if antithetic:
+ a_po = self.payoff.calculate(a_spot_prices)
+ payoff = (payoff + a_po) / 2
+ payoffs[idx] = payoff
+
+ # Discount to current time
+ df = math.exp(-self.path.net_r * (T[-1] - T[0]))
+ dis_payoffs = [x * df for x in payoffs]
+
+ # Payoff expectation and standard error
+ exp_payoff = mean(dis_payoffs)
+ stderr = stdev(dis_payoffs, exp_payoff) / math.sqrt(ntrials)
+
+ return MCResult(exp_payoff, stderr)
diff --git a/utils/enums.py b/utils/enums.py
new file mode 100644
index 0000000..6a607ea
--- /dev/null
+++ b/utils/enums.py
@@ -0,0 +1,40 @@
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+"""
+Enumeration utility classes.
+"""
+
+from enum import Enum, auto
+
+__all__ = ['OptionRight', 'BarrierUpDown', 'BarrierInOut']
+
+
+class PPEnum(Enum):
+ """Enum with prettier printing."""
+
+ def __repr__(self) -> str:
+ return super().__repr__().split('.')[1].split(':')[0]
+
+ def __str__(self) -> str:
+ return super().__str__().split('.')[1]
+
+
+class OptionRight(PPEnum):
+ """Right of an option."""
+ Call = auto()
+ Put = auto()
+
+
+class BarrierUpDown(PPEnum):
+ """Up or down type barrier option."""
+ Up = auto()
+ Down = auto()
+
+
+class BarrierInOut(PPEnum):
+ """In or out type barrier option."""
+ In = auto()
+ Out = auto()
diff --git a/utils/misc.py b/utils/misc.py
new file mode 100644
index 0000000..708d759
--- /dev/null
+++ b/utils/misc.py
@@ -0,0 +1,65 @@
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+"""
+Miscellaneous utility methods.
+"""
+
+
+__all__ = ['is_num', 'is_pos']
+
+
+def is_num(val: (int, float)) -> bool:
+ """
+ Check if the input value is a non-infinite number.
+
+ Parameters
+ ----------
+ val : Value to check.
+
+ Returns
+ ----------
+ is_num : Whether it is a non-infinite number.
+
+ Examples
+ ----------
+ >>> from utils.misc import is_num
+ >>> print(is_num(10))
+ True
+ >>> print(is_num(None))
+ False
+
+ """
+ if not isinstance(val, (int, float)):
+ return False
+ return True
+
+
+def is_pos(val: (int, float)) -> bool:
+ """
+ Check if the input value is a non-infinite positive number.
+
+ Parameters
+ ----------
+ val : Value to check.
+
+ Returns
+ ----------
+ is_pos : Whether it is a non-infinite positive number.
+
+ Examples
+ ----------
+ >>> from utils.misc import is_pos
+ >>> print(is_pos(10))
+ True
+ >>> print(is_pos(-10))
+ False
+
+ """
+ if not is_num(val):
+ return False
+ if not val >= 0:
+ return False
+ return True
diff --git a/utils/path.py b/utils/path.py
new file mode 100644
index 0000000..6855b7f
--- /dev/null
+++ b/utils/path.py
@@ -0,0 +1,135 @@
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+"""
+Path generator for underlying.
+"""
+
+import math
+import random
+from copy import deepcopy
+from dataclasses import dataclass
+from typing import List, Tuple
+
+
+__all__ = ['PathGenerator']
+
+
+@dataclass
+class PathGenerator:
+ """
+ Class for generating underlying prices using MC techniques.
+
+ Attributes
+ ----------
+ S : Spot price.
+ r : Risk-free interest rate.
+ div : Dividend yield.
+ vol : Volatility.
+ net_r : Net risk free rate.
+
+ Methods
+ ----------
+ generate(T)
+ Generate a random path {S_t1, S_t2, ..., S_tn}.
+ generate_antithetic(T)
+ Generate a random plus antithetic path
+ [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].
+
+ Examples
+ ----------
+ >>> from utils.path import PathGenerator
+ >>> path = PathGenerator(S=100., r=0.1, div=0.01, vol=0.3)
+ >>> print(path.generate(T=range(4)))
+ [100.0, 91.11981160354563, 94.87596210593794, 117.44132223235353]
+ >>> print(path.generate(T=range(4)))
+ [100.0, 68.73668230722738, 71.43490333826567, 70.70833180133955]
+
+ """
+ S: float
+ r: float
+ div: float
+ vol: float
+
+ @property
+ def net_r(self) -> float:
+ """Net risk free rate."""
+ return self.r - self.div
+
+ def generate(self, T: List[float]) -> List[float]:
+ """
+ Generate a random path {S_t1, S_t2, ..., S_tn}.
+
+ Parameters
+ ----------
+ T : Set of times {t1, t2, ..., tn} in years.
+
+ Returns
+ ----------
+ spot_prices : Set of prices for the underlying {S_t1, S_t2, ..., S_tn}.
+
+ """
+ # Calculate dt time differences
+ dts = [T[idx + 1] - T[idx] for idx in range(len(T) - 1)]
+
+ spot_prices = [0] * len(T)
+ spot_prices[0] = self.S
+ for idx, dt in enumerate(dts):
+ # Calculate the drift e^{(r - (1/2) σ²) Δt}
+ drift = math.exp((self.net_r - (1/2) * self.vol**2) * dt)
+
+ # Calculate the volatility term e^{σ √{Δt} N(0, 1)}
+ rdm_gauss = random.gauss(0, 1)
+ vol_term = math.exp(self.vol * math.sqrt(dt) * rdm_gauss)
+
+ # Calculate next spot price
+ S_t = spot_prices[idx] * drift * vol_term
+ spot_prices[idx + 1] = S_t
+ return spot_prices
+
+ def generate_antithetic(self, T: List[float]) -> Tuple[List[float],
+ List[float]]:
+ """
+ Generate a random plus antithetic path
+ [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].
+
+ Parameters
+ ----------
+ T : Set of times {t1, t2, ..., tn} in years.
+
+ Returns
+ ----------
+ prices_tuple : Set of prices for the underlying
+ [{S_t1, S_t2, ..., S_tn}, {S'_t1, S'_t2, ..., S'_tn}].
+
+ """
+ # Calculate dt time differences
+ dts = [T[idx + 1] - T[idx] for idx in range(len(T) - 1)]
+
+ # Create data structures
+ spot_prices = [0] * len(T)
+ spot_prices[0] = self.S
+
+ a_spot_prices = deepcopy(spot_prices)
+
+ for idx, dt in enumerate(dts):
+ # Calculate the drift e^{(r - (1/2) σ²) Δt}
+ drift = math.exp((self.net_r - (1/2) * self.vol**2) * dt)
+
+ # Calculate the volatility term e^{σ √{Δt} N(0, 1)}
+ rdm_gauss = random.gauss(0, 1)
+ a_gauss = -rdm_gauss
+ vol_term = math.exp(self.vol * math.sqrt(dt) * rdm_gauss)
+ a_vol_term = math.exp(self.vol * math.sqrt(dt) * a_gauss)
+
+ # Calculate next spot price
+ S_t = spot_prices[idx] * drift * vol_term
+ a_S_t = a_spot_prices[idx] * drift * a_vol_term
+
+ # Add to data structure
+ spot_prices[idx + 1] = S_t
+ a_spot_prices[idx + 1] = a_S_t
+
+ return (spot_prices, a_spot_prices)
diff --git a/utils/payoff.py b/utils/payoff.py
new file mode 100644
index 0000000..fffbdba
--- /dev/null
+++ b/utils/payoff.py
@@ -0,0 +1,265 @@
+# author : S. Mandalia
+# shivesh.mandalia@outlook.com
+#
+# date : March 19, 2020
+
+"""
+Payoff of an option.
+"""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import List
+
+from utils.enums import OptionRight, BarrierUpDown, BarrierInOut
+
+
+__all__ = ['BasePayoff', 'VanillaPayOff', 'AsianArithmeticPayOff',
+ 'DiscreteBarrierPayOff']
+
+
+@dataclass
+class BasePayoff(ABC):
+ """Base class for calculating the payoff."""
+ K: float
+ option_right: (str, OptionRight)
+
+ @property
+ def option_right(self) -> OptionRight:
+ """Right of the option."""
+ return self._option_right
+
+ @option_right.setter
+ def option_right(self, val: (str, OptionRight)) -> None:
+ """Set the option_right of the option."""
+ if isinstance(val, str):
+ if not hasattr(OptionRight, val):
+ or_names = [x.name for x in OptionRight]
+ raise ValueError(f'Invalid str {val}, expected {or_names}')
+ self._option_right = OptionRight[val]
+ elif isinstance(val, OptionRight):
+ self._option_right = val
+ else:
+ raise TypeError(
+ f'Expected str or OptionRight, instead got type {type(val)}!'
+ )
+
+ def _calculate_call(self, S: float) -> float:
+ """Call option."""
+ return max(S - self.K, 0.)
+
+ def _calculate_put(self, S: float) -> float:
+ """Put option."""
+ return max(self.K - S, 0.)
+
+ @abstractmethod
+ def calculate(self, S: float) -> float:
+ """Calulate the payoff for a given spot."""
+
+
+class VanillaPayOff(BasePayoff):
+ """
+ Class for calculating the payoff of a vanilla option.
+
+ Attributes
+ ----------
+ option_right : Right of the option.
+ K : Strike price.
+
+ Methods
+ ----------
+ calculate(S)
+ Calulate the payoff given a spot price.
+
+ Examples
+ ----------
+ >>> from utils.payoff import VanillaPayOff
+ >>> payoff = VanillaPayOff(option_right='Call', K=150.)
+ >>> print(payoff.calculate(160.))
+ 10.0
+
+ """
+
+ def calculate(self, S: (float, List[float])) -> float:
+ """
+ Calulate the payoff given a spot price.
+
+ Parameters
+ ----------
+ S : Spot price or list of spot prices.
+
+ Returns
+ ----------
+ payoff : Payoff.
+
+ Notes
+ ----------
+ If a list is given as input, the final entry will be taken to evaluate.
+
+ """
+ if not isinstance(S, float):
+ S = S[-1]
+ if self.option_right == OptionRight.Call:
+ payoff = self._calculate_call(S)
+ else:
+ payoff = self._calculate_put(S)
+ return payoff
+
+
+class AsianArithmeticPayOff(BasePayoff):
+ """
+ Class for calculating the payoff of an arithmetic Asian option.
+
+ Attributes
+ ----------
+ option_right : Right of the option.
+ K : Strike price.
+
+ Methods
+ ----------
+ calculate(S)
+ Calulate the payoff given a set of prices for the underlying.
+
+ Examples
+ ----------
+ >>> from utils.payoff import AsianArithmeticPayOff
+ >>> payoff = AsianArithmeticPayOff(option_right='Call', K=150)
+ >>> print(payoff.calculate([140, 150, 160, 170, 180]))
+ 10.0
+
+ """
+
+ def calculate(self, S: List[float]) -> float:
+ """
+ Calulate the payoff given a set of prices for the underlying.
+
+ Parameters
+ ----------
+ S : Set of prices for the underlying {S_t1, S_t2, ..., S_tn}.
+
+ Returns
+ ----------
+ payoff : Payoff.
+
+ """
+ avg_sum = sum(S) / len(S)
+ if self.option_right == OptionRight.Call:
+ payoff = self._calculate_call(avg_sum)
+ else:
+ payoff = self._calculate_put(avg_sum)
+ return payoff
+
+
+@dataclass(init=False)
+class DiscreteBarrierPayOff(BasePayoff):
+ """
+ Class for calculating the payoff of a discrete barrier European style
+ option.
+
+ Attributes
+ ----------
+ option_right : Right of the option.
+ K : Strike price.
+ B : Barrier price.
+ barrier_updown : Up or down type barrier option.
+ barrier_inout : In or out type barrier option.
+
+ Methods
+ ----------
+ calculate(S)
+ Calulate the payoff given a set of prices for the underlying.
+
+ Examples
+ ----------
+ >>> from utils.payoff import DiscreteBarrierPayOff
+ >>> payoff = DiscreteBarrierPayOff(option_right='Call', K=100, B=90,
+ barrier_updown='Down', barrier_inout='Out')
+ >>> print(payoff.calculate([100., 110., 120.]))
+ 20.0
+ >>> print(payoff.calculate([100., 110., 120., 80., 110.]))
+ 0.0
+
+ """
+ B: float
+ barrier_updown: (str, BarrierUpDown)
+ barrier_inout: (str, BarrierInOut)
+
+ def __init__(self, option_right: (str, OptionRight), K: float, B: float,
+ barrier_updown: (str, BarrierUpDown),
+ barrier_inout: (str, BarrierInOut)):
+ super().__init__(K, option_right)
+ self.B = B
+ self.barrier_updown = barrier_updown
+ self.barrier_inout = barrier_inout
+
+ @property
+ def barrier_updown(self) -> BarrierUpDown:
+ """Up or down type barrier option."""
+ return self._barrier_updown
+
+ @barrier_updown.setter
+ def barrier_updown(self, val: (str, BarrierUpDown)) -> None:
+ """Set either up or down type barrier option."""
+ if isinstance(val, str):
+ if not hasattr(BarrierUpDown, val):
+ or_names = [x.name for x in BarrierUpDown]
+ raise ValueError(f'Invalid str {val}, expected {or_names}')
+ self._barrier_updown = BarrierUpDown[val]
+ elif isinstance(val, BarrierUpDown):
+ self._barrier_updown = val
+ else:
+ raise TypeError(
+ f'Expected str or BarrierUpDown, instead got type {type(val)}!'
+ )
+
+ @property
+ def barrier_inout(self) -> BarrierInOut:
+ """Up or down type barrier option."""
+ return self._barrier_inout
+
+ @barrier_inout.setter
+ def barrier_inout(self, val: (str, BarrierInOut)) -> None:
+ """Set either up or down type barrier option."""
+ if isinstance(val, str):
+ if not hasattr(BarrierInOut, val):
+ or_names = [x.name for x in BarrierInOut]
+ raise ValueError(f'Invalid str {val}, expected {or_names}')
+ self._barrier_inout = BarrierInOut[val]
+ elif isinstance(val, BarrierInOut):
+ self._barrier_inout = val
+ else:
+ raise TypeError(
+ f'Expected str or BarrierInOut, instead got type {type(val)}!'
+ )
+
+ def calculate(self, S: List[float]) -> float:
+ """
+ Calulate the payoff given a set of prices for the underlying.
+
+ Parameters
+ ----------
+ S : Set of prices for the underlying {S_t1, S_t2, ..., S_tn}.
+
+ Returns
+ ----------
+ payoff : Payoff.
+
+ """
+ # Calculate the heavyside
+ if self.barrier_updown == BarrierUpDown.Up:
+ H = [1 if self.B - x > 0 else 0 for x in S]
+ else:
+ H = [1 if x - self.B > 0 else 0 for x in S]
+
+ # Calculate whether it has been activated
+ if self.barrier_inout == BarrierInOut.In:
+ activation = 1 if min(H) == 0 else 0
+ else:
+ activation = min(H)
+
+ # Calculate payoff using final price
+ if self.option_right == OptionRight.Call:
+ payoff = activation * self._calculate_call(S[-1])
+ else:
+ payoff = activation * self._calculate_put(S[-1])
+ return payoff