├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── doc ├── Makefile ├── quant-dsl-definition-and-proof.bbl ├── quant-dsl-definition-and-proof.bib ├── quant-dsl-definition-and-proof.pdf └── quant-dsl-definition-and-proof.tex ├── example_notebook.ipynb ├── quantdsl ├── __init__.py ├── __main__.py ├── application │ ├── __init__.py │ ├── base.py │ ├── call_result_policy.py │ ├── persistence_policy.py │ ├── with_multithreading.py │ ├── with_multithreading_and_python_objects.py │ └── with_pythonobjects.py ├── calculate.py ├── defaults.py ├── domain │ ├── __init__.py │ ├── model │ │ ├── __init__.py │ │ ├── call_dependencies.py │ │ ├── call_dependents.py │ │ ├── call_leafs.py │ │ ├── call_link.py │ │ ├── call_requirement.py │ │ ├── call_result.py │ │ ├── contract_specification.py │ │ ├── contract_valuation.py │ │ ├── market_calibration.py │ │ ├── market_simulation.py │ │ ├── perturbation_dependencies.py │ │ ├── simulated_price.py │ │ └── simulated_price_requirements.py │ └── services │ │ ├── __init__.py │ │ ├── call_links.py │ │ ├── contract_valuations.py │ │ ├── dependency_graphs.py │ │ ├── parser.py │ │ ├── price_processes.py │ │ ├── schwartzsmith_old.py │ │ ├── simulated_prices.py │ │ └── uuids.py ├── exceptions.py ├── infrastructure │ ├── __init__.py │ ├── dependency_graph_subscriber.py │ ├── evaluation_subscriber.py │ ├── event_sourced_repos │ │ ├── __init__.py │ │ ├── call_dependencies_repo.py │ │ ├── call_dependents_repo.py │ │ ├── call_leafs_repo.py │ │ ├── call_link_repo.py │ │ ├── call_requirement_repo.py │ │ ├── call_result_repo.py │ │ ├── contract_specification_repo.py │ │ ├── contract_valuation_repo.py │ │ ├── market_calibration_repo.py │ │ ├── market_simulation_repo.py │ │ ├── perturbation_dependencies_repo.py │ │ ├── simulated_price_dependencies_repo.py │ │ └── simulated_price_repo.py │ └── simulation_subscriber.py ├── interfaces │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ └── main.py │ └── results.py ├── lib │ ├── __init__.py │ ├── american1.py │ ├── european1.py │ ├── option1.py │ ├── powerplant1.py │ ├── powerplant2.py │ ├── storage1.py │ └── storage2.py ├── priceprocess │ ├── __init__.py │ ├── base.py │ ├── blackscholes.py │ ├── common.py │ └── forwardcurve.py ├── semantics.py ├── syntax.py └── tests │ ├── _____test.py │ ├── __init__.py │ ├── lib │ ├── __init__.py │ └── broken_python_syntax.py │ ├── test_application.py │ ├── test_application_with_multithreading.py │ ├── test_calc.py │ ├── test_call_result_policy.py │ ├── test_dependency_graph_subscriber.py │ ├── test_domain_services.py │ ├── test_evaluation_subscriber.py │ ├── test_eventsourced_repos.py │ ├── test_exceptions.py │ ├── test_forward_curve.py │ ├── test_forwardcurve.py │ ├── test_least_squares.py │ ├── test_market_calibration.py │ ├── test_market_simulation.py │ ├── test_parser.py │ ├── test_price_processes.py │ ├── test_quantdsl_lib.py │ ├── test_readme.py │ ├── test_schwartzsmith_old.py │ ├── test_semantics.py │ ├── test_settlements.py │ └── test_simulation_subscriber.py ├── requirements.txt ├── scripts ├── prepare-distribution.py ├── release-distribution.py └── test-released-distribution.py └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = quantdsl 3 | omit = 4 | */python?.?/* 5 | */test_?.py 6 | */tests/* 7 | */quantdsl/lib/* 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.README.py 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | bin/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | eggs/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | 45 | # Rope 46 | .ropeproject 47 | 48 | # Django stuff: 49 | *.log 50 | *.pot 51 | 52 | # Sphinx documentation 53 | docs/_build/ 54 | 55 | # Project. 56 | powerplant.py -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | # Travis whitelists the installable packages, additions can be requested 6 | # https://github.com/travis-ci/apt-package-whitelist 7 | addons: 8 | apt: 9 | packages: &common_packages 10 | - gfortran 11 | - libatlas-dev 12 | - libatlas-base-dev 13 | # Speedup builds, particularly when USE_CHROOT=1 14 | - eatmydata 15 | 16 | cache: 17 | directories: 18 | - $HOME/.cache/pip 19 | 20 | python: 21 | - 2.7 22 | - 3.4 23 | - 3.5 24 | - 3.5-dev 25 | - 3.6 26 | - 3.6-dev 27 | # - 3.7-dev 28 | 29 | install: 30 | - uname -a 31 | - free -m 32 | - df -h 33 | - ulimit -a 34 | - mkdir builds 35 | - pushd builds 36 | # Build into own virtualenv 37 | # We therefore control our own environment, avoid travis' numpy 38 | # 39 | - pip install -U virtualenv 40 | - virtualenv --python=python venv 41 | - source venv/bin/activate 42 | - python -V 43 | - pip install --upgrade pip setuptools 44 | #- pip install nose pytz cython 45 | - popd 46 | #- pip install -U pip 47 | - pip install . 48 | - pip install python-coveralls 49 | 50 | script: 51 | - echo "Python version:" 52 | - python --version 53 | - coverage run -m unittest discover quantdsl.tests -v 54 | 55 | after_success: 56 | - coveralls 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | [BSD 3Clause] 2 | 3 | Copyright (c) 2014, Appropriate Software Foundation 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the {organization} nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | 15 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest 16 | 17 | help: 18 | @echo "Please use \`make ' where is one of" 19 | @echo " html to make standalone HTML files" 20 | @echo " dirhtml to make HTML files named index.html in directories" 21 | @echo " singlehtml to make a single large HTML file" 22 | @echo " pickle to make pickle files" 23 | @echo " json to make JSON files" 24 | @echo " htmlhelp to make HTML files and a HTML help project" 25 | @echo " qthelp to make HTML files and a qthelp project" 26 | @echo " devhelp to make HTML files and a Devhelp project" 27 | @echo " epub to make an epub" 28 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 29 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 30 | @echo " text to make text files" 31 | @echo " man to make manual pages" 32 | @echo " changes to make an overview of all changed/added/deprecated items" 33 | @echo " linkcheck to check all external links for integrity" 34 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 35 | 36 | clean: 37 | -rm -rf $(BUILDDIR)/* 38 | 39 | html: 40 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 41 | @echo 42 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 43 | 44 | dirhtml: 45 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 48 | 49 | singlehtml: 50 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 51 | @echo 52 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 53 | 54 | pickle: 55 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 56 | @echo 57 | @echo "Build finished; now you can process the pickle files." 58 | 59 | json: 60 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 61 | @echo 62 | @echo "Build finished; now you can process the JSON files." 63 | 64 | htmlhelp: 65 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 66 | @echo 67 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 68 | ".hhp project file in $(BUILDDIR)/htmlhelp." 69 | 70 | qthelp: 71 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 72 | @echo 73 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 74 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 75 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Quant.qhcp" 76 | @echo "To view the help file:" 77 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Quant.qhc" 78 | 79 | devhelp: 80 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 81 | @echo 82 | @echo "Build finished." 83 | @echo "To view the help file:" 84 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Quant" 85 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Quant" 86 | @echo "# devhelp" 87 | 88 | epub: 89 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 90 | @echo 91 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 92 | 93 | latex: 94 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 95 | @echo 96 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 97 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 98 | "(use \`make latexpdf' here to do that automatically)." 99 | 100 | latexpdf: 101 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 102 | @echo "Running LaTeX files through pdflatex..." 103 | make -C $(BUILDDIR)/latex all-pdf 104 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 105 | 106 | text: 107 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 108 | @echo 109 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 110 | 111 | man: 112 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 113 | @echo 114 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 115 | 116 | changes: 117 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 118 | @echo 119 | @echo "The overview file is in $(BUILDDIR)/changes." 120 | 121 | linkcheck: 122 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 123 | @echo 124 | @echo "Link check complete; look for any errors in the above output " \ 125 | "or in $(BUILDDIR)/linkcheck/output.txt." 126 | 127 | doctest: 128 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 129 | @echo "Testing of doctests in the sources finished, look at the " \ 130 | "results in $(BUILDDIR)/doctest/output.txt." 131 | -------------------------------------------------------------------------------- /doc/quant-dsl-definition-and-proof.bbl: -------------------------------------------------------------------------------- 1 | \begin{thebibliography}{1} 2 | 3 | \bibitem{Longstaff} 4 | Francis~A. Longstaff and Eduardo~S. Schwartz. 5 | \newblock Valuing american options by simulation: A simple least-squares 6 | approach. 7 | \newblock {\em Review of Financial Studies}, pages 113--147, 2001. 8 | 9 | \bibitem{Quant} 10 | Appropriate~Software Foundation. 11 | \newblock Quant. 12 | \newblock {\em Python Package Index}, (http://pypi.python.org/pypi/quant), 13 | 2011. 14 | 15 | \bibitem{SciPy} 16 | The~SciPy Community. 17 | \newblock Scipy. 18 | \newblock {\em SciPy Website}, (http://www.scipy.org), 2011. 19 | 20 | \bibitem{DomainModel} 21 | Appropriate~Software Foundation. 22 | \newblock Domain model. 23 | \newblock {\em Python Package Index}, 24 | (http://pypi.python.org/pypi/domainmodel), 2011. 25 | 26 | \end{thebibliography} 27 | -------------------------------------------------------------------------------- /doc/quant-dsl-definition-and-proof.bib: -------------------------------------------------------------------------------- 1 | @ARTICLE{Longstaff, 2 | author = {Francis A. Longstaff and Eduardo S. Schwartz}, 3 | title = {Valuing American options by simulation: A simple least-squares approach}, 4 | journal = {Review of Financial Studies}, 5 | year = {2001}, 6 | pages = {113--147}, 7 | } 8 | 9 | @ARTICLE{Quant, 10 | author = {Appropriate Software Foundation}, 11 | title = {Quant}, 12 | journal = {Python Package Index}, 13 | year = {2011}, 14 | number = {http://pypi.python.org/pypi/quant} 15 | } 16 | 17 | @ARTICLE{DomainModel, 18 | author = {Appropriate Software Foundation}, 19 | title = {Domain Model}, 20 | journal = {Python Package Index}, 21 | year = {2011}, 22 | number = {http://pypi.python.org/pypi/domainmodel} 23 | } 24 | 25 | @ARTICLE{SciPy, 26 | author = {The SciPy Community}, 27 | title = {SciPy}, 28 | journal = {SciPy Website}, 29 | year = {2011}, 30 | number = {http://www.scipy.org} 31 | } 32 | -------------------------------------------------------------------------------- /doc/quant-dsl-definition-and-proof.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/doc/quant-dsl-definition-and-proof.pdf -------------------------------------------------------------------------------- /quantdsl/__init__.py: -------------------------------------------------------------------------------- 1 | from quantdsl.defaults import DEFAULT_MAX_DEPENDENCY_GRAPH_SIZE, DEFAULT_PATH_COUNT, DEFAULT_PERTURBATION_FACTOR, \ 2 | DEFAULT_PRICE_PROCESS_NAME, DEFAULT_INTEREST_RATE 3 | 4 | __version__ = '1.4.0' 5 | 6 | 7 | def calc(source_code, observation_date=None, interest_rate=DEFAULT_INTEREST_RATE, path_count=DEFAULT_PATH_COUNT, 8 | perturbation_factor=DEFAULT_PERTURBATION_FACTOR, price_process=None, periodisation=None, 9 | dsl_classes=None, max_dependency_graph_size=DEFAULT_MAX_DEPENDENCY_GRAPH_SIZE, 10 | timeout=None, is_double_sided_deltas=True, verbose=False): 11 | import quantdsl.calculate 12 | 13 | return quantdsl.calculate.calc(source_code, 14 | observation_date=observation_date, 15 | interest_rate=interest_rate, 16 | path_count=path_count, 17 | perturbation_factor=perturbation_factor, 18 | price_process=price_process, 19 | periodisation=periodisation, 20 | dsl_classes=dsl_classes, 21 | max_dependency_graph_size=max_dependency_graph_size, 22 | timeout=timeout, 23 | is_double_sided_deltas=is_double_sided_deltas, 24 | verbose=verbose 25 | ) 26 | 27 | 28 | 29 | 30 | 31 | 32 | # Todo: Write test for import module that doesn't exist (currently fails with AttributeError cos spec is None). 33 | # Todo: Support things like "third Wednesday of contract month" e.g. for settlement of futures. 34 | # Todo: Multiprocessing of repeat simulations and valuations, with combination of results, to increase accuracy. 35 | # Todo: Something to show estimated memory usage. 36 | # Todo: Finish off support for yearly periodisation, and also hourly. 37 | # Todo: Command line support, so QuantDSL can be written and executed without knowing Python. 38 | # Todo: Price process as DSL (included in module directly or by import, module named as arg to calc()). 39 | # Todo: Price process as DSL, either calibration params, or args for calibration with e.g. data from quandl). 40 | # Todo: More price processes (jumps, heston). 41 | # Todo: Better report object: separate out the delta hedging. 42 | # Todo: Better interface objects: separate out the print() statements. 43 | # Todo: Better deltas (dx sometimes uses average of prices in month, when not all prices may be used in expression, 44 | # so identify which are involved and just use those). 45 | # Todo: Tidy up how args are passed into evaluate(), it seems correct, but also a bit ad hoc. 46 | # Todo: Support names in expressions being resolved by evaluation args (e.g. like 'observation_date' but more general). 47 | # Todo: StockMarket element. 48 | # Todo: Make price process create calibration params from market observations, as well as consume the calibration 49 | # parameters. 50 | # Todo: Change all names from lower camel case to underscore separated style. 51 | # Todo: Develop multi-factor PriceProcess model (e.g. Schwartz-Smith)? 52 | # Todo: Separate more clearly the syntax parsing (the Parser methods) from the semantic model the DSL objects. 53 | # Todo: Separate more clearly a general function language implementation, which could be extended with any set of 54 | # primitive elements. 55 | # Todo: Use function arg annotation to declare types of DSL function args (will only work with Python 3). 56 | # Todo: Develop closures, function defs within function defs may help to reduce call argument complexity. 57 | # Todo: Think about other possibility of supporting another syntax? Perhaps there is a better syntax than the Python 58 | # based syntax? 59 | # Todo: Develop natural language "skin" for Quant DSL expressions (something like how Gherkin syntax maps to 60 | # functions?)? 61 | # Todo: Support list comprehensions, for things like a strip of options? 62 | # Todo: Develop a GUI that shows the graph being evaluated, allowing results to be examined, allows models to be 63 | # developed. Look at the "language workbench" ideas from Martin Fowler (environment which shows example results, 64 | # with editable code reachable from the results, and processing built-in)? 65 | # Todo: Better stats available on number of call requirements, number of leaves in dependency graph, depth of graph? 66 | # Todo: Prediction of cost of evaluating an expression, cost of network data requests, could calibrate by running 67 | # sample stubbed expressions (perhaps complicated for LongstaffSchwartz cos the LeastSqaures routine is run 68 | # different numbers of times). 69 | # Todo: Support plotting. 70 | # Todo: Clean up the str, repr, pprint stuff? 71 | # Todo: Raise Quant DSL-specific type mismatch errors at run time (ie e.g. handle situation where datetime and 72 | # string can't be added). 73 | # Todo: Anyway, identify when type mismatches will occur - can't multiply a date by a number, can't add a date to a 74 | # date or to a number, can't add a number to a timedelta. Etc? 75 | # Todo: (Long one) Go through all ways of writing broken DSL source code, and make sure there are sensible errors. 76 | # Todo: Figure out behaviour for observation_date > any fixing date, currently leads to a complex numbers (square 77 | # root of negative time delta). 78 | # Todo: Think/talk about regressing on correlated brownian motions, rather than uncorrelated ones - is there 79 | # actually a difference? If no difference, there is no need to keep the uncorrelated Brownian motions. 80 | # Todo: Review the test coverage of the code. 81 | # Todo: Review the separation of concerns between the various test cases. 82 | # Todo: Move these todos to an issue tracker. 83 | 84 | # Note on how to install matplotlib in virtualenv: 85 | # http://www.stevenmaude.co.uk/2013/09/installing-matplotlib-in-virtualenv.html 86 | -------------------------------------------------------------------------------- /quantdsl/__main__.py: -------------------------------------------------------------------------------- 1 | """Main entry point""" 2 | 3 | import sys 4 | if sys.argv[0].endswith("__main__.py"): 5 | sys.argv[0] = "python -m quantdsl" 6 | 7 | __unittest = True 8 | 9 | from quantdsl.interfaces.cli.main import main, TestProgram, USAGE_AS_MAIN 10 | TestProgram.USAGE = USAGE_AS_MAIN 11 | 12 | main(module=None) 13 | -------------------------------------------------------------------------------- /quantdsl/application/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/application/__init__.py -------------------------------------------------------------------------------- /quantdsl/application/call_result_policy.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.events import subscribe, unsubscribe 2 | 3 | from quantdsl.domain.model.call_dependencies import CallDependencies 4 | from quantdsl.domain.model.call_dependents import CallDependents 5 | from quantdsl.domain.model.call_result import CallResult, make_call_result_id 6 | from quantdsl.infrastructure.event_sourced_repos.call_result_repo import CallResultRepo 7 | 8 | 9 | class CallResultPolicy(object): 10 | def __init__(self, call_result_repo, call_evaluation_queue=None): 11 | self.call_result_repo = call_result_repo 12 | self.call_evaluation_queue = call_evaluation_queue 13 | self.result = {} 14 | self.dependents = {} 15 | self.dependencies = {} 16 | self.outstanding_dependents = {} 17 | self.outstanding_dependencies = {} 18 | 19 | subscribe(self.is_call_dependencies_created, self.cache_dependencies) 20 | subscribe(self.is_call_dependents_created, self.cache_dependents) 21 | subscribe(self.is_call_result_created, self.cache_result) 22 | subscribe(self.is_call_result_discarded, self.purge_result) 23 | 24 | def close(self): 25 | unsubscribe(self.is_call_dependencies_created, self.cache_dependencies) 26 | unsubscribe(self.is_call_dependents_created, self.cache_dependents) 27 | unsubscribe(self.is_call_result_created, self.cache_result) 28 | unsubscribe(self.is_call_result_discarded, self.purge_result) 29 | 30 | def is_call_dependencies_created(self, event): 31 | return isinstance(event, CallDependencies.Created) 32 | 33 | def is_call_dependents_created(self, event): 34 | return isinstance(event, CallDependents.Created) 35 | 36 | def is_call_result_created(self, event): 37 | return isinstance(event, CallResult.Created) 38 | 39 | def is_call_result_discarded(self, event): 40 | return isinstance(event, CallResult.Discarded) 41 | 42 | def cache_dependencies(self, event): 43 | assert isinstance(event, CallDependencies.Created) 44 | call_result_id = event.entity_id 45 | self.dependencies[call_result_id] = event.dependencies[:] 46 | 47 | # Count one outstanding dependency for each dependency of the call. 48 | # - minus 1, so that pop() raises at the right time, see below 49 | if self.call_evaluation_queue: 50 | self.outstanding_dependencies[call_result_id] = [None] * (len(event.dependencies) - 1) 51 | 52 | def cache_dependents(self, event): 53 | assert isinstance(event, CallDependents.Created) 54 | call_result_id = event.entity_id 55 | self.dependents[call_result_id] = event.dependents[:] 56 | 57 | # Count one outstanding dependent for each dependent of the call. 58 | # - minus 1, so that pop() raises at the right time, see below 59 | self.outstanding_dependents[call_result_id] = [None] * (len(event.dependents) - 1) 60 | 61 | def cache_result(self, event): 62 | assert isinstance(event, CallResult.Created) 63 | 64 | # Remember the call result entity. 65 | this_result_id = event.entity_id 66 | call_result = CallResult.mutator(event=event) 67 | self.result[this_result_id] = call_result 68 | if isinstance(self.call_result_repo, dict): 69 | self.call_result_repo[this_result_id] = call_result 70 | 71 | # Decrement outstanding dependents for each dependency of this result. 72 | for dependency_id in self.dependencies.get(event.call_id, ()): 73 | try: 74 | self.outstanding_dependents[dependency_id].pop() 75 | except IndexError: 76 | # Discard the result when there are no longer any outstanding dependents. 77 | dependent_result_id = make_call_result_id(event.contract_valuation_id, dependency_id) 78 | self.result[dependent_result_id].discard() 79 | 80 | # Remove one outstanding dependency for each dependent of this result. 81 | if self.call_evaluation_queue: 82 | for dependent_id in self.dependents.get(event.call_id, ()): 83 | try: 84 | self.outstanding_dependencies[dependent_id].pop() 85 | except IndexError: 86 | # Queue the call if there are no more outstanding results. 87 | job = (event.contract_specification_id, event.contract_valuation_id, dependent_id) 88 | self.call_evaluation_queue.put(job) 89 | 90 | def purge_result(self, event): 91 | assert isinstance(event, CallResult.Discarded) 92 | # Remove from the local results dict. 93 | del (self.result[event.entity_id]) 94 | 95 | # Todo: Push this onto the repository object (after refactoring them to all be cached). 96 | # Remove from the call result repo. 97 | if isinstance(self.call_result_repo, dict): 98 | del (self.call_result_repo[event.entity_id]) 99 | elif isinstance(self.call_result_repo, CallResultRepo): 100 | del (self.call_result_repo._cache[event.entity_id]) 101 | -------------------------------------------------------------------------------- /quantdsl/application/persistence_policy.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.persistence_subscriber import PersistenceSubscriber 2 | 3 | from quantdsl.domain.model.call_result import CallResult 4 | from quantdsl.domain.model.simulated_price import SimulatedPrice 5 | 6 | 7 | class PersistencePolicy(PersistenceSubscriber): 8 | @staticmethod 9 | def is_domain_event(event): 10 | return PersistenceSubscriber.is_domain_event(event) and \ 11 | not isinstance(event, ( 12 | CallResult.Created, 13 | CallResult.Discarded, 14 | SimulatedPrice.Created, 15 | )) -------------------------------------------------------------------------------- /quantdsl/application/with_multithreading.py: -------------------------------------------------------------------------------- 1 | from threading import Event, Thread 2 | from time import sleep 3 | 4 | import six.moves.queue as queue 5 | 6 | from quantdsl.application.base import QuantDslApplication 7 | from quantdsl.domain.model.contract_valuation import ContractValuation 8 | from quantdsl.exceptions import TimeoutError, DslCompareArgsError, DslBinOpArgsError, DslIfTestExpressionError 9 | 10 | 11 | class ServiceExit(Exception): 12 | pass 13 | 14 | 15 | class QuantDslApplicationWithMultithreading(QuantDslApplication): 16 | def __init__(self, num_threads=4, *args, **kwargs): 17 | super(QuantDslApplicationWithMultithreading, self).__init__(call_evaluation_queue=queue.Queue(), 18 | *args, **kwargs) 19 | self.num_threads = num_threads 20 | self.has_thread_errored = Event() 21 | self.thread_exception = None 22 | self.threads = [] 23 | 24 | # Start evaluation worker threads. 25 | for _ in range(self.num_threads): 26 | t = Thread(target=self.protected_loop_on_evaluation_queue) 27 | t.setDaemon(True) 28 | t.daemon = True 29 | t.start() 30 | self.threads.append(t) 31 | 32 | def protected_loop_on_evaluation_queue(self): 33 | try: 34 | self.loop_on_evaluation_queue() 35 | except Exception as e: 36 | if not self.has_thread_errored.is_set(): 37 | self.thread_exception = e 38 | self.has_thread_errored.set() 39 | if not isinstance(e, (TimeoutError, DslCompareArgsError, DslBinOpArgsError, DslIfTestExpressionError)): 40 | raise 41 | 42 | def get_result(self, contract_valuation): 43 | assert isinstance(contract_valuation, ContractValuation) 44 | 45 | # Todo: Subscribe to call result, with handler that sets an event. Then wait for the 46 | # event with a timeout, in a while True loop, checking for interruptions and timeouts 47 | # like in Calculate.calculate(). 48 | while True: 49 | try: 50 | return super(QuantDslApplicationWithMultithreading, self).get_result(contract_valuation) 51 | except KeyError: 52 | sleep(0.1) 53 | self.check_has_thread_errored() 54 | 55 | def check_has_thread_errored(self): 56 | if self.has_thread_errored.is_set(): 57 | raise self.thread_exception 58 | -------------------------------------------------------------------------------- /quantdsl/application/with_multithreading_and_python_objects.py: -------------------------------------------------------------------------------- 1 | from quantdsl.application.with_multithreading import QuantDslApplicationWithMultithreading 2 | from quantdsl.application.with_pythonobjects import QuantDslApplicationWithPythonObjects 3 | 4 | 5 | class QuantDslApplicationWithMultithreadingAndPythonObjects(QuantDslApplicationWithMultithreading, 6 | QuantDslApplicationWithPythonObjects): 7 | 8 | pass -------------------------------------------------------------------------------- /quantdsl/application/with_pythonobjects.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.application.with_pythonobjects import EventSourcingWithPythonObjects 2 | from quantdsl.application.base import QuantDslApplication 3 | 4 | 5 | class QuantDslApplicationWithPythonObjects(EventSourcingWithPythonObjects, QuantDslApplication): 6 | pass 7 | -------------------------------------------------------------------------------- /quantdsl/defaults.py: -------------------------------------------------------------------------------- 1 | DEFAULT_CONFIDENCE_INTERVAL = 95 2 | DEFAULT_MAX_DEPENDENCY_GRAPH_SIZE = 10000 3 | DEFAULT_PATH_COUNT = 20000 4 | DEFAULT_PERTURBATION_FACTOR = 0.01 5 | DEFAULT_PRICE_PROCESS_NAME = 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess' 6 | DEFAULT_INTEREST_RATE = 0.0 -------------------------------------------------------------------------------- /quantdsl/domain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/domain/__init__.py -------------------------------------------------------------------------------- /quantdsl/domain/model/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/domain/model/__init__.py -------------------------------------------------------------------------------- /quantdsl/domain/model/call_dependencies.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class CallDependencies(EventSourcedEntity): 6 | """ 7 | A call dependency is a call that must be evaluated before this call can be evaluated. 8 | 9 | The number of dependencies will be the number of different function calls made 10 | by the function that is called by this call. 11 | """ 12 | 13 | class Created(EventSourcedEntity.Created): 14 | @property 15 | def dependencies(self): 16 | return self.__dict__['dependencies'] 17 | 18 | class Discarded(EventSourcedEntity.Discarded): 19 | pass 20 | 21 | def __init__(self, dependencies, **kwargs): 22 | super(CallDependencies, self).__init__(**kwargs) 23 | self._dependencies = dependencies 24 | 25 | def __getitem__(self, item): 26 | return self._dependencies.__getitem__(item) 27 | 28 | @property 29 | def dependencies(self): 30 | return self._dependencies 31 | 32 | 33 | def register_call_dependencies(call_id, dependencies): 34 | "Registers things needed by this call." 35 | created_event = CallDependencies.Created(entity_id=call_id, dependencies=dependencies) 36 | call_dependencies = CallDependencies.mutator(event=created_event) 37 | # print("Number of call dependencies:", len(dependencies)) 38 | 39 | publish(created_event) 40 | return call_dependencies 41 | 42 | 43 | class CallDependenciesRepository(EntityRepository): 44 | pass 45 | -------------------------------------------------------------------------------- /quantdsl/domain/model/call_dependents.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class CallDependents(EventSourcedEntity): 6 | """ 7 | Call dependents are the calls that are waiting for this call to be evaluated. 8 | 9 | The number of dependents will be the number of reuses of this call from the call 10 | cache. Looking at the call dependents therefore gives a good idea of whether 11 | recombination in a lattice is working properly. 12 | """ 13 | 14 | class Created(EventSourcedEntity.Created): 15 | pass 16 | 17 | class Discarded(EventSourcedEntity.Discarded): 18 | pass 19 | 20 | def __init__(self, dependents, **kwargs): 21 | super(CallDependents, self).__init__(**kwargs) 22 | self._dependents = dependents 23 | 24 | def __getitem__(self, item): 25 | return self._dependents.__getitem__(item) 26 | 27 | @property 28 | def dependents(self): 29 | return self._dependents 30 | 31 | 32 | def register_call_dependents(call_id, dependents): 33 | created_event = CallDependents.Created(entity_id=call_id, dependents=dependents) 34 | call_dependents = CallDependents.mutator(event=created_event) 35 | # print("Number of call dependents:", len(dependents)) 36 | publish(created_event) 37 | return call_dependents 38 | 39 | 40 | class CallDependentsRepository(EntityRepository): 41 | pass 42 | -------------------------------------------------------------------------------- /quantdsl/domain/model/call_leafs.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class CallLeafs(EventSourcedEntity): 6 | 7 | class Created(EventSourcedEntity.Created): 8 | pass 9 | 10 | class Discarded(EventSourcedEntity.Discarded): 11 | pass 12 | 13 | def __init__(self, leaf_ids, **kwargs): 14 | super(CallLeafs, self).__init__(**kwargs) 15 | self._leaf_ids = leaf_ids 16 | 17 | @property 18 | def leaf_ids(self): 19 | return self._leaf_ids 20 | 21 | 22 | def register_call_leafs(contract_specification_id, leaf_ids): 23 | created_event = CallLeafs.Created(entity_id=contract_specification_id, leaf_ids=leaf_ids) 24 | call_leafs = CallLeafs.mutator(event=created_event) 25 | publish(created_event) 26 | return call_leafs 27 | 28 | 29 | class CallLeafsRepository(EntityRepository): 30 | pass 31 | -------------------------------------------------------------------------------- /quantdsl/domain/model/call_link.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class CallLink(EventSourcedEntity): 6 | 7 | class Created(EventSourcedEntity.Created): 8 | pass 9 | 10 | class Discarded(EventSourcedEntity.Discarded): 11 | pass 12 | 13 | def __init__(self, call_id, **kwargs): 14 | super(CallLink, self).__init__(**kwargs) 15 | self._call_id = call_id 16 | 17 | @property 18 | def call_id(self): 19 | return self._call_id 20 | 21 | 22 | def register_call_link(link_id, call_id): 23 | created_event = CallLink.Created(entity_id=link_id, call_id=call_id) 24 | call_link = CallLink.mutator(event=created_event) 25 | publish(created_event) 26 | return call_link 27 | 28 | 29 | class CallLinkRepository(EntityRepository): 30 | pass 31 | -------------------------------------------------------------------------------- /quantdsl/domain/model/call_requirement.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | import datetime 4 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 5 | from eventsourcing.domain.model.events import publish 6 | 7 | StubbedCall = namedtuple('StubbedCall', ['call_id', 'dsl_expr', 'present_time', 'requirements']) 8 | 9 | 10 | class CallRequirement(EventSourcedEntity): 11 | 12 | class Created(EventSourcedEntity.Created): 13 | pass 14 | 15 | class Discarded(EventSourcedEntity.Discarded): 16 | pass 17 | 18 | def __init__(self, dsl_source, present_time, contract_specification_id, cost, **kwargs): 19 | super(CallRequirement, self).__init__(**kwargs) 20 | self._dsl_source = dsl_source 21 | self._present_time = present_time 22 | self._contract_specification_id = contract_specification_id 23 | self._dsl_expr = None 24 | self._cost = cost 25 | 26 | @property 27 | def dsl_source(self): 28 | return self._dsl_source 29 | 30 | @property 31 | def present_time(self): 32 | return self._present_time 33 | 34 | @property 35 | def contract_specification_id(self): 36 | return self._contract_specification_id 37 | 38 | @property 39 | def cost(self): 40 | return self._cost 41 | 42 | 43 | def register_call_requirement(call_id, dsl_source, present_time, contract_specification_id, cost): 44 | assert isinstance(present_time, (datetime.date, type(None))), present_time 45 | created_event = CallRequirement.Created( 46 | entity_id=call_id, 47 | dsl_source=dsl_source, 48 | present_time=present_time, 49 | contract_specification_id=contract_specification_id, 50 | cost=cost, 51 | ) 52 | call_requirement = CallRequirement.mutator(event=created_event) 53 | publish(created_event) 54 | return call_requirement 55 | 56 | 57 | class CallRequirementRepository(EntityRepository): 58 | pass 59 | -------------------------------------------------------------------------------- /quantdsl/domain/model/call_result.py: -------------------------------------------------------------------------------- 1 | import scipy 2 | import six 3 | from eventsourcing.domain.model.entity import EntityRepository, EventSourcedEntity 4 | from eventsourcing.domain.model.events import publish, QualnameABCMeta 5 | 6 | 7 | class CallResult(EventSourcedEntity): 8 | class Created(EventSourcedEntity.Created): 9 | @property 10 | def call_id(self): 11 | return self.__dict__['call_id'] 12 | 13 | @property 14 | def contract_valuation_id(self): 15 | return self.__dict__['contract_valuation_id'] 16 | 17 | @property 18 | def contract_specification_id(self): 19 | return self.__dict__['contract_specification_id'] 20 | 21 | class Discarded(EventSourcedEntity.Discarded): 22 | pass 23 | 24 | def __init__(self, result_value, perturbed_values, contract_valuation_id, call_id, contract_specification_id, 25 | involved_market_names, **kwargs): 26 | super(CallResult, self).__init__(**kwargs) 27 | self._result_value = result_value 28 | self._perturbed_values = perturbed_values 29 | self._contract_valuation_id = contract_valuation_id 30 | self._call_id = call_id 31 | self._contract_specification_id = contract_specification_id 32 | self._involved_market_names = involved_market_names 33 | 34 | @property 35 | def result_value(self): 36 | return self._result_value 37 | 38 | @property 39 | def perturbed_values(self): 40 | return self._perturbed_values 41 | 42 | @property 43 | def involved_market_names(self): 44 | return self._involved_market_names 45 | 46 | 47 | def register_call_result(call_id, result_value, perturbed_values, contract_valuation_id, contract_specification_id, 48 | involved_market_names): 49 | call_result_id = make_call_result_id(contract_valuation_id, call_id) 50 | created_event = CallResult.Created(entity_id=call_result_id, 51 | result_value=result_value, 52 | perturbed_values=perturbed_values, 53 | contract_valuation_id=contract_valuation_id, 54 | call_id=call_id, 55 | # Todo: Don't persist this, get the contract valuation object when needed. 56 | # Todo: Also save the list of fixing dates separately (if needs to be saved). 57 | contract_specification_id=contract_specification_id, 58 | involved_market_names=involved_market_names 59 | ) 60 | call_result = CallResult.mutator(event=created_event) 61 | 62 | publish(created_event) 63 | return call_result 64 | 65 | 66 | def make_call_result_id(contract_valuation_id, call_id): 67 | assert contract_valuation_id, contract_valuation_id 68 | assert call_id, call_id 69 | return contract_valuation_id + call_id 70 | 71 | 72 | class CallResultRepository(EntityRepository): 73 | pass 74 | 75 | 76 | class ResultValueComputed(six.with_metaclass(QualnameABCMeta)): 77 | """Event published when a result value is computed. 78 | 79 | Used to track progress of computation more smoothly than 80 | can be achieved by listening for CallResult.Created events. 81 | 82 | (This is not a persisted domain event.) 83 | """ 84 | def __init__(self, cost): 85 | self.cost = cost 86 | -------------------------------------------------------------------------------- /quantdsl/domain/model/contract_specification.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | from quantdsl.domain.services.uuids import create_uuid4 4 | 5 | 6 | class ContractSpecification(EventSourcedEntity): 7 | 8 | class Created(EventSourcedEntity.Created): 9 | pass 10 | 11 | class Discarded(EventSourcedEntity.Discarded): 12 | pass 13 | 14 | def __init__(self, source_code, observation_date, **kwargs): 15 | super(ContractSpecification, self).__init__(**kwargs) 16 | self._source_code = source_code 17 | self._observation_date = observation_date 18 | 19 | @property 20 | def source_code(self): 21 | return self._source_code 22 | 23 | @property 24 | def observation_date(self): 25 | return self._observation_date 26 | 27 | 28 | def register_contract_specification(source_code, observation_date=None): 29 | created_event = ContractSpecification.Created( 30 | entity_id=create_uuid4(), 31 | source_code=source_code, 32 | observation_date=observation_date, 33 | ) 34 | contract_specification = ContractSpecification.mutator(event=created_event) 35 | publish(created_event) 36 | return contract_specification 37 | 38 | 39 | # Todo: Rename market_name to commodity_name? 40 | 41 | 42 | class ContractSpecificationRepository(EntityRepository): 43 | pass 44 | -------------------------------------------------------------------------------- /quantdsl/domain/model/contract_valuation.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | from quantdsl.domain.services.uuids import create_uuid4 4 | 5 | 6 | class ContractValuation(EventSourcedEntity): 7 | 8 | class Created(EventSourcedEntity.Created): 9 | pass 10 | 11 | class Discarded(EventSourcedEntity.Discarded): 12 | pass 13 | 14 | def __init__(self, market_simulation_id, contract_specification_id, periodisation, is_double_sided_deltas, 15 | **kwargs): 16 | super(ContractValuation, self).__init__(**kwargs) 17 | self._market_simulation_id = market_simulation_id 18 | self._contract_specification_id = contract_specification_id 19 | self._periodisation = periodisation 20 | self._is_double_sided_deltas = is_double_sided_deltas 21 | 22 | @property 23 | def market_simulation_id(self): 24 | return self._market_simulation_id 25 | 26 | @property 27 | def contract_specification_id(self): 28 | return self._contract_specification_id 29 | 30 | @property 31 | def periodisation(self): 32 | return self._periodisation 33 | 34 | @property 35 | def is_double_sided_deltas(self): 36 | return self._is_double_sided_deltas 37 | 38 | 39 | def start_contract_valuation(contract_specification_id, market_simulation_id, periodisation, is_double_sided_deltas): 40 | contract_valuation_id = create_contract_valuation_id() 41 | contract_valuation_created = ContractValuation.Created( 42 | entity_id=contract_valuation_id, 43 | market_simulation_id=market_simulation_id, 44 | contract_specification_id=contract_specification_id, 45 | periodisation=periodisation, 46 | is_double_sided_deltas=is_double_sided_deltas, 47 | ) 48 | contract_valuation = ContractValuation.mutator(event=contract_valuation_created) 49 | publish(contract_valuation_created) 50 | return contract_valuation 51 | 52 | 53 | def create_contract_valuation_id(): 54 | return create_uuid4() 55 | 56 | 57 | class ContractValuationRepository(EntityRepository): 58 | pass 59 | -------------------------------------------------------------------------------- /quantdsl/domain/model/market_calibration.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | from quantdsl.domain.services.uuids import create_uuid4 4 | 5 | 6 | class MarketCalibration(EventSourcedEntity): 7 | 8 | class Created(EventSourcedEntity.Created): 9 | pass 10 | 11 | class Discarded(EventSourcedEntity.Discarded): 12 | pass 13 | 14 | def __init__(self, price_process_name, calibration_params, **kwargs): 15 | super(MarketCalibration, self).__init__(**kwargs) 16 | self._price_process_name = price_process_name 17 | self._calibration_params = calibration_params 18 | 19 | @property 20 | def price_process_name(self): 21 | return self._price_process_name 22 | 23 | @property 24 | def calibration_params(self): 25 | return self._calibration_params 26 | 27 | 28 | def register_market_calibration(price_process_name, calibration_params): 29 | created_event = MarketCalibration.Created(entity_id=create_uuid4(), 30 | price_process_name=price_process_name, 31 | calibration_params=calibration_params) 32 | call_result = MarketCalibration.mutator(event=created_event) 33 | publish(created_event) 34 | return call_result 35 | 36 | 37 | # def compute_market_calibration_params(price_process_name, historical_data): 38 | # # Todo: Generate model params from historical price data. 39 | # return {} 40 | 41 | 42 | class MarketCalibrationRepository(EntityRepository): 43 | pass 44 | -------------------------------------------------------------------------------- /quantdsl/domain/model/market_simulation.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EntityRepository, EventSourcedEntity 2 | from eventsourcing.domain.model.events import publish 3 | from quantdsl.domain.services.uuids import create_uuid4 4 | 5 | 6 | class MarketSimulation(EventSourcedEntity): 7 | class Created(EventSourcedEntity.Created): 8 | pass 9 | 10 | class Discarded(EventSourcedEntity.Discarded): 11 | pass 12 | 13 | def __init__(self, market_calibration_id, requirements, observation_date, path_count, perturbation_factor, 14 | interest_rate, **kwargs): 15 | super(MarketSimulation, self).__init__(**kwargs) 16 | self._market_calibration_id = market_calibration_id 17 | self._requirements = requirements 18 | self._observation_date = observation_date 19 | self._path_count = path_count 20 | self._perturbation_factor = perturbation_factor 21 | self._interest_rate = interest_rate 22 | 23 | @property 24 | def market_calibration_id(self): 25 | return self._market_calibration_id 26 | 27 | @property 28 | def observation_date(self): 29 | return self._observation_date 30 | 31 | @property 32 | def requirements(self): 33 | return self._requirements 34 | 35 | @property 36 | def path_count(self): 37 | return self._path_count 38 | 39 | @property 40 | def interest_rate(self): 41 | return self._interest_rate 42 | 43 | @property 44 | def perturbation_factor(self): 45 | return self._perturbation_factor 46 | 47 | 48 | def register_market_simulation(market_calibration_id, observation_date, requirements, path_count, interest_rate, 49 | perturbation_factor): 50 | 51 | # Todo: Eliminate duplicate requirements before publishing the event. 52 | 53 | assert isinstance(requirements, list), type(requirements) 54 | created_event = MarketSimulation.Created(entity_id=create_uuid4(), 55 | market_calibration_id=market_calibration_id, 56 | requirements=requirements, 57 | observation_date=observation_date, 58 | path_count=path_count, 59 | interest_rate=interest_rate, 60 | perturbation_factor=perturbation_factor 61 | ) 62 | call_result = MarketSimulation.mutator(event=created_event) 63 | publish(created_event) 64 | return call_result 65 | 66 | 67 | class MarketSimulationRepository(EntityRepository): 68 | pass 69 | -------------------------------------------------------------------------------- /quantdsl/domain/model/perturbation_dependencies.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class PerturbationDependencies(EventSourcedEntity): 6 | """ 7 | Perturbation requirements are the names of the perturbed values required by call requirement with this entity ID. 8 | """ 9 | 10 | class Created(EventSourcedEntity.Created): 11 | pass 12 | 13 | class Discarded(EventSourcedEntity.Discarded): 14 | pass 15 | 16 | def __init__(self, dependencies, **kwargs): 17 | super(PerturbationDependencies, self).__init__(**kwargs) 18 | self._dependencies = dependencies 19 | 20 | @property 21 | def dependencies(self): 22 | return self._dependencies 23 | 24 | 25 | def register_perturbation_dependencies(call_requirement_id, dependencies): 26 | created_event = PerturbationDependencies.Created(entity_id=call_requirement_id, dependencies=dependencies) 27 | perturbation_dependencies = PerturbationDependencies.mutator(event=created_event) 28 | publish(created_event) 29 | return perturbation_dependencies 30 | 31 | 32 | class PerturbationDependenciesRepository(EntityRepository): 33 | pass 34 | -------------------------------------------------------------------------------- /quantdsl/domain/model/simulated_price.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import six 3 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 4 | from eventsourcing.domain.model.events import publish 5 | from quantdsl.priceprocess.base import datetime_from_date 6 | 7 | 8 | class SimulatedPrice(EventSourcedEntity): 9 | 10 | class Created(EventSourcedEntity.Created): 11 | pass 12 | 13 | class Discarded(EventSourcedEntity.Discarded): 14 | pass 15 | 16 | def __init__(self, value, **kwargs): 17 | super(SimulatedPrice, self).__init__(**kwargs) 18 | self._value = value 19 | 20 | @property 21 | def value(self): 22 | return self._value 23 | 24 | 25 | def register_simulated_price(market_simulation_id, market_name, fixing_date, delivery_date, price_value): 26 | simulated_price_id = make_simulated_price_id(market_simulation_id, market_name, fixing_date, delivery_date) 27 | created_event = SimulatedPrice.Created(entity_id=simulated_price_id, value=price_value) 28 | simulated_price = SimulatedPrice.mutator(event=created_event) 29 | publish(created_event) 30 | return simulated_price 31 | 32 | 33 | def make_simulated_price_id(simulation_id, market_name, fixing_date, delivery_date): 34 | assert isinstance(market_name, six.string_types), market_name 35 | assert isinstance(fixing_date, (datetime.datetime, datetime.date)), (fixing_date, type(fixing_date)) 36 | assert isinstance(delivery_date, (datetime.datetime, datetime.date)), (delivery_date, type(delivery_date)) 37 | fixing_date = datetime_from_date(fixing_date) 38 | delivery_date = datetime_from_date(delivery_date) 39 | price_id = ("PriceId(simulation_id='{}' commodity_name='{}' fixing_date='{}', delivery_date='{}')" 40 | "".format(simulation_id, market_name, fixing_date, delivery_date)) 41 | return price_id 42 | 43 | 44 | class SimulatedPriceRepository(EntityRepository): 45 | pass 46 | 47 | 48 | -------------------------------------------------------------------------------- /quantdsl/domain/model/simulated_price_requirements.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository 2 | from eventsourcing.domain.model.events import publish 3 | 4 | 5 | class SimulatedPriceRequirements(EventSourcedEntity): 6 | """ 7 | Simulated price requirements are the IDs of the simulated prices required by the call requirement with the same ID. 8 | """ 9 | 10 | class Created(EventSourcedEntity.Created): 11 | pass 12 | 13 | class Discarded(EventSourcedEntity.Discarded): 14 | pass 15 | 16 | def __init__(self, requirements, **kwargs): 17 | super(SimulatedPriceRequirements, self).__init__(**kwargs) 18 | self._requirements = requirements 19 | 20 | @property 21 | def requirements(self): 22 | return self._requirements 23 | 24 | 25 | def register_simulated_price_requirements(call_requirement_id, requirements): 26 | assert isinstance(requirements, list), type(requirements) 27 | event = SimulatedPriceRequirements.Created(entity_id=call_requirement_id, requirements=requirements) 28 | entity = SimulatedPriceRequirements.mutator(event=event) 29 | publish(event) 30 | return entity 31 | 32 | 33 | class SimulatedPriceRequirementsRepository(EntityRepository): 34 | pass 35 | -------------------------------------------------------------------------------- /quantdsl/domain/services/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /quantdsl/domain/services/call_links.py: -------------------------------------------------------------------------------- 1 | def regenerate_execution_order(contract_specification_id, call_link_repo): 2 | # assert isinstance(call_link_repo, CallLinkRepository) 3 | call_id = contract_specification_id 4 | while True: 5 | call_id = call_link_repo[call_id].call_id 6 | yield call_id 7 | if call_id == contract_specification_id: 8 | break 9 | -------------------------------------------------------------------------------- /quantdsl/domain/services/dependency_graphs.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from collections import defaultdict 3 | 4 | import six.moves.queue as queue 5 | 6 | from quantdsl.domain.model.call_dependencies import CallDependenciesRepository, register_call_dependencies 7 | from quantdsl.domain.model.call_dependents import CallDependentsRepository, register_call_dependents 8 | from quantdsl.domain.model.call_leafs import register_call_leafs 9 | from quantdsl.domain.model.call_link import register_call_link 10 | from quantdsl.domain.model.call_requirement import StubbedCall, register_call_requirement 11 | from quantdsl.domain.model.contract_specification import ContractSpecification 12 | from quantdsl.domain.services.parser import dsl_parse 13 | from quantdsl.exceptions import DslSyntaxError 14 | from quantdsl.semantics import DslExpression, DslNamespace, FunctionDef, Module, PendingCall, Stub 15 | 16 | 17 | def generate_dependency_graph(contract_specification, call_dependencies_repo, call_dependents_repo, 18 | call_requirement_repo, dsl_classes=None): 19 | assert isinstance(contract_specification, ContractSpecification) 20 | dsl_module = dsl_parse( 21 | dsl_source=contract_specification.source_code, 22 | dsl_classes=dsl_classes, 23 | ) 24 | assert isinstance(dsl_module, Module) 25 | dsl_globals = dsl_module.namespace.copy() 26 | function_defs, expressions = extract_defs_and_exprs(dsl_module, dsl_globals) 27 | dsl_expr = expressions[0] 28 | assert isinstance(dsl_expr, DslExpression) 29 | dsl_locals = DslNamespace() 30 | 31 | leaf_ids = [] 32 | all_dependents = defaultdict(list) 33 | 34 | # Generate stubbed call from the parsed DSL module object. 35 | for stubed_call in generate_stubbed_calls(contract_specification.id, dsl_expr, dsl_globals, dsl_locals, 36 | contract_specification.observation_date): 37 | # assert isinstance(stub, StubbedCall) 38 | 39 | # Estimate the cost of evaluating this expression. 40 | estimated_cost = stubed_call.dsl_expr.cost_expression() 41 | 42 | # Register the call requirements. 43 | call_id = stubed_call.call_id 44 | dsl_source = str(stubed_call.dsl_expr) 45 | present_time = stubed_call.present_time 46 | call_requirement = register_call_requirement( 47 | call_id=call_id, 48 | dsl_source=dsl_source, 49 | present_time=present_time, 50 | contract_specification_id=contract_specification.id, 51 | cost=estimated_cost, 52 | ) 53 | 54 | # Hold onto the dsl_expr, helps in "single process" modes. 55 | # - put the entity directly in the cache, otherwise the entity will be 56 | # regenerated when it is next accessed and the _dsl_expr will be "lost" 57 | call_requirement._dsl_expr = stubed_call.dsl_expr 58 | call_requirement_repo.add_cache(call_id, call_requirement) 59 | 60 | # Register the call dependencies (things needed by this call). 61 | dependencies = stubed_call.requirements 62 | register_call_dependencies(call_id, dependencies) 63 | 64 | # Keep track of the leaves and the dependents. 65 | if len(dependencies) == 0: 66 | leaf_ids.append(call_id) 67 | else: 68 | for dependency_call_id in dependencies: 69 | all_dependents[dependency_call_id].append(call_id) 70 | 71 | # Register the call dependents. 72 | for call_id, dependents in all_dependents.items(): 73 | register_call_dependents(call_id, dependents) 74 | register_call_dependents(contract_specification.id, []) 75 | 76 | # Generate and register the call order. 77 | link_id = contract_specification.id 78 | for call_id in generate_execution_order(leaf_ids, call_dependents_repo, call_dependencies_repo): 79 | register_call_link(link_id, call_id) 80 | link_id = call_id 81 | 82 | # Register the leaf ids. 83 | register_call_leafs(contract_specification.id, leaf_ids) 84 | 85 | 86 | def generate_execution_order(leaf_call_ids, call_dependents_repo, call_dependencies_repo): 87 | """ 88 | Topological sort, using Kahn's algorithm. 89 | """ 90 | assert isinstance(call_dependents_repo, CallDependentsRepository) 91 | assert isinstance(call_dependencies_repo, CallDependenciesRepository) 92 | 93 | # Initialise set of nodes that have no outstanding requirements with the leaf nodes. 94 | S = set(leaf_call_ids) 95 | removed_edges = defaultdict(set) 96 | while S: 97 | 98 | # Pick a node, n, that has zero outstanding requirements. 99 | n = S.pop() 100 | 101 | # Yield node n. 102 | yield n 103 | 104 | # Get dependents (calls waiting for this call). 105 | dependents = call_dependents_repo[n] 106 | 107 | # Visit the nodes that are dependent on n. 108 | for m in dependents: 109 | 110 | # Remove the edge n to m from the graph. 111 | removed_edges[m].add(n) 112 | 113 | # If there are zero edges to m that have not been removed, then we 114 | # can add m to the set of nodes with zero outstanding requirements. 115 | for d in call_dependencies_repo[m]: 116 | if d not in removed_edges[m]: 117 | break 118 | else: 119 | # Forget about removed edges to m. 120 | removed_edges.pop(m) 121 | 122 | # Add m to the set of nodes that have zero outstanding requirements. 123 | S.add(m) 124 | 125 | # Todo: Restore the check for remaining (unremoved) edges. Hard to do from the domain model, 126 | # so perhaps get all nodes in memory and actually remove them from a 127 | # collection so that we can see if anything remains unremoved (indicates cyclical dependencies). 128 | 129 | 130 | def generate_stubbed_calls(root_stub_id, dsl_expr, dsl_globals, dsl_locals, observation_date): 131 | # Create a stack of discovered calls to function defs. 132 | # - since we are basically doing a breadth-first search, the pending call queue 133 | # will be the max width of the graph, so it might sometimes be useful to 134 | # persist the queue to allow for larger graph. For now, just use a Python queue. 135 | pending_call_stack = PythonPendingCallQueue() 136 | 137 | # Reduce the module object into a "root" stubbed expression with pending calls on the stack. 138 | # - If an expression has a FunctionCall, it will cause a pending 139 | # call to be placed on the pending call stack, and the function call will be 140 | # replaced with a stub, which acts as a placeholder for the result of the function 141 | # call. By looping over the pending call stack until it is empty, evaluating 142 | # pending calls to generate stubbed expressions and further pending calls, the 143 | # module can be compiled into a stack of stubbed expressions. 144 | # Of course if the module's expression doesn't have a function call, there 145 | # will just be one expression on the stack of "stubbed" expressions, and it will 146 | # not have any stubs, and there will be no pending calls on the pending call stack. 147 | 148 | # Substitute the Name elements, e.g. so function calls have function defs. 149 | dsl_expr = dsl_expr.substitute_names(dsl_globals.combine(dsl_locals)) 150 | 151 | # Call functions (causes FunctionCall elements to be substituted 152 | # with Stub elements, and pending calls to be put on the stack). 153 | stubbed_expr = dsl_expr.call_functions( 154 | pending_call_stack=pending_call_stack, 155 | present_time=observation_date, 156 | observation_date=observation_date, 157 | ) 158 | 159 | # Find all the Stub elemements in the expression. 160 | dependencies = list_stub_dependencies(stubbed_expr) 161 | 162 | # Yield a stubbed call (becomes a dependency graph node). 163 | yield StubbedCall(root_stub_id, stubbed_expr, observation_date, dependencies) 164 | 165 | # Continue by looping over any pending calls. 166 | while not pending_call_stack.empty(): 167 | # Get the next pending call. 168 | pending_call = pending_call_stack.get() 169 | # assert isinstance(pending_call, PendingCall), pending_call 170 | 171 | # Get the function def. 172 | function_def = pending_call.stacked_function_def 173 | assert isinstance(function_def, FunctionDef), type(function_def) 174 | 175 | # Apply the stacked call values to the called function def. 176 | stacked_locals = dsl_locals.combine(pending_call.stacked_locals) 177 | stubbed_expr = function_def.apply( 178 | present_time=pending_call.present_time, 179 | observation_date=observation_date, 180 | pending_call_stack=pending_call_stack, 181 | # Make sure calling this pending call doesn't result 182 | # in just a pending call being added to the stack. 183 | is_destacking=True, 184 | **stacked_locals) 185 | 186 | # Put the resulting (potentially stubbed) expression on the stack of stubbed expressions. 187 | dependencies = list_stub_dependencies(stubbed_expr) 188 | 189 | yield StubbedCall(pending_call.stub_id, stubbed_expr, pending_call.present_time, dependencies) 190 | 191 | 192 | def list_stub_dependencies(stubbed_expr): 193 | return [s.name for s in stubbed_expr.list_instances(Stub)] 194 | 195 | 196 | def extract_defs_and_exprs(dsl_module, dsl_globals): 197 | # Pick out the expressions and function defs from the module body. 198 | function_defs = [] 199 | expressions = [] 200 | for dsl_obj in dsl_module.body: 201 | 202 | if isinstance(dsl_obj, FunctionDef): 203 | function_defs.append(dsl_obj) 204 | elif isinstance(dsl_obj, DslExpression): 205 | expressions.append(dsl_obj) 206 | else: 207 | raise DslSyntaxError("'%s' not allowed in module" % type(dsl_obj), dsl_obj, node=dsl_obj.node) 208 | 209 | return function_defs, expressions 210 | 211 | 212 | class PendingCallQueue(object): 213 | def put(self, stub_id, stacked_function_def, stacked_locals, present_time): 214 | pending_call = self.validate_pending_call(present_time, stacked_function_def, stacked_locals, stub_id) 215 | self.put_pending_call(pending_call) 216 | 217 | def validate_pending_call(self, present_time, stacked_function_def, stacked_locals, stub_id): 218 | # assert isinstance(stub_id, six.string_types), type(stub_id) 219 | # assert isinstance(stacked_function_def, FunctionDef), type(stacked_function_def) 220 | # assert isinstance(stacked_locals, DslNamespace), type(stacked_locals) 221 | # assert isinstance(stacked_globals, DslNamespace), type(stacked_globals) 222 | # assert isinstance(present_time, (datetime.datetime, type(None))), type(present_time) 223 | return PendingCall(stub_id, stacked_function_def, stacked_locals, present_time) 224 | 225 | @abstractmethod 226 | def put_pending_call(self, pending_call): 227 | """ 228 | Puts pending call on the queue. 229 | """ 230 | 231 | @abstractmethod 232 | def get(self): 233 | """ 234 | Gets pending call from the queue. 235 | """ 236 | 237 | 238 | class PythonPendingCallQueue(PendingCallQueue): 239 | def __init__(self): 240 | self.queue = queue.Queue() 241 | 242 | def put_pending_call(self, pending_call): 243 | self.queue.put(pending_call) 244 | 245 | def empty(self): 246 | return self.queue.empty() 247 | 248 | def get(self, *args, **kwargs): 249 | return self.queue.get(*args, **kwargs) 250 | -------------------------------------------------------------------------------- /quantdsl/domain/services/parser.py: -------------------------------------------------------------------------------- 1 | from quantdsl.syntax import DslParser 2 | 3 | 4 | def dsl_parse(dsl_source, filename='', dsl_classes=None): 5 | """ 6 | Parses DSL module, created according to the given DSL source. 7 | """ 8 | if dsl_classes is None: 9 | dsl_classes = {} 10 | 11 | from quantdsl.semantics import defaultDslClasses 12 | _dsl_classes = defaultDslClasses.copy() 13 | _dsl_classes.update(dsl_classes) 14 | 15 | return DslParser(_dsl_classes).parse(dsl_source, filename=filename) -------------------------------------------------------------------------------- /quantdsl/domain/services/price_processes.py: -------------------------------------------------------------------------------- 1 | from quantdsl.exceptions import DslError 2 | from quantdsl.priceprocess.base import PriceProcess 3 | 4 | 5 | def get_price_process(price_process_name): 6 | # Load the price process object. 7 | assert price_process_name, "Price process name is required" 8 | price_process_module_name, price_process_class_name = price_process_name.rsplit('.', 1) 9 | try: 10 | price_process_module = __import__(price_process_module_name, '', '', '*') 11 | except Exception as e: 12 | raise DslError("Can't import price process module '%s': %s" % (price_process_module_name, e)) 13 | try: 14 | price_process_class = getattr(price_process_module, price_process_class_name) 15 | except Exception as e: 16 | raise DslError("Can't find price process class '%s' in module '%s': %s" % ( 17 | price_process_class_name, price_process_module_name, e)) 18 | assert issubclass(price_process_class, PriceProcess) 19 | # Instantiate the price process object class. 20 | price_process = price_process_class() 21 | return price_process 22 | -------------------------------------------------------------------------------- /quantdsl/domain/services/simulated_prices.py: -------------------------------------------------------------------------------- 1 | import six 2 | 3 | from quantdsl.domain.model.call_dependencies import CallDependencies 4 | from quantdsl.domain.model.call_link import CallLinkRepository 5 | from quantdsl.domain.model.call_requirement import CallRequirementRepository, CallRequirement 6 | from quantdsl.domain.model.market_calibration import MarketCalibration 7 | from quantdsl.domain.model.perturbation_dependencies import register_perturbation_dependencies 8 | from quantdsl.domain.model.market_simulation import MarketSimulation 9 | from quantdsl.domain.model.simulated_price import register_simulated_price 10 | from quantdsl.domain.model.simulated_price_requirements import register_simulated_price_requirements 11 | from quantdsl.domain.services.call_links import regenerate_execution_order 12 | from quantdsl.domain.services.parser import dsl_parse 13 | from quantdsl.domain.services.price_processes import get_price_process 14 | from quantdsl.priceprocess.base import PriceProcess 15 | from quantdsl.semantics import DslObject, AbstractMarket 16 | 17 | 18 | def generate_simulated_prices(market_simulation, market_calibration): 19 | for commodity_name, fixing_date, delivery_date, price_value in simulate_future_prices(market_simulation, market_calibration): 20 | yield register_simulated_price(market_simulation.id, commodity_name, fixing_date, delivery_date, price_value) 21 | 22 | 23 | def simulate_future_prices(market_simulation, market_calibration): 24 | assert isinstance(market_simulation, MarketSimulation), market_simulation 25 | if not market_simulation.requirements: 26 | return [] 27 | assert isinstance(market_calibration, MarketCalibration), market_calibration 28 | price_process = get_price_process(market_calibration.price_process_name) 29 | assert isinstance(price_process, PriceProcess), price_process 30 | return price_process.simulate_future_prices( 31 | observation_date=market_simulation.observation_date, 32 | requirements=market_simulation.requirements, 33 | path_count=market_simulation.path_count, 34 | calibration_params=market_calibration.calibration_params) 35 | 36 | 37 | def identify_simulation_requirements(contract_specification_id, call_requirement_repo, call_link_repo, 38 | call_dependencies_repo, observation_date, requirements, periodisation=None): 39 | assert isinstance(call_requirement_repo, CallRequirementRepository) 40 | assert isinstance(call_link_repo, CallLinkRepository) 41 | 42 | all_perturbation_dependencies = {} 43 | 44 | call_requirements = [] 45 | 46 | for call_id in regenerate_execution_order(contract_specification_id, call_link_repo): 47 | 48 | # Get the stubbed expression. 49 | call_requirement = call_requirement_repo[call_id] 50 | assert isinstance(call_requirement, CallRequirement) 51 | 52 | if call_requirement._dsl_expr is not None: 53 | dsl_expr = call_requirement._dsl_expr 54 | else: 55 | dsl_module = dsl_parse(call_requirement.dsl_source) 56 | dsl_expr = dsl_module.body[0] 57 | call_requirement._dsl_expr = dsl_expr 58 | assert isinstance(dsl_expr, DslObject), dsl_expr 59 | 60 | call_requirements.append(call_requirement) 61 | 62 | all_market_names = set() 63 | for call_requirement in call_requirements: 64 | dsl_expr = call_requirement._dsl_expr 65 | for market in dsl_expr.find_instances(AbstractMarket): 66 | assert isinstance(market.commodity_name, six.string_types) 67 | all_market_names.add(market.commodity_name) 68 | 69 | for call_requirement in call_requirements: 70 | dsl_expr = call_requirement._dsl_expr 71 | 72 | call_id = call_requirement.id 73 | 74 | # Todo: Consolidate 'date' attributes to be a single element (rather than a possibly long sum expression). 75 | 76 | # Identify this call's requirements for simulated prices. 77 | simulation_requirements = set() 78 | present_time = call_requirement.present_time 79 | dsl_expr.identify_price_simulation_requirements( 80 | requirements=simulation_requirements, 81 | present_time=present_time, 82 | observation_date=observation_date, 83 | periodisation=periodisation, 84 | all_market_names=all_market_names, 85 | ) 86 | 87 | # Register the simulation requirements for each call (needed during evaluation). 88 | register_simulated_price_requirements(call_id, list(simulation_requirements)) 89 | 90 | # Update the simulation requirements (needed for the market simulation). 91 | requirements.update(simulation_requirements) 92 | 93 | # Identify this call's perturbation dependencies. 94 | perturbation_dependencies = set() 95 | dsl_expr.identify_perturbation_dependencies( 96 | dependencies=perturbation_dependencies, 97 | present_time=present_time, 98 | periodisation=periodisation, 99 | ) 100 | 101 | # Add the expression's perturbation dependencies to the perturbation dependencies of its call dependencies. 102 | call_dependencies = call_dependencies_repo[call_id] 103 | assert isinstance(call_dependencies, CallDependencies), call_dependencies 104 | for dependency_id in call_dependencies.dependencies: 105 | dependency_perturbation_dependencies = all_perturbation_dependencies[dependency_id] 106 | perturbation_dependencies.update(dependency_perturbation_dependencies) 107 | # Register the perturbation dependencies in the repo (needed when evaluating the call). 108 | register_perturbation_dependencies(call_id, list(perturbation_dependencies)) 109 | 110 | # Save the perturbation dependencies for this call, so they are available for the dependent calls. 111 | all_perturbation_dependencies[call_id] = perturbation_dependencies 112 | 113 | -------------------------------------------------------------------------------- /quantdsl/domain/services/uuids.py: -------------------------------------------------------------------------------- 1 | def create_uuid4(): 2 | import uuid 3 | return uuid.uuid4().hex 4 | -------------------------------------------------------------------------------- /quantdsl/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | 4 | class DslError(Exception): 5 | """ 6 | Quant DSL exception base class. 7 | """ 8 | # Todo: Just have one msg parameter (merge 'error' and 'descr')? 9 | # Todo: Don't keep reference to node, just keep the line number. 10 | def __init__(self, error, descr=None, node=None): 11 | self.error = error 12 | self.descr = descr 13 | self.node = node 14 | self.lineno = getattr(node, "lineno", None) 15 | 16 | def __repr__(self): 17 | msg = self.error 18 | if self.descr: 19 | msg += ": %s" % self.descr 20 | if self.lineno: 21 | msg += " (line %d)" % (self.lineno) 22 | return msg 23 | 24 | __str__ = __repr__ 25 | 26 | 27 | class DslSyntaxError(DslError): 28 | """ 29 | Exception class for user syntax errors. 30 | """ 31 | 32 | 33 | class DslTestExpressionCannotBeEvaluated(DslSyntaxError): 34 | """ 35 | Exception class for test expression evaluation errors. 36 | """ 37 | 38 | 39 | class DslPresentTimeNotInScope(DslSyntaxError): 40 | """ 41 | Exception class for present time not being in scope. 42 | """ 43 | 44 | 45 | class DslIfTestExpressionError(DslSyntaxError): 46 | """ 47 | Exception class for user syntax errors (comparison errors). 48 | """ 49 | 50 | 51 | class DslCompareArgsError(DslSyntaxError): 52 | """ 53 | Exception class for user syntax errors (comparison errors). 54 | """ 55 | 56 | 57 | class DslBinOpArgsError(DslSyntaxError): 58 | """ 59 | Exception class for user syntax errors (binary operation). 60 | """ 61 | 62 | 63 | class DslNameError(DslSyntaxError): 64 | """ 65 | Exception class for undefined names. 66 | """ 67 | 68 | 69 | class DslSystemError(DslError): 70 | """ 71 | Exception class for DSL system errors. 72 | """ 73 | 74 | 75 | class DslCompileError(DslError): 76 | pass 77 | 78 | 79 | class CallLimitError(DslCompileError): 80 | pass 81 | 82 | 83 | class RecursionDepthError(DslCompileError): 84 | pass 85 | 86 | 87 | class TimeoutError(DslError): 88 | pass 89 | 90 | 91 | class InterruptSignalReceived(DslError): 92 | pass -------------------------------------------------------------------------------- /quantdsl/infrastructure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/infrastructure/__init__.py -------------------------------------------------------------------------------- /quantdsl/infrastructure/dependency_graph_subscriber.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from eventsourcing.domain.model.events import subscribe, unsubscribe 3 | from quantdsl.domain.model.call_dependencies import CallDependenciesRepository 4 | from quantdsl.domain.model.call_dependents import CallDependentsRepository 5 | from quantdsl.domain.model.call_requirement import CallRequirement 6 | from quantdsl.domain.model.contract_specification import ContractSpecificationRepository, ContractSpecification 7 | from quantdsl.domain.services.dependency_graphs import generate_dependency_graph 8 | from quantdsl.exceptions import CallLimitError, RecursionDepthError 9 | 10 | 11 | class DependencyGraphSubscriber(object): 12 | 13 | def __init__(self, contract_specification_repo, call_dependencies_repo, call_dependents_repo, call_leafs_repo, 14 | call_requirement_repo, max_dependency_graph_size, dsl_classes): 15 | assert isinstance(contract_specification_repo, ContractSpecificationRepository) 16 | assert isinstance(call_dependencies_repo, CallDependenciesRepository) 17 | assert isinstance(call_dependents_repo, CallDependentsRepository) 18 | self.contract_specification_repo = contract_specification_repo 19 | self.call_dependencies_repo = call_dependencies_repo 20 | self.call_dependents_repo = call_dependents_repo 21 | self.call_leafs_repo = call_leafs_repo 22 | self.call_requirement_repo = call_requirement_repo 23 | subscribe(self.contract_specification_created, self.generate_dependency_graph) 24 | subscribe(self.call_requirement_created, self.limit_calls) 25 | self.total_calls = defaultdict(int) 26 | self.max_dependency_graph_size = max_dependency_graph_size 27 | self.dsl_classes = dsl_classes 28 | 29 | def close(self): 30 | unsubscribe(self.contract_specification_created, self.generate_dependency_graph) 31 | unsubscribe(self.call_requirement_created, self.limit_calls) 32 | 33 | def contract_specification_created(self, event): 34 | return isinstance(event, ContractSpecification.Created) 35 | 36 | def call_requirement_created(self, event): 37 | return isinstance(event, CallRequirement.Created) 38 | 39 | def limit_calls(self, event): 40 | if self.max_dependency_graph_size: 41 | contract_specification_id = event.contract_specification_id 42 | self.total_calls[contract_specification_id] += 1 43 | if self.total_calls[contract_specification_id] > self.max_dependency_graph_size: 44 | raise CallLimitError('maximum dependency graph size ({}) exceeded'.format( 45 | self.max_dependency_graph_size)) 46 | 47 | def generate_dependency_graph(self, event): 48 | assert isinstance(event, ContractSpecification.Created) 49 | contract_specification = self.contract_specification_repo[event.entity_id] 50 | try: 51 | generate_dependency_graph(contract_specification, self.call_dependencies_repo, self.call_dependents_repo, 52 | self.call_requirement_repo, dsl_classes=self.dsl_classes) 53 | except RuntimeError as e: 54 | if 'maximum recursion depth exceeded' in str(e): 55 | raise RecursionDepthError('maximum recursion depth exceeded') 56 | else: 57 | raise 58 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/evaluation_subscriber.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.events import subscribe, unsubscribe 2 | 3 | from quantdsl.domain.model.call_dependencies import CallDependenciesRepository 4 | from quantdsl.domain.model.call_dependents import CallDependentsRepository 5 | from quantdsl.domain.model.call_link import CallLinkRepository 6 | from quantdsl.domain.model.call_requirement import CallRequirementRepository 7 | from quantdsl.domain.model.call_result import CallResultRepository 8 | from quantdsl.domain.model.contract_valuation import ContractValuation, ContractValuationRepository 9 | from quantdsl.domain.model.market_simulation import MarketSimulationRepository 10 | from quantdsl.domain.model.simulated_price import SimulatedPriceRepository 11 | from quantdsl.domain.services.contract_valuations import generate_contract_valuation 12 | from quantdsl.infrastructure.event_sourced_repos.perturbation_dependencies_repo import PerturbationDependenciesRepo 13 | from quantdsl.infrastructure.event_sourced_repos.simulated_price_dependencies_repo import \ 14 | SimulatedPriceRequirementsRepo 15 | 16 | 17 | class EvaluationSubscriber(object): 18 | 19 | def __init__(self, contract_valuation_repo, call_link_repo, call_dependencies_repo, call_requirement_repo, 20 | call_result_repo, simulated_price_repo, market_simulation_repo, call_leafs_repo, 21 | call_evaluation_queue, call_dependents_repo, 22 | perturbation_dependencies_repo, simulated_price_requirements_repo): 23 | assert isinstance(contract_valuation_repo, ContractValuationRepository), contract_valuation_repo 24 | assert isinstance(call_link_repo, CallLinkRepository), call_link_repo 25 | assert isinstance(call_dependencies_repo, CallDependenciesRepository), call_dependencies_repo 26 | assert isinstance(call_requirement_repo, CallRequirementRepository), call_requirement_repo 27 | assert isinstance(call_result_repo, (CallResultRepository, dict)), call_result_repo 28 | # assert isinstance(simulated_price_repo, SimulatedPriceRepository), simulated_price_repo 29 | assert isinstance(market_simulation_repo, MarketSimulationRepository), market_simulation_repo 30 | assert isinstance(call_dependents_repo, CallDependentsRepository), call_dependents_repo 31 | assert isinstance(perturbation_dependencies_repo, PerturbationDependenciesRepo), perturbation_dependencies_repo 32 | assert isinstance(simulated_price_requirements_repo, SimulatedPriceRequirementsRepo), simulated_price_requirements_repo 33 | self.contract_valuation_repo = contract_valuation_repo 34 | self.call_link_repo = call_link_repo 35 | self.call_dependencies_repo = call_dependencies_repo 36 | self.call_requirement_repo = call_requirement_repo 37 | self.call_result_repo = call_result_repo 38 | self.simulated_price_repo = simulated_price_repo 39 | self.market_simulation_repo = market_simulation_repo 40 | self.call_leafs_repo = call_leafs_repo 41 | self.call_evaluation_queue = call_evaluation_queue 42 | self.call_dependents_repo = call_dependents_repo 43 | self.perturbation_dependencies_repo = perturbation_dependencies_repo 44 | self.simulated_price_dependencies_repo = simulated_price_requirements_repo 45 | subscribe(self.is_contract_valuation_created, self.generate_contract_valuation) 46 | 47 | def close(self): 48 | unsubscribe(self.is_contract_valuation_created, self.generate_contract_valuation) 49 | 50 | @staticmethod 51 | def is_contract_valuation_created(event): 52 | return isinstance(event, ContractValuation.Created) 53 | 54 | def generate_contract_valuation(self, event): 55 | assert isinstance(event, ContractValuation.Created) 56 | generate_contract_valuation(contract_valuation_id=event.entity_id, 57 | call_dependencies_repo=self.call_dependencies_repo, 58 | call_evaluation_queue=self.call_evaluation_queue, 59 | call_leafs_repo=self.call_leafs_repo, 60 | call_link_repo=self.call_link_repo, 61 | call_requirement_repo=self.call_requirement_repo, 62 | call_result_repo=self.call_result_repo, 63 | contract_valuation_repo=self.contract_valuation_repo, 64 | market_simulation_repo=self.market_simulation_repo, 65 | simulated_price_repo=self.simulated_price_repo, 66 | perturbation_dependencies_repo=self.perturbation_dependencies_repo, 67 | simulated_price_dependencies_repo=self.simulated_price_dependencies_repo, 68 | is_double_sided_deltas=event.is_double_sided_deltas 69 | ) 70 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'john' 2 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_dependencies_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.call_dependencies import CallDependencies, CallDependenciesRepository 4 | 5 | 6 | class CallDependenciesRepo(CallDependenciesRepository, EventSourcedRepository): 7 | 8 | domain_class = CallDependencies -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_dependents_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.call_dependents import CallDependents, CallDependentsRepository 4 | 5 | 6 | class CallDependentsRepo(CallDependentsRepository, EventSourcedRepository): 7 | 8 | domain_class = CallDependents -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_leafs_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.call_leafs import CallLeafs, CallLeafsRepository 4 | 5 | 6 | class CallLeafsRepo(CallLeafsRepository, EventSourcedRepository): 7 | 8 | domain_class = CallLeafs -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_link_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | from quantdsl.domain.model.call_link import CallLink, CallLinkRepository 3 | 4 | 5 | class CallLinkRepo(CallLinkRepository, EventSourcedRepository): 6 | 7 | domain_class = CallLink 8 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_requirement_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.call_requirement import CallRequirement 4 | from quantdsl.domain.model.call_requirement import CallRequirementRepository 5 | 6 | 7 | class CallRequirementRepo(CallRequirementRepository, EventSourcedRepository): 8 | 9 | domain_class = CallRequirement -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/call_result_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.call_result import CallResult, CallResultRepository 4 | 5 | 6 | class CallResultRepo(CallResultRepository, EventSourcedRepository): 7 | 8 | domain_class = CallResult 9 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/contract_specification_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | from quantdsl.domain.model.contract_specification import ContractSpecification, ContractSpecificationRepository 3 | 4 | 5 | class ContractSpecificationRepo(ContractSpecificationRepository, EventSourcedRepository): 6 | 7 | domain_class = ContractSpecification 8 | 9 | 10 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/contract_valuation_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.contract_valuation import ContractValuationRepository, ContractValuation 4 | 5 | 6 | class ContractValuationRepo(ContractValuationRepository, EventSourcedRepository): 7 | 8 | domain_class = ContractValuation 9 | 10 | 11 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/market_calibration_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | from quantdsl.domain.model.market_calibration import MarketCalibration, MarketCalibrationRepository 3 | 4 | 5 | class MarketCalibrationRepo(MarketCalibrationRepository, EventSourcedRepository): 6 | 7 | domain_class = MarketCalibration 8 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/market_simulation_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | from quantdsl.domain.model.market_simulation import MarketSimulation, MarketSimulationRepository 3 | 4 | 5 | class MarketSimulationRepo(MarketSimulationRepository, EventSourcedRepository): 6 | 7 | domain_class = MarketSimulation 8 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/perturbation_dependencies_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.perturbation_dependencies import PerturbationDependencies, PerturbationDependenciesRepository 4 | 5 | 6 | class PerturbationDependenciesRepo(PerturbationDependenciesRepository, EventSourcedRepository): 7 | 8 | domain_class = PerturbationDependencies -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/simulated_price_dependencies_repo.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | 3 | from quantdsl.domain.model.simulated_price_requirements import SimulatedPriceRequirements, SimulatedPriceRequirementsRepository 4 | 5 | 6 | class SimulatedPriceRequirementsRepo(SimulatedPriceRequirementsRepository, EventSourcedRepository): 7 | 8 | domain_class = SimulatedPriceRequirements -------------------------------------------------------------------------------- /quantdsl/infrastructure/event_sourced_repos/simulated_price_repo.py: -------------------------------------------------------------------------------- 1 | # from eventsourcing.infrastructure.event_sourced_repo import EventSourcedRepository 2 | # 3 | # from quantdsl.domain.model.simulated_price import SimulatedPrice, SimulatedPriceRepository 4 | # 5 | # 6 | # class SimulatedPriceRepo(SimulatedPriceRepository, EventSourcedRepository): 7 | # 8 | # domain_class = SimulatedPrice 9 | # 10 | # 11 | -------------------------------------------------------------------------------- /quantdsl/infrastructure/simulation_subscriber.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.events import subscribe, unsubscribe 2 | 3 | from quantdsl.domain.model.market_calibration import MarketCalibrationRepository 4 | from quantdsl.domain.model.market_simulation import MarketSimulation, MarketSimulationRepository 5 | from quantdsl.domain.model.simulated_price import SimulatedPriceRepository 6 | from quantdsl.domain.services.simulated_prices import generate_simulated_prices 7 | 8 | 9 | class SimulationSubscriber(object): 10 | # When a market simulation is created, generate and register all the simulated prices. 11 | 12 | def __init__(self, market_calibration_repo, market_simulation_repo, simulated_price_repo): 13 | assert isinstance(market_calibration_repo, MarketCalibrationRepository) 14 | assert isinstance(market_simulation_repo, MarketSimulationRepository) 15 | # assert isinstance(simulated_price_repo, SimulatedPriceRepository) 16 | self.market_calibration_repo = market_calibration_repo 17 | self.market_simulation_repo = market_simulation_repo 18 | self.simulated_price_repo = simulated_price_repo 19 | subscribe(self.market_simulation_created, self.generate_simulated_prices_for_market_simulation) 20 | 21 | def close(self): 22 | unsubscribe(self.market_simulation_created, self.generate_simulated_prices_for_market_simulation) 23 | 24 | def market_simulation_created(self, event): 25 | return isinstance(event, MarketSimulation.Created) 26 | 27 | def generate_simulated_prices_for_market_simulation(self, event): 28 | assert isinstance(event, MarketSimulation.Created) 29 | market_simulation = self.market_simulation_repo[event.entity_id] 30 | market_calibration = self.market_calibration_repo[event.market_calibration_id] 31 | for simulated_price in generate_simulated_prices(market_simulation, market_calibration): 32 | self.simulated_price_repo[simulated_price.id] = simulated_price 33 | -------------------------------------------------------------------------------- /quantdsl/interfaces/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/interfaces/__init__.py -------------------------------------------------------------------------------- /quantdsl/interfaces/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/interfaces/cli/__init__.py -------------------------------------------------------------------------------- /quantdsl/interfaces/cli/main.py: -------------------------------------------------------------------------------- 1 | """Unittest main program""" 2 | 3 | import sys 4 | import os 5 | import types 6 | 7 | # from . import loader, runner 8 | # from .signals import installHandler 9 | 10 | # __unittest = True 11 | 12 | # FAILFAST = " -f, --failfast Stop on first failure\n" 13 | # CATCHBREAK = " -c, --catch Catch control-C and display results\n" 14 | # BUFFEROUTPUT = " -b, --buffer Buffer stdout and stderr during test runs\n" 15 | import datetime 16 | import six 17 | 18 | from quantdsl.interfaces.calcandplot import calc_print 19 | from quantdsl.syntax import find_module_path 20 | 21 | USAGE_AS_MAIN = """\ 22 | Usage: %(progName)s [options] [tests] 23 | 24 | Options: 25 | -h, --help Show this message 26 | -v, --verbose Verbose output 27 | -q, --quiet Minimal output 28 | 29 | Example: 30 | %(progName)s mylib.model1 31 | 32 | """ 33 | 34 | 35 | class TestProgram(object): 36 | """A command-line program that evaluates a module. 37 | """ 38 | USAGE = USAGE_AS_MAIN 39 | 40 | progName = None 41 | 42 | def __init__(self, module='__main__', argv=None, exit=True, verbosity=1): 43 | if isinstance(module, six.string_types): 44 | self.module = __import__(module) 45 | for part in module.split('.')[1:]: 46 | self.module = getattr(self.module, part) 47 | else: 48 | self.module = module 49 | if argv is None: 50 | argv = sys.argv 51 | 52 | self.exit = exit 53 | self.verbosity = verbosity 54 | self.progName = os.path.basename(argv[0]) 55 | self.parseArgs(argv) 56 | self.runTests() 57 | 58 | def usageExit(self, msg=None): 59 | if msg: 60 | print(msg) 61 | usage = {'progName': self.progName} 62 | print(self.USAGE % usage) 63 | sys.exit(2) 64 | 65 | def parseArgs(self, argv): 66 | # if len(argv) > 1 and argv[1].lower() == 'discover': 67 | # self._do_discovery(argv[2:]) 68 | # return 69 | 70 | import getopt 71 | long_opts = ['help', 'verbose', 'quiet'] 72 | try: 73 | options, args = getopt.getopt(argv[1:], 'hHvq', long_opts) 74 | for opt, value in options: 75 | if opt in ('-h','-H','--help'): 76 | self.usageExit() 77 | if opt in ('-q','--quiet'): 78 | self.verbosity = 0 79 | if opt in ('-v','--verbose'): 80 | self.verbosity = 2 81 | if len(args) == 0: 82 | self.usageExit('module name required') 83 | self.testNames = args 84 | if __name__ == '__main__': 85 | # to support python -m unittest ... 86 | self.module = None 87 | self.createTests() 88 | except getopt.error as msg: 89 | self.usageExit(msg) 90 | 91 | def createTests(self): 92 | pass 93 | 94 | def runTests(self): 95 | for module_name in self.testNames: 96 | path = find_module_path(module_name) 97 | with open(path) as f: 98 | source_code = f.read() 99 | calc_print(source_code, 100 | observation_date='2011-1-1', 101 | verbose=True, 102 | ) 103 | print("Evaluating module") 104 | # if self.testRunner is None: 105 | # self.testRunner = runner.TextTestRunner 106 | # if isinstance(self.testRunner, (type, types.ClassType)): 107 | # try: 108 | # testRunner = self.testRunner(verbosity=self.verbosity, 109 | # failfast=self.failfast, 110 | # buffer=self.buffer) 111 | # except TypeError: 112 | # # didn't accept the verbosity, buffer or failfast arguments 113 | # testRunner = self.testRunner() 114 | # else: 115 | # # it is assumed to be a TestRunner instance 116 | # # testRunner = self.testRunner 117 | # self.result = testRunner.run(self.test) 118 | # if self.exit: 119 | # sys.exit(not self.result.wasSuccessful()) 120 | 121 | main = TestProgram 122 | -------------------------------------------------------------------------------- /quantdsl/interfaces/results.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import datetime 3 | from collections import defaultdict 4 | 5 | import math 6 | import pandas 7 | import pandas.plotting 8 | import scipy 9 | import six 10 | from matplotlib import pylab as plt 11 | from numpy import nanpercentile 12 | from numpy.core.multiarray import array 13 | from pandas import DataFrame, Series 14 | 15 | from quantdsl.defaults import DEFAULT_CONFIDENCE_INTERVAL 16 | from quantdsl.domain.model.call_result import CallResult 17 | from quantdsl.domain.model.contract_valuation import ContractValuation 18 | from quantdsl.domain.model.market_simulation import MarketSimulation 19 | 20 | 21 | class Results(object): 22 | def __init__(self, valuation_result, periods, contract_valuation, market_simulation): 23 | assert isinstance(contract_valuation, ContractValuation), type(contract_valuation) 24 | assert isinstance(market_simulation, MarketSimulation), type(market_simulation) 25 | assert isinstance(valuation_result, CallResult), type(valuation_result) 26 | assert isinstance(periods, list) 27 | self.valuation_result = valuation_result 28 | self.fair_value = valuation_result.result_value 29 | self.periods = periods 30 | self.contract_valuation = contract_valuation 31 | self.market_simulation = market_simulation 32 | self.observation_date = market_simulation.observation_date 33 | self.path_count = market_simulation.path_count 34 | self.perturbation_factor = market_simulation.perturbation_factor 35 | self.interest_rate = market_simulation.interest_rate 36 | self.init_dataframes() 37 | self.confidence_interval = None 38 | 39 | def init_dataframes(self): 40 | if not self.periods: 41 | return 42 | names = set() 43 | dates = set() 44 | self.periods_by_market_and_date = defaultdict(dict) 45 | self.periods_by_date_and_market = defaultdict(dict) 46 | for p in self.periods: 47 | name = p['market_name'] 48 | names.add(name) 49 | date = p['delivery_date'] 50 | dates.add(date) 51 | self.periods_by_market_and_date[name][date] = p 52 | self.periods_by_date_and_market[date][name] = p 53 | 54 | self.names = sorted(names) 55 | self.dates = sorted(dates) 56 | 57 | # Price. 58 | # self.prices_raw = get_dataframe('price_simulated', measure='direct') 59 | self.prices_mean = self.get_dataframe('price_simulated') 60 | 61 | # Hedge units. 62 | self.hedges_mean = self.get_dataframe('hedge_units') 63 | 64 | # Cash. 65 | self.cash_mean = self.get_dataframe('cash', sum=True, cum=True) 66 | 67 | self.deltas = {p['perturbation_name']: p['delta'] for p in self.periods} 68 | 69 | # Old stuff, currently used by print(). 70 | # Todo: Change print to depend on stuff above, then remove next four lines. 71 | self.by_delivery_date = defaultdict(list) 72 | self.by_market_name = defaultdict(list) 73 | [self.by_delivery_date[p['delivery_date']].append(p) for p in self.periods] 74 | [self.by_market_name[p['market_name']].append(p) for p in self.periods] 75 | 76 | def init_dataframe_errors(self, confidence_interval): 77 | self.confidence_interval = confidence_interval 78 | self.prices_errors = self.get_dataframe('price_simulated', measure='quantile') 79 | self.hedges_errors = self.get_dataframe('hedge_units', measure='quantile') 80 | self.cash_errors = self.get_dataframe('cash', sum=True, cum=True, measure='quantile') 81 | 82 | def get_dataframe(self, attr, measure='mean', sum=False, cum=False): 83 | """ 84 | :rtype NDFrame 85 | """ 86 | values = [] 87 | for name in self.names: 88 | market_periods_by_date = self.periods_by_market_and_date[name] 89 | values.append([market_periods_by_date[date][attr] for date in self.dates]) 90 | 91 | values = array(values) # shape is names-dates-samples 92 | 93 | if cum: 94 | # Accumulate over the dates, the second axis. 95 | # shape is the same: names-dates-samples 96 | values = values.cumsum(axis=1) 97 | 98 | if sum: 99 | # Sum over the names, the first axis. 100 | # shape is dates-samples 101 | values = values.sum(axis=0) 102 | pass 103 | 104 | if measure == 'mean': 105 | values = values.mean(axis=-1) 106 | elif measure == 'std': 107 | values = values.std(axis=-1) 108 | elif measure == 'quantile': 109 | assert self.confidence_interval is not None 110 | low_percentile = (100 - self.confidence_interval) / 2.0 111 | high_percentile = 100 - low_percentile 112 | mean = values.mean(axis=-1) 113 | low = mean - nanpercentile(values, q=low_percentile, axis=-1) 114 | high = nanpercentile(values, q=high_percentile, axis=-1) - mean 115 | errors = [] 116 | if sum: 117 | # Need to return 2-len(dates) sized array, for a Series. 118 | errors.append([low, high]) 119 | else: 120 | # Need to return len(names)-2-len(dates) sized array, for a DateFrame. 121 | for i in range(len(self.names)): 122 | errors.append([low[i], high[i]]) 123 | values = array(errors) 124 | return values 125 | # elif measure == 'direct': 126 | # raise NotImplementedError() 127 | # if len(values) == 1: 128 | # values = values[0] 129 | # else: 130 | # raise NotImplementedError() 131 | # return DataFrame(values, index=dates, columns=names) 132 | else: 133 | raise Exception("Measure '{}' not supported".format(measure)) 134 | 135 | if sum: 136 | return Series(values, index=self.dates) 137 | else: 138 | return DataFrame(values.T, index=self.dates, columns=self.names) 139 | 140 | @property 141 | def fair_value_mean(self): 142 | if isinstance(self.fair_value, scipy.ndarray): 143 | fair_value_mean = self.fair_value.mean() 144 | else: 145 | fair_value_mean = self.fair_value 146 | return fair_value_mean 147 | 148 | def plot(self, title='', confidence_interval=DEFAULT_CONFIDENCE_INTERVAL, block=False, pause=0, figsize=None): 149 | 150 | self.init_dataframe_errors(confidence_interval) 151 | 152 | assert isinstance(self, Results) 153 | 154 | if not self.periods: 155 | raise ValueError("Results have no periods to plot") 156 | 157 | fig, axes = plt.subplots(nrows=3, ncols=1, figsize=figsize) 158 | 159 | if title: 160 | fig.canvas.set_window_title(title) 161 | 162 | if isinstance(self.observation_date, datetime.datetime): 163 | observation_date = self.observation_date.date() 164 | else: 165 | observation_date = self.observation_date 166 | 167 | fig.suptitle('Obs {}, rate {}%, path {}, pert {}, conf {}%'.format( 168 | observation_date, self.interest_rate, self.path_count, 169 | self.perturbation_factor, 170 | confidence_interval)) 171 | 172 | with pandas.plotting.plot_params.use('x_compat', False): 173 | 174 | # Todo: Try to get the box plots working: 175 | # https://stackoverflow.com/questions/38120688/pandas-box-plot-for-multiple-column 176 | 177 | # if len(results.prices_raw) == 1: 178 | # prices = results.prices_raw[0] 179 | # seaborn.boxplot(prices, prices.to_series().apply(lambda x: x.strftime('%Y%m%d')), ax=axes[0]) 180 | 181 | self.prices_mean.plot(ax=axes[0], kind='bar', yerr=self.prices_errors) 182 | axes[0].set_title('Market prices') 183 | axes[0].get_xaxis().set_visible(False) 184 | 185 | self.hedges_mean.plot(ax=axes[1], kind='bar', yerr=self.hedges_errors).axhline(0, color='0.5') 186 | axes[1].set_title('Delta hedges') 187 | axes[1].get_xaxis().set_visible(False) 188 | 189 | self.cash_mean.plot(ax=axes[2], kind='bar', yerr=self.cash_errors, color='g').axhline(0, color='0.5') 190 | axes[2].set_title('Cash account') 191 | axes[2].get_xaxis().set_visible(True) 192 | 193 | fig.autofmt_xdate(rotation=30) 194 | 195 | if pause and not block: 196 | plt.pause(pause) 197 | else: 198 | plt.show(block=block) 199 | 200 | def __str__(self): 201 | s = '' 202 | s += "\n\n" 203 | 204 | dates = [] 205 | for period in self.periods: 206 | date = period['delivery_date'] 207 | if date not in dates: 208 | dates.append(date) 209 | 210 | sqrt_path_count = math.sqrt(self.path_count) 211 | if isinstance(self.fair_value, six.integer_types + (float,)): 212 | fair_value_mean = self.fair_value 213 | fair_value_stderr = 0 214 | else: 215 | fair_value_mean = self.fair_value.mean() 216 | fair_value_stderr = self.fair_value.std() / sqrt_path_count 217 | 218 | if self.periods: 219 | net_hedge_cost = 0.0 220 | net_hedge_units = defaultdict(int) 221 | 222 | for delivery_date, markets_results in sorted(self.by_delivery_date.items()): 223 | for market_result in sorted(markets_results, key=lambda x: x['market_name']): 224 | market_name = market_result['market_name'] 225 | if delivery_date: 226 | s += "{} {}\n".format(delivery_date, market_name) 227 | else: 228 | s += market_name 229 | price_simulated = market_result['price_simulated'].mean() 230 | s += "Price: {: >8.2f}\n".format(price_simulated) 231 | delta = market_result['delta'].mean() 232 | s += "Delta: {: >8.2f}\n".format(delta) 233 | hedge_units = market_result['hedge_units'] 234 | hedge_units_mean = hedge_units.mean() 235 | hedge_units_stderr = hedge_units.std() / sqrt_path_count 236 | s += "Hedge: {: >8.2f} ± {:.2f}\n".format(hedge_units_mean, hedge_units_stderr) 237 | hedge_cost = market_result['hedge_cost'] 238 | hedge_cost_mean = hedge_cost.mean() 239 | hedge_cost_stderr = hedge_cost.std() / sqrt_path_count 240 | net_hedge_cost += hedge_cost 241 | s += "Cash: {: >8.2f} ± {:.2f}\n".format(-hedge_cost_mean, 3 * hedge_cost_stderr) 242 | if len(dates) > 1: 243 | market_name = market_result['market_name'] 244 | net_hedge_units[market_name] += hedge_units 245 | 246 | s += '\n' 247 | 248 | for commodity in sorted(net_hedge_units.keys()): 249 | units = net_hedge_units[commodity] 250 | s += "Net hedge {:6} {: >8.2f} ± {: >.2f}\n".format( 251 | commodity + ':', units.mean(), 3 * units.std() / sqrt_path_count) 252 | 253 | net_hedge_cost_mean = net_hedge_cost.mean() 254 | net_hedge_cost_stderr = net_hedge_cost.std() / sqrt_path_count 255 | 256 | s += "Net hedge cash: {: >8.2f} ± {: >.2f}\n".format(-net_hedge_cost_mean, 3 * net_hedge_cost_stderr) 257 | s += '\n' 258 | s += "Fair value: {:.2f} ± {:.2f}\n".format(fair_value_mean, 3 * fair_value_stderr) 259 | return s -------------------------------------------------------------------------------- /quantdsl/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/lib/__init__.py -------------------------------------------------------------------------------- /quantdsl/lib/american1.py: -------------------------------------------------------------------------------- 1 | from quantdsl.lib.option1 import Option 2 | 3 | 4 | def American(start, end, strike, underlying, step): 5 | if start <= end: 6 | Option( 7 | start, 8 | strike, 9 | underlying, 10 | American(start + step, end, strike, underlying, step) 11 | ) 12 | else: 13 | 0 14 | -------------------------------------------------------------------------------- /quantdsl/lib/european1.py: -------------------------------------------------------------------------------- 1 | from quantdsl.lib.option1 import Option 2 | 3 | def European(date, strike, underlying): 4 | return Option(date, strike, underlying, 0) 5 | -------------------------------------------------------------------------------- /quantdsl/lib/option1.py: -------------------------------------------------------------------------------- 1 | from quantdsl.semantics import Wait, Choice 2 | 3 | 4 | def Option(date, strike, underlying, alternative): 5 | return Wait(date, Choice(underlying - strike, alternative)) 6 | -------------------------------------------------------------------------------- /quantdsl/lib/powerplant1.py: -------------------------------------------------------------------------------- 1 | from quantdsl.semantics import Add, Choice, Fixing, Market, Min, Mult, Wait, inline 2 | 3 | 4 | def PowerPlant(start, end, commodity, cold, step): 5 | if (start < end): 6 | Wait(start, Choice( 7 | Add( 8 | PowerPlant(start + step, end, commodity, Running(), step), 9 | ProfitFromRunning(start, commodity, cold) 10 | ), 11 | PowerPlant(start + step, end, commodity, Stopped(cold), step), 12 | )) 13 | else: 14 | return 0 15 | 16 | 17 | @inline 18 | def Running(): 19 | return 0 20 | 21 | 22 | @inline 23 | def Stopped(cold): 24 | return Min(2, cold + 1) 25 | 26 | 27 | @inline 28 | def ProfitFromRunning(start, commodity, cold): 29 | return Mult((1 - cold / 10), Fixing(start, Burn(commodity))) 30 | 31 | 32 | @inline 33 | def Burn(commodity): 34 | return Market(commodity) 35 | -------------------------------------------------------------------------------- /quantdsl/lib/powerplant2.py: -------------------------------------------------------------------------------- 1 | from quantdsl.semantics import Choice, TimeDelta, Wait, inline, ForwardMarket 2 | 3 | 4 | def PowerPlant(start, end, temp): 5 | if (start < end): 6 | Wait(start, Choice( 7 | PowerPlant(Tomorrow(start), end, Hot()) + ProfitFromRunning(start, temp), 8 | PowerPlant(Tomorrow(start), end, Stopped(temp)) 9 | )) 10 | else: 11 | return 0 12 | 13 | @inline 14 | def Power(start): 15 | DayAhead(start, 'POWER') 16 | 17 | @inline 18 | def Gas(start): 19 | DayAhead(start, 'GAS') 20 | 21 | @inline 22 | def DayAhead(start, name): 23 | ForwardMarket(Tomorrow(start), name) 24 | 25 | @inline 26 | def ProfitFromRunning(start, temp): 27 | if temp == Cold(): 28 | return 0.3 * Power(start) - Gas(start) 29 | elif temp == Warm(): 30 | return 0.6 * Power(start) - Gas(start) 31 | else: 32 | return Power(start) - Gas(start) 33 | 34 | @inline 35 | def Stopped(temp): 36 | if temp == Hot(): 37 | Warm() 38 | else: 39 | Cold() 40 | 41 | @inline 42 | def Hot(): 43 | 2 44 | 45 | @inline 46 | def Warm(): 47 | 1 48 | 49 | @inline 50 | def Cold(): 51 | 0 52 | 53 | @inline 54 | def Tomorrow(today): 55 | today + TimeDelta('1d') 56 | -------------------------------------------------------------------------------- /quantdsl/lib/storage1.py: -------------------------------------------------------------------------------- 1 | from quantdsl.semantics import Wait, Choice, inline, Settlement, ForwardMarket 2 | 3 | 4 | def GasStorage(start, end, commodity_name, quantity, limit, step): 5 | if ((start < end) and (limit > 0)): 6 | if quantity <= 0: 7 | return Wait(start, Choice( 8 | Continue(start, end, commodity_name, quantity, limit, step), 9 | Inject(start, end, commodity_name, quantity, limit, step, 1), 10 | )) 11 | elif quantity < limit: 12 | return Wait(start, Choice( 13 | Continue(start, end, commodity_name, quantity, limit, step), 14 | Inject(start, end, commodity_name, quantity, limit, step, -1), 15 | )) 16 | else: 17 | return Wait(start, Choice( 18 | Continue(start, end, commodity_name, quantity, limit, step), 19 | Inject(start, end, commodity_name, quantity, limit, step, 1), 20 | Inject(start, end, commodity_name, quantity, limit, step, -1), 21 | )) 22 | else: 23 | return 0 24 | 25 | 26 | @inline 27 | def Continue(start, end, commodity_name, quantity, limit, step): 28 | GasStorage(start + step, end, commodity_name, quantity, limit, step) 29 | 30 | 31 | @inline 32 | def Continue(start, end, commodity_name, quantity, limit, step): 33 | GasStorage(start + step, end, commodity_name, quantity, limit, step) 34 | 35 | 36 | @inline 37 | def Inject(start, end, commodity_name, quantity, limit, step, vol): 38 | Continue(start, end, commodity_name, quantity + vol, limit, step) - \ 39 | Settlement(start, vol * ForwardMarket(start, commodity_name)) 40 | -------------------------------------------------------------------------------- /quantdsl/lib/storage2.py: -------------------------------------------------------------------------------- 1 | from quantdsl.semantics import Choice, Market, Wait, inline 2 | 3 | 4 | def GasStorage(start, end, commodity_name, quantity, target, limit, step, slew): 5 | if ((start < end) and (limit > 0)): 6 | if quantity <= 0: 7 | return Wait(start, Choice( 8 | Continue(start, end, commodity_name, quantity, target, limit, step, slew), 9 | Inject(start, end, commodity_name, quantity, target, limit, step, slew, slew), 10 | )) 11 | elif quantity >= limit: 12 | return Wait(start, Choice( 13 | Continue(start, end, commodity_name, quantity, target, limit, step, slew), 14 | Inject(start, end, commodity_name, quantity, target, limit, step, -slew, slew), 15 | )) 16 | else: 17 | return Wait(start, Choice( 18 | Continue(start, end, commodity_name, quantity, target, limit, step, slew), 19 | Inject(start, end, commodity_name, quantity, target, limit, step, slew, slew), 20 | Inject(start, end, commodity_name, quantity, target, limit, step, -slew, slew), 21 | )) 22 | else: 23 | if target < 0 or target == quantity: 24 | 0 25 | else: 26 | BreachOfContract() 27 | 28 | 29 | @inline 30 | def BreachOfContract(): 31 | -10000000000000000 32 | 33 | 34 | @inline 35 | def Continue(start, end, commodity_name, quantity, target, limit, step, slew): 36 | GasStorage(start + step, end, commodity_name, quantity, target, limit, step, slew) 37 | 38 | 39 | @inline 40 | def Inject(start, end, commodity_name, quantity, target, limit, step, vol, slew): 41 | Continue(start, end, commodity_name, quantity + vol, target, limit, step, slew) - \ 42 | vol * Market(commodity_name) 43 | -------------------------------------------------------------------------------- /quantdsl/priceprocess/__init__.py: -------------------------------------------------------------------------------- 1 | ## Market dynamics. 2 | 3 | -------------------------------------------------------------------------------- /quantdsl/priceprocess/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import datetime 4 | from abc import ABCMeta, abstractmethod 5 | import six 6 | from dateutil.relativedelta import relativedelta 7 | 8 | DAYS_PER_YEAR = 365 9 | SECONDS_PER_DAY = 86400 10 | 11 | 12 | class PriceProcess(six.with_metaclass(ABCMeta)): 13 | 14 | @abstractmethod 15 | def simulate_future_prices(self, observation_date, requirements, path_count, calibration_params): 16 | """ 17 | Returns a generator that yields a sequence of simulated prices. 18 | """ 19 | 20 | def get_commodity_names_and_fixing_dates(self, observation_date, requirements): 21 | # Get an ordered list of all the commodity names and fixing dates. 22 | commodity_names = sorted(set([r[0] for r in requirements])) 23 | observation_date = datetime_from_date(observation_date) 24 | 25 | requirement_datetimes = [datetime_from_date(r[1]) for r in requirements] 26 | 27 | fixing_dates = sorted(set([observation_date] + requirement_datetimes)) 28 | return commodity_names, fixing_dates 29 | 30 | 31 | def get_duration_years(start_date, end_date, days_per_year=DAYS_PER_YEAR): 32 | assert isinstance(start_date, datetime.date), type(start_date) 33 | assert isinstance(end_date, datetime.date), type(end_date) 34 | r = relativedelta(end_date, start_date) 35 | return r.years + r.months / 12.0 + (r.days + r.hours / 24) / float(days_per_year) 36 | 37 | 38 | def datetime_from_date(date): 39 | return datetime.datetime(date.year, date.month, date.day) 40 | -------------------------------------------------------------------------------- /quantdsl/priceprocess/blackscholes.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | import datetime 4 | from collections import defaultdict 5 | 6 | import numpy 7 | import numpy as np 8 | import scipy 9 | import scipy.linalg 10 | from dateutil.relativedelta import relativedelta 11 | from scipy.linalg import LinAlgError 12 | 13 | from quantdsl.exceptions import DslError 14 | from quantdsl.priceprocess.base import PriceProcess, get_duration_years 15 | from quantdsl.priceprocess.common import get_historical_data 16 | from quantdsl.priceprocess.forwardcurve import ForwardCurve 17 | 18 | 19 | class BlackScholesPriceProcess(PriceProcess): 20 | def simulate_future_prices(self, observation_date, requirements, path_count, calibration_params): 21 | # Compute correlated Brownian motions for each market. 22 | 23 | if not requirements: 24 | return 25 | 26 | all_brownian_motions = self.get_brownian_motions(observation_date, requirements, path_count, 27 | calibration_params) 28 | 29 | delivery_dates = defaultdict(set) 30 | for requirement in requirements: 31 | fixing_date = requirement[1] 32 | delivery_date = requirement[2] 33 | delivery_dates[fixing_date].add(delivery_date) 34 | 35 | # delivery_dates[observation_date].add(observation_date) 36 | 37 | # Compute simulated market prices using the correlated Brownian 38 | # motions, the actual historical volatility, and the last price. 39 | for commodity_name, brownian_motions in all_brownian_motions: 40 | # Get the 'last price' for this commodity. 41 | 42 | index = calibration_params['market'].index(commodity_name) 43 | sigma = calibration_params['sigma'][index] 44 | curve = ForwardCurve(commodity_name, calibration_params['curve'][commodity_name]) 45 | for fixing_date, brownian_rv in brownian_motions: 46 | for delivery_date in sorted(delivery_dates[fixing_date]): 47 | forward_price = curve.get_price(delivery_date) 48 | T = get_duration_years(observation_date, fixing_date) 49 | simulated_value = forward_price * scipy.exp(sigma * brownian_rv - 0.5 * sigma * sigma * T) 50 | yield commodity_name, fixing_date, delivery_date, simulated_value 51 | 52 | def get_brownian_motions(self, observation_date, requirements, path_count, calibration_params): 53 | assert isinstance(observation_date, datetime.datetime), observation_date 54 | assert isinstance(requirements, list), requirements 55 | assert isinstance(path_count, int), path_count 56 | 57 | commodity_names, fixing_dates = self.get_commodity_names_and_fixing_dates(observation_date, requirements) 58 | 59 | len_commodity_names = len(commodity_names) 60 | 61 | len_fixing_dates = len(fixing_dates) 62 | 63 | # Check the observation date equals the first fixing date. 64 | assert observation_date == fixing_dates[0], "Observation date {} not equal to first fixing date: {}" \ 65 | "".format(observation_date, fixing_dates[0]) 66 | 67 | # Diffuse random variables through each date for each market (uncorrelated increments). 68 | brownian_motions = scipy.zeros((len_commodity_names, len_fixing_dates, path_count)) 69 | all_brownian_motions = [] 70 | 71 | if len_fixing_dates and len_commodity_names: 72 | for i in range(len_commodity_names): 73 | _start_date = fixing_dates[0] 74 | start_rv = brownian_motions[i][0] 75 | for j in range(len_fixing_dates - 1): 76 | fixing_date = fixing_dates[j + 1] 77 | draws = scipy.random.standard_normal(path_count) 78 | T = get_duration_years(_start_date, fixing_date) 79 | if T < 0: 80 | raise DslError( 81 | "Can't really square root negative time durations: %s. Contract starts before " 82 | "observation time?" % T) 83 | end_rv = start_rv + scipy.sqrt(T) * draws 84 | try: 85 | brownian_motions[i][j + 1] = end_rv 86 | except ValueError as e: 87 | raise ValueError("Can't set end_rv in brownian_motions: %s" % e) 88 | _start_date = fixing_date 89 | start_rv = end_rv 90 | 91 | if len_commodity_names > 1: 92 | correlation_matrix = scipy.zeros((len_commodity_names, len_commodity_names)) 93 | for i in range(len_commodity_names): 94 | for j in range(len_commodity_names): 95 | 96 | # Get the correlation between market i and market j... 97 | name_i = commodity_names[i] 98 | name_j = commodity_names[j] 99 | if name_i == name_j: 100 | # - they are identical 101 | correlation = 1 102 | else: 103 | # - correlation is expected to be in the "calibration" data 104 | correlation = self.get_correlation_from_calibration(calibration_params, name_i, name_j) 105 | 106 | # ...and put the correlation in the correlation matrix. 107 | correlation_matrix[i][j] = correlation 108 | 109 | # Compute lower triangular matrix, using Cholesky decomposition. 110 | try: 111 | U = scipy.linalg.cholesky(correlation_matrix) 112 | except LinAlgError as e: 113 | msg = "Cholesky decomposition failed with correlation matrix: %s: %s" % (correlation_matrix, e) 114 | raise DslError(msg) 115 | 116 | # Construct correlated increments from uncorrelated increments 117 | # and lower triangular matrix for the correlation matrix. 118 | try: 119 | # Put markets on the last axis, so the broadcasting works, before computing 120 | # the dot product with the lower triangular matrix of the correlation matrix. 121 | brownian_motions_correlated = brownian_motions.T.dot(U) 122 | except Exception as e: 123 | msg = ("Couldn't multiply uncorrelated Brownian increments with decomposed correlation matrix: " 124 | "%s, %s: %s" % (brownian_motions, U, e)) 125 | raise DslError(msg) 126 | 127 | # Put markets back on the first dimension. 128 | brownian_motions_correlated = brownian_motions_correlated.transpose() 129 | brownian_motions = brownian_motions_correlated 130 | 131 | # Put random variables into a nested Python dict, keyed by market commodity_name and fixing date. 132 | for i, commodity_name in enumerate(commodity_names): 133 | market_rvs = [] 134 | for j, fixing_date in enumerate(fixing_dates): 135 | rv = brownian_motions[i][j] 136 | market_rvs.append((fixing_date, rv)) 137 | all_brownian_motions.append((commodity_name, market_rvs)) 138 | 139 | return all_brownian_motions 140 | 141 | def get_correlation_from_calibration(self, market_calibration, name_i, name_j): 142 | index_i = market_calibration['market'].index(name_i) 143 | index_j = market_calibration['market'].index(name_j) 144 | try: 145 | correlation = market_calibration['rho'][index_i][index_j] 146 | except KeyError as e: 147 | msg = "Can't find correlation between '%s' and '%s' in market calibration params: %s: %s" % ( 148 | name_i, 149 | name_j, 150 | market_calibration, 151 | e 152 | ) 153 | raise DslError(msg) 154 | else: 155 | assert isinstance(correlation, (float, int)), correlation 156 | return correlation 157 | 158 | 159 | def generate_calibration_params(start, end, markets, get_historical_data=get_historical_data): 160 | name = 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess' 161 | all_quotes = [] 162 | sigmas = [] 163 | date = start 164 | all_market_names = [] 165 | # Iterate over all "markets" (e.g. 'GAS'). 166 | all_curves = {} 167 | for market_name, market_spec in markets.items(): 168 | all_market_names.append(market_name) 169 | forward_vols = [] 170 | forward_curve = [] 171 | while date <= end: 172 | forward_year = date.year 173 | forward_month = date.month 174 | kwargs = market_spec.copy() 175 | kwargs['sym'] += quandl_month_codes[forward_month] + str(forward_year) 176 | 177 | # Get the data. 178 | quotes = get_historical_data(**kwargs) 179 | 180 | all_quotes.append(quotes) 181 | vol = calc_historical_volatility(quotes) 182 | forward_vols.append(vol) 183 | last = pick_last_price(quotes) 184 | forward_curve.append((date, last)) 185 | 186 | # Next month. 187 | date = date + relativedelta(months=1) 188 | sigma = np.median(forward_vols) 189 | sigmas.append(sigma) 190 | all_curves[market_name] = forward_curve 191 | # Todo: Align dates and drop rows that aren't full. 192 | rho = calc_correlation(all_quotes) 193 | return { 194 | 'name': name, 195 | 'market': all_market_names, 196 | 'sigma': sigmas, 197 | 'rho': rho, 198 | 'curve': all_curves, 199 | } 200 | 201 | 202 | def calc_historical_volatility(quotes): 203 | logreturns = np.log(quotes / quotes.shift(1)) 204 | return np.sqrt(252 * logreturns.var()) 205 | 206 | 207 | def pick_last_price(quotes): 208 | if len(quotes): 209 | return quotes[-1] 210 | else: 211 | return None 212 | 213 | 214 | def calc_correlation(*args): 215 | if len(args) == 1: 216 | return numpy.array([[1]]) 217 | else: 218 | raise NotImplementedError('Need to align dates. Also, which time series?') 219 | return numpy.corrcoef(numpy.array(args)) 220 | 221 | 222 | quandl_month_codes = { 223 | 1: 'F', 224 | 2: 'G', 225 | 3: 'H', 226 | 4: 'J', 227 | 5: 'K', 228 | 6: 'M', 229 | 7: 'N', 230 | 8: 'Q', 231 | 9: 'U', 232 | 10: 'V', 233 | 11: 'X', 234 | 12: 'Z', 235 | } 236 | 237 | 238 | QuandlProducts = { 239 | 'Endex Dutch TTF Gas Base Load Futures': 'ICE/TFM', 240 | 'Endex Belgian Power Base Load Futures': 'ICE/BPB', 241 | 'UK Natural Gas Futures': 'ICE/M' 242 | } 243 | -------------------------------------------------------------------------------- /quantdsl/priceprocess/common.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import six 4 | from dateutil.relativedelta import relativedelta 5 | from pandas import Series 6 | from pandas_datareader import DataReader 7 | 8 | 9 | def get_historical_data(service, sym, days=30, start=None, end=None, col=None, limit=None): 10 | if end is None: 11 | end = datetime.datetime.now() 12 | if start is None: 13 | start = end - relativedelta(days=days) 14 | df = DataReader(sym, service, start=start, end=end) 15 | if col is not None: 16 | df = df[col] 17 | if limit is not None: 18 | df = df[-limit:] 19 | return df 20 | 21 | 22 | def from_csvtext(csvtext, cls=Series): 23 | return cls.from_csv(six.StringIO(csvtext)) 24 | 25 | 26 | def to_csvtext(ndframe): 27 | f = six.StringIO() 28 | ndframe.to_csv(f) 29 | f.seek(0) 30 | return f.read() -------------------------------------------------------------------------------- /quantdsl/priceprocess/forwardcurve.py: -------------------------------------------------------------------------------- 1 | import dateutil.parser 2 | import six 3 | from scipy import sort, array, searchsorted 4 | 5 | from quantdsl.priceprocess.base import datetime_from_date 6 | 7 | 8 | class ForwardCurve(object): 9 | def __init__(self, name, data): 10 | assert isinstance(name, six.string_types) 11 | assert isinstance(data, (list, tuple)) 12 | self.name = name 13 | self.data = data 14 | self.by_date = dict( 15 | [(datetime_from_date(dateutil.parser.parse(d)), v) for (d, v) in self.data] 16 | ) 17 | self.sorted = sort(array(list(self.by_date.keys()))) 18 | 19 | def get_price(self, date): 20 | try: 21 | price = self.by_date[date] 22 | except: 23 | # Search for earlier date. 24 | index = searchsorted(self.sorted, date) - 1 25 | if index < 0: 26 | raise KeyError("Delivery date {} not found in '{}' forward curve.".format(date, self.name)) 27 | price = self.by_date[self.sorted[index]] 28 | return price -------------------------------------------------------------------------------- /quantdsl/syntax.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | import six 4 | 5 | from quantdsl.exceptions import DslSyntaxError 6 | from quantdsl.semantics import FunctionDef, DslNamespace 7 | 8 | if six.PY2: 9 | from importlib import import_module 10 | elif six.PY3: 11 | find_spec = None 12 | find_loader = None 13 | try: 14 | from importlib.util import find_spec 15 | except: 16 | from importlib.util import find_loader 17 | 18 | 19 | def find_module_path(name): 20 | # Find path. 21 | 22 | if six.PY2: 23 | try: 24 | module = import_module(name) 25 | except SyntaxError as e: 26 | raise DslSyntaxError("Can't import {}: {}".format(name, e)) 27 | path = module.__file__.strip('c') 28 | elif six.PY3: 29 | if find_loader: 30 | loader = find_loader(name) 31 | path = loader.path 32 | else: 33 | spec = find_spec(name) 34 | path = spec.origin 35 | 36 | assert path.endswith('.py'), path 37 | return path 38 | 39 | 40 | class DslParser(object): 41 | 42 | def __init__(self, dsl_classes=None): 43 | if dsl_classes is None: 44 | dsl_classes = {} 45 | self.dsl_classes = dsl_classes 46 | 47 | def parse(self, dsl_source, filename=''): 48 | """ 49 | Creates a DSL Module object from a DSL source text. 50 | """ 51 | if not isinstance(dsl_source, six.string_types): 52 | raise DslSyntaxError("Can't dsl_parse non-string object", dsl_source) 53 | 54 | # assert isinstance(dsl_source, six.string_types) 55 | try: 56 | # Parse as Python source code, into a Python abstract syntax tree. 57 | ast_module = ast.parse(dsl_source, filename=filename, mode='exec') 58 | except SyntaxError as e: 59 | raise DslSyntaxError("DSL source code is not valid Python code: {}".format(dsl_source), e) 60 | 61 | # Generate Quant DSL from Python AST. 62 | return self.visitAstNode(ast_module) 63 | 64 | def visitAstNode(self, node): 65 | """ 66 | Identifies which "visit" method to call, according to type of node being visited. 67 | 68 | Returns the result of calling the identified "visit" method. 69 | """ 70 | # assert isinstance(node, ast.AST) 71 | 72 | # Construct the "visit" method name. 73 | dsl_element_name = node.__class__.__name__ 74 | method_name = 'visit' + dsl_element_name 75 | 76 | # Try to get_quantdsl_app the "visit" method object. 77 | try: 78 | method = getattr(self, method_name) 79 | except AttributeError: 80 | msg = "element '%s' is not supported (visit method '%s' not found on parser): %s" % ( 81 | dsl_element_name, method_name, node) 82 | raise DslSyntaxError(msg) 83 | 84 | # Call the "visit" method object, and return the result of visiting the node. 85 | return method(node=node) 86 | 87 | def visitModule(self, node): 88 | """ 89 | Visitor method for ast.Module nodes. 90 | 91 | Returns a DSL Module, with a list of DSL expressions as the body. 92 | """ 93 | # assert isinstance(node, ast.Module) 94 | body = [] 95 | 96 | # Namespace for function defs in module. 97 | module_namespace = DslNamespace() 98 | 99 | for n in node.body: 100 | dsl_object = self.visitAstNode(n) 101 | 102 | # Put function defs in module namespace. 103 | if isinstance(dsl_object, FunctionDef): 104 | module_namespace[dsl_object.name] = dsl_object 105 | 106 | # Share module namespace with this function. 107 | if dsl_object.module_namespace is None: 108 | dsl_object.module_namespace = module_namespace 109 | 110 | # Include imported things. 111 | if isinstance(dsl_object, list): 112 | for _dsl_object in dsl_object: 113 | if isinstance(_dsl_object, FunctionDef): 114 | module_namespace[_dsl_object.name] = _dsl_object 115 | else: 116 | body.append(dsl_object) 117 | 118 | return self.dsl_classes['Module'](body, module_namespace, node=node) 119 | 120 | def visitImportFrom(self, node): 121 | """ 122 | Visitor method for ast.ImportFrom nodes. 123 | 124 | Returns the result of visiting the expression held by the return statement. 125 | """ 126 | assert isinstance(node, ast.ImportFrom) 127 | if node.module == 'quantdsl.semantics': 128 | return [] 129 | from_names = [a.name for a in node.names] 130 | dsl_module = self.import_dsl_module(node.module) 131 | nodes = [] 132 | for node in dsl_module.body: 133 | if isinstance(node, FunctionDef) and node.name in from_names: 134 | nodes.append(node) 135 | return nodes 136 | 137 | def import_dsl_module(self, name): 138 | path = find_module_path(name) 139 | with open(path) as f: 140 | source = f.read() 141 | dsl_module = self.parse(source, filename=path) 142 | assert isinstance(dsl_module, self.dsl_classes['Module']), type(dsl_module) 143 | return dsl_module 144 | 145 | def visitReturn(self, node): 146 | """ 147 | Visitor method for ast.Return nodes. 148 | 149 | Returns the result of visiting the expression held by the return statement. 150 | """ 151 | # assert isinstance(node, ast.Return) 152 | return self.visitAstNode(node.value) 153 | 154 | def visitExpr(self, node): 155 | """ 156 | Visitor method for ast.Expr nodes. 157 | 158 | Returns the result of visiting the contents of the expression node. 159 | """ 160 | # assert isinstance(node, ast.Expr) 161 | assert isinstance(node.value, ast.AST), type(node.value) 162 | return self.visitAstNode(node.value) 163 | 164 | def visitNum(self, node): 165 | """ 166 | Visitor method for ast.Name. 167 | 168 | Returns a DSL Number object, with the number value. 169 | """ 170 | # assert isinstance(node, ast.Num) 171 | return self.dsl_classes['Number'](node.n, node=node) 172 | 173 | def visitStr(self, node): 174 | """ 175 | Visitor method for ast.Str. 176 | 177 | Returns a DSL String object, with the string value. 178 | """ 179 | # assert isinstance(node, ast.Str) 180 | return self.dsl_classes['String'](node.s, node=node) 181 | 182 | def visitUnaryOp(self, node): 183 | """ 184 | Visitor method for ast.UnaryOp. 185 | 186 | Returns a specific DSL UnaryOp object (e.g UnarySub), along with the operand. 187 | """ 188 | # assert isinstance(node, ast.UnaryOp) 189 | args = [self.visitAstNode(node.operand)] 190 | if isinstance(node.op, ast.USub): 191 | dsl_class = self.dsl_classes['UnarySub'] 192 | else: 193 | raise DslSyntaxError("Unsupported unary operator token: %s" % node.op) 194 | return dsl_class(node=node, *args) 195 | 196 | def visitBinOp(self, node): 197 | """ 198 | Visitor method for ast.BinOp. 199 | 200 | Returns a specific DSL BinOp object (e.g Add), along with the left and right operands. 201 | """ 202 | # assert isinstance(node, ast.BinOp) 203 | type_map = { 204 | ast.Add: self.dsl_classes['Add'], 205 | ast.Sub: self.dsl_classes['Sub'], 206 | ast.Mult: self.dsl_classes['Mult'], 207 | ast.Div: self.dsl_classes['Div'], 208 | ast.Pow: self.dsl_classes['Pow'], 209 | ast.Mod: self.dsl_classes['Mod'], 210 | ast.FloorDiv: self.dsl_classes['FloorDiv'], 211 | } 212 | try: 213 | dsl_class = type_map[type(node.op)] 214 | except KeyError: 215 | raise DslSyntaxError("Unsupported binary operator token", node.op, node=node) 216 | args = [self.visitAstNode(node.left), self.visitAstNode(node.right)] 217 | return dsl_class(node=node, *args) 218 | 219 | def visitBoolOp(self, node): 220 | """ 221 | Visitor method for ast.BoolOp. 222 | 223 | Returns a specific DSL BoolOp object (e.g And), along with the left and right operands. 224 | """ 225 | # assert isinstance(node, ast.BoolOp) 226 | type_map = { 227 | ast.And: self.dsl_classes['And'], 228 | ast.Or: self.dsl_classes['Or'], 229 | } 230 | dsl_class = type_map[type(node.op)] 231 | values = [self.visitAstNode(v) for v in node.values] 232 | args = [values] 233 | return dsl_class(node=node, *args) 234 | 235 | def visitName(self, node): 236 | """ 237 | Visitor method for ast.Name. 238 | 239 | Returns a DSL Name object, along with the name's string. 240 | """ 241 | return self.dsl_classes['Name'](node.id, node=node) 242 | 243 | def visitCall(self, node): 244 | """ 245 | Visitor method for ast.Call. 246 | 247 | Returns a built-in DSL expression, or a DSL FunctionCall if the name refers to a user 248 | defined function. 249 | """ 250 | if node.keywords: 251 | raise DslSyntaxError("Calling with keywords is not currently supported (positional args only).") 252 | if hasattr(node, 'starargs') and node.starargs: 253 | raise DslSyntaxError("Calling with starargs is not currently supported (positional args only).") 254 | if hasattr(node, 'kwargs') and node.kwargs: 255 | raise DslSyntaxError("Calling with kwargs is not currently supported (positional args only).") 256 | 257 | # Collect the call arg expressions (whose values will be passed into the call when it is made). 258 | call_arg_exprs = [self.visitAstNode(arg) for arg in node.args] 259 | 260 | # Check the called node is an ast.Name. 261 | called_node = node.func 262 | # assert isinstance(called_node, ast.Name) 263 | called_node_name = called_node.id 264 | 265 | # Construct a DSL object for this call. 266 | try: 267 | # Resolve the name with a new instance of a DSL class. 268 | dsl_class = self.dsl_classes[called_node_name] 269 | except KeyError: 270 | # Resolve as a FunctionCall, and expect 271 | # to resolve the name to a function def later. 272 | dsl_name_class = self.dsl_classes['Name'] 273 | dsl_args = [dsl_name_class(called_node_name, node=called_node), call_arg_exprs] 274 | return self.dsl_classes['FunctionCall'](node=node, *dsl_args) 275 | else: 276 | dsl_object_class = self.dsl_classes['DslObject'] 277 | assert issubclass(dsl_class, dsl_object_class), dsl_class 278 | return dsl_class(node=node, *call_arg_exprs) 279 | 280 | def visitFunctionDef(self, node): 281 | """ 282 | Visitor method for ast.FunctionDef. 283 | 284 | Returns a named DSL FunctionDef, with a definition of the expected call argument values. 285 | """ 286 | name = node.name 287 | dsl_function_arg_class = self.dsl_classes['FunctionArg'] 288 | if six.PY2: 289 | arg_name_attr = 'id' 290 | else: 291 | arg_name_attr = 'arg' 292 | call_arg_defs = [dsl_function_arg_class(getattr(arg, arg_name_attr), '') for arg in node.args.args] 293 | assert len(node.body) == 1, "Function defs with more than one body statement are not supported at the moment." 294 | decorator_names = [ast_name.id for ast_name in node.decorator_list] 295 | body = self.visitAstNode(node.body[0]) 296 | dsl_args = [name, call_arg_defs, body, decorator_names] 297 | function_def = self.dsl_classes['FunctionDef'](node=node, *dsl_args) 298 | return function_def 299 | 300 | def visitIfExp(self, node): 301 | """ 302 | Visitor method for ast.IfExp. 303 | 304 | Returns a named DSL IfExp, with a test DSL expression and expressions whose usage is 305 | conditional upon the test. 306 | """ 307 | test = self.visitAstNode(node.test) 308 | body = self.visitAstNode(node.body) 309 | orelse = self.visitAstNode(node.orelse) 310 | args = [test, body, orelse] 311 | return self.dsl_classes['IfExp'](node=node, *args) 312 | 313 | def visitIf(self, node): 314 | """ 315 | Visitor method for ast.If. 316 | 317 | Returns a named DSL If object, with a test DSL expression and expressions whose usage is 318 | conditional upon the test. 319 | """ 320 | test = self.visitAstNode(node.test) 321 | assert len(node.body) == 1, "If statement body must have exactly one statement" 322 | body = self.visitAstNode(node.body[0]) 323 | assert len(node.orelse) == 1, "If statement must have exactly one orelse statement" 324 | orelse = self.visitAstNode(node.orelse[0]) 325 | args = [test, body, orelse] 326 | return self.dsl_classes['If'](node=node, *args) 327 | 328 | def visitCompare(self, node): 329 | """ 330 | Visitor method for ast.Compare. 331 | 332 | Returns a named DSL Compare object, with operators (ops) and operands (comparators). 333 | """ 334 | 335 | left = self.visitAstNode(node.left) 336 | op_names = [o.__class__.__name__ for o in node.ops] 337 | comparators = [self.visitAstNode(c) for c in node.comparators] 338 | args = [left, op_names, comparators] 339 | return self.dsl_classes['Compare'](node=node, *args) 340 | -------------------------------------------------------------------------------- /quantdsl/tests/_____test.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | 3 | 4 | # Todo: Review this module, and reestablish tests that have not already been reproduced. 5 | import datetime 6 | import sys 7 | import unittest 8 | 9 | from quantdsl.priceprocess.blackscholes import BlackScholesPriceProcess 10 | from quantdsl.semantics import DslExpression, DslNamespace 11 | from quantdsl.tests.test_parser import dsl_eval, dsl_compile 12 | 13 | 14 | def suite(): 15 | return unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) 16 | 17 | 18 | # class MockImage(object): 19 | # 20 | # def __init__(self, price_process): 21 | # self.price_process = price_process 22 | # 23 | # 24 | # class MockPriceProcess(object): pass 25 | 26 | 27 | class DslTestCase(unittest.TestCase): 28 | 29 | def assertValuation(self, dsl_source=None, expected_value=None, expected_delta=None, expected_gamma=None, 30 | tolerance_value=0.05, tolerance_delta = 0.1, tolerance_gamma=0.1): 31 | 32 | # Check option value. 33 | observation_date = datetime.date(2011, 1, 1) 34 | estimated_value = self.calc_value(dsl_source, observation_date)['mean'] 35 | self.assertTolerance(estimated_value, expected_value, tolerance_value) 36 | 37 | # Todo: Reinstate the delta tests. 38 | return 39 | # Check deltas. 40 | markets = self.pricer.get_markets() 41 | if not markets: 42 | assert self.expected_delta == None 43 | return 44 | market = list(markets)[0] 45 | # Check option delta. 46 | estimated_delta = self.pricer.calc_delta(market) 47 | self.assertTolerance(estimated_delta, expected_delta, tolerance_delta) 48 | 49 | # Todo: Decide what to do with gamma (too much noise to pass tests consistently at the mo). Double-side differentials? 50 | # Check option gamma. 51 | #estimatedGamma = self.pricer.calcGamma(market) 52 | #roundedGamma = round(estimatedGamma, self.DECIMALS) 53 | #expected_gamma = round(self.expected_gamma, self.DECIMALS) 54 | #msg = "Value: %s Expected: %s" % (roundedGamma, expected_gamma) 55 | #self.assertEqual(roundedGamma, expected_gamma, msg) 56 | 57 | def assertTolerance(self, estimated, expected, tolerance): 58 | upper = expected + tolerance 59 | lower = expected - tolerance 60 | assert lower <= estimated <= upper, "Estimated '%s' not close enough to expected '%s' (tolerance '%s')." % (estimated, expected, tolerance) 61 | 62 | def calc_value(self, dsl_source, observation_date): 63 | # Todo: Rename 'allRvs' to 'simulatedPrices'? 64 | evaluation_kwds = DslNamespace({ 65 | 'observation_date': observation_date, 66 | 'interest_rate': '2.5', 67 | 'market_calibration': { 68 | '#1-LAST-PRICE': 10, 69 | '#1-ACTUAL-HISTORICAL-VOLATILITY': 50, 70 | '#2-LAST-PRICE': 10, 71 | '#2-ACTUAL-HISTORICAL-VOLATILITY': 50, 72 | '#1-#2-CORRELATION': 0.0, 73 | 'NBP-LAST-PRICE': 10, 74 | 'NBP-ACTUAL-HISTORICAL-VOLATILITY': 50, 75 | 'TTF-LAST-PRICE': 11, 76 | 'TTF-ACTUAL-HISTORICAL-VOLATILITY': 40, 77 | 'BRENT-LAST-PRICE': 90, 78 | 'BRENT-ACTUAL-HISTORICAL-VOLATILITY': 60, 79 | 'NBP-TTF-CORRELATION': 0.4, 80 | 'BRENT-TTF-CORRELATION': 0.5, 81 | 'BRENT-NBP-CORRELATION': 0.3, 82 | }, 83 | 'path_count': 200000, 84 | # 'simulated_price_repo': { 85 | # 'sim1#12011-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 86 | # 'sim1#12011-01-03': Mock(spec=SimulatedPrice, value=scipy.array([10])), 87 | # 'sim1#12011-06-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 88 | # 'sim1#12012-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 89 | # 'sim1#12012-01-02': Mock(spec=SimulatedPrice, value=scipy.array([10])), 90 | # 'sim1#12012-01-03': Mock(spec=SimulatedPrice, value=scipy.array([10])), 91 | # 'sim1#12012-06-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 92 | # 'sim1#12013-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 93 | # 94 | # 'sim1#22011-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 95 | # 'sim1#22012-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 96 | # 97 | # 'sim1TTF2012-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 98 | # 'sim1NBP2012-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 99 | # 'sim1TTF2013-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 100 | # 'sim1NBP2013-01-01': Mock(spec=SimulatedPrice, value=scipy.array([10])), 101 | # }, 102 | 'simulation_id': 'sim1', 103 | }) 104 | return dsl_eval(dsl_source, evaluation_kwds=evaluation_kwds) 105 | 106 | 107 | # dsl_expr = dsl_compile(dsl_source) 108 | # 109 | # evaluation_kwds = DslNamespace({ 110 | # 'observation_date': observation_date, 111 | # 'present_time': observation_date, 112 | # 'simulated_price_repo': {}, 113 | # 'interest_rate': '2.5', 114 | # 'calibration': { 115 | # '#1-LAST-PRICE': 10, 116 | # '#1-ACTUAL-HISTORICAL-VOLATILITY': 50, 117 | # '#2-LAST-PRICE': 10, 118 | # '#2-ACTUAL-HISTORICAL-VOLATILITY': 50, 119 | # }, 120 | # 'allRvs': BlackScholesPriceProcess().getAllRvs(dsl_expr, observation_date, path_count=100000), 121 | # }) 122 | # assert isinstance(dsl_expr, DslExpression) 123 | # value = dsl_expr.evaluate(**evaluation_kwds) 124 | # if hasattr(value, 'mean'): 125 | # value = value.mean() 126 | # return value 127 | # 128 | 129 | class TestDslMarket(DslTestCase): 130 | 131 | def testValuation(self): 132 | specification = "Market('#1')" 133 | self.assertValuation(specification, 10, 1, 0) 134 | 135 | # 136 | # class TestDslFixing(DslTestCase): 137 | # 138 | # def testValuation(self): 139 | # specification = "Fixing(Date('2012-01-01'), Market('#1'))" 140 | # self.assertValuation(specification, 10, 1, 0) 141 | # 142 | # 143 | # class TestDslWait(DslTestCase): 144 | # 145 | # def testValuation(self): 146 | # specification = "Wait(Date('2012-01-01'), Market('#1'))" 147 | # self.assertValuation(specification, 9.753, 0.975, 0) 148 | # 149 | # 150 | # class TestDslSettlement(DslTestCase): 151 | # 152 | # def testValuation(self): 153 | # specification = "Settlement(Date('2012-01-01'), Market('#1'))" 154 | # self.assertValuation(specification, 9.753, 0.975, 0) 155 | # 156 | # 157 | # class TestDslChoice(DslTestCase): 158 | # 159 | # def testValuation(self): 160 | # specification = "Fixing(Date('2012-01-01'), Choice( Market('#1') - 9, 0))" 161 | # self.assertValuation(specification, 2.416, 0.677, 0.07) 162 | # 163 | # 164 | # class TestDslMax(DslTestCase): 165 | # 166 | # def testValuation(self): 167 | # specification = "Fixing(Date('2012-01-01'), Max(Market('#1'), Market('#2')))" 168 | # self.assertValuation(specification, 12.766, 0.636, 0) 169 | # #self.assertValuation(specification, 11.320, 0.636, 0) 170 | # 171 | 172 | # class TestDslAdd(DslTestCase): 173 | # 174 | # def test_valuation(self): 175 | # specification = "10 + Market('#1')" 176 | # self.assertValuation(specification, 20, 1, 0) 177 | # 178 | # def test_valuation2(self): 179 | # specification = "10 + Market('#2')" 180 | # self.assertValuation(specification, 20, 1, 0) 181 | # 182 | # 183 | # class TestDslSubtract(DslTestCase): 184 | # 185 | # def testValuation(self): 186 | # specification = "Market('#1') - 10" 187 | # self.assertValuation(specification, 0, 1, 0) 188 | # 189 | # 190 | # class TestDslMultiply(DslTestCase): 191 | # 192 | # def testValuation(self): 193 | # specification = "Market('#1') * Market('#2')" 194 | # self.assertValuation(specification, 100, 10, 0) 195 | # 196 | # 197 | # class TestDslDivide(DslTestCase): 198 | # 199 | # def testValuation(self): 200 | # specification = "Market('#1') / 10" 201 | # self.assertValuation(specification, 1, 0.1, 0) 202 | # 203 | # 204 | # class TestDslIdenticalFixings(DslTestCase): 205 | # 206 | # def testValuation(self): 207 | # specification = """ 208 | # Fixing(Date('2012-01-01'), Market('#1')) - Fixing(Date('2012-01-01'), Market('#1')) 209 | # """ 210 | # self.assertValuation(specification, 0, 0, 0) 211 | # 212 | # 213 | # class TestDslBrownianIncrements(DslTestCase): 214 | # 215 | # def testValuation(self): 216 | # specification = """ 217 | # Wait( 218 | # Date('2012-03-15'), 219 | # Max( 220 | # Fixing( 221 | # Date('2012-01-01'), 222 | # Market('#1') 223 | # ) / 224 | # Fixing( 225 | # Date('2011-01-01'), 226 | # Market('#1') 227 | # ), 228 | # 1.0 229 | # ) - 230 | # Max( 231 | # Fixing( 232 | # Date('2013-01-01'), 233 | # Market('#1') 234 | # ) / 235 | # Fixing( 236 | # Date('2012-01-01'), 237 | # Market('#1') 238 | # ), 239 | # 1.0 240 | # ) 241 | # )""" 242 | # self.assertValuation(specification, 0, 0, 0) 243 | # 244 | # 245 | # class TestDslUncorrelatedMarkets(DslTestCase): 246 | # 247 | # def testValuation(self): 248 | # specification = """ 249 | # Max( 250 | # Fixing( 251 | # Date('2012-01-01'), 252 | # Market('#1') 253 | # ) * 254 | # Fixing( 255 | # Date('2012-01-01'), 256 | # Market('#2') 257 | # ) / 10.0, 258 | # 0.0 259 | # ) - Max( 260 | # Fixing( 261 | # Date('2013-01-01'), 262 | # Market('#1') 263 | # ), 0 264 | # )""" 265 | # self.assertValuation(specification, 0, 0, 0, 0.07, 0.2, 0.2) # Todo: Figure out why the delta sometimes evaluates to 1 for a period of time and then 266 | # 267 | # 268 | # class TestDslCorrelatedMarkets(DslTestCase): 269 | # 270 | # def testValuation(self): 271 | # specification = """ 272 | # Max( 273 | # Fixing( 274 | # Date('2012-01-01'), 275 | # Market('TTF') 276 | # ) * 277 | # Fixing( 278 | # Date('2012-01-01'), 279 | # Market('NBP') 280 | # ) / 10.0, 281 | # 0.0 282 | # ) - Max( 283 | # Fixing( 284 | # Date('2013-01-01'), 285 | # Market('TTF') 286 | # ), 0 287 | # )""" 288 | # self.assertValuation(specification, 0.92, 0, 0, 0.15, 0.2, 0.2) 289 | # 290 | # 291 | # class TestDslFutures(DslTestCase): 292 | # 293 | # def testValuation(self): 294 | # specification = """ 295 | # Wait( Date('2012-01-01'), 296 | # Market('#1') - 9 297 | # ) """ 298 | # self.assertValuation(specification, 0.9753, 0.9753, 0) 299 | # 300 | # 301 | # class TestDslEuropean(DslTestCase): 302 | # 303 | # def testValuation(self): 304 | # specification = "Wait(Date('2012-01-01'), Choice(Market('#1') - 9, 0))" 305 | # self.assertValuation(specification, 2.356, 0.660, 0.068) 306 | # 307 | # 308 | # class TestDslBermudan(DslTestCase): 309 | # 310 | # def testValuation(self): 311 | # specification = """ 312 | # Fixing( Date('2011-06-01'), Choice( Market('#1') - 9, 313 | # Fixing( Date('2012-01-01'), Choice( Market('#1') - 9, 0)) 314 | # )) 315 | # """ 316 | # self.assertValuation(specification, 2.401, 0.677, 0.0001) 317 | # 318 | # 319 | # class TestDslSumContracts(DslTestCase): 320 | # 321 | # def testValuation(self): 322 | # specification = """ 323 | # Fixing( 324 | # Date('2011-06-01'), 325 | # Choice( 326 | # Market('#1') - 9, 327 | # Fixing( 328 | # Date('2012-01-01'), 329 | # Choice( 330 | # Market('#1') - 9, 331 | # 0 332 | # ) 333 | # ) 334 | # ) 335 | # ) + Fixing( 336 | # Date('2011-06-01'), 337 | # Choice( 338 | # Market('#1') - 9, 339 | # Fixing( 340 | # Date('2012-01-01'), 341 | # Choice( 342 | # Market('#1') - 9, 343 | # 0 344 | # ) 345 | # ) 346 | # ) 347 | # ) 348 | # """ 349 | # self.assertValuation(specification, 4.812, 2 * 0.677, 2*0.07, 0.09, 0.2, 0.2) 350 | # 351 | # 352 | # class TestDslAddition(DslTestCase): 353 | # 354 | # def testValuation2(self): 355 | # specification = """ 356 | # Fixing( Date('2012-01-01'), 357 | # Max(Market('#1') - 9, 0) + Market('#1') - 9 358 | # ) 359 | # """ 360 | # self.assertValuation(specification, 3.416, 1.677, 0.07, 0.07, 0.2, 0.2) 361 | # 362 | # 363 | # class TestDslFunctionDefSwing(DslTestCase): 364 | # 365 | # def testValuation(self): 366 | # specification = """ 367 | # def Swing(starts, ends, underlying, quantity): 368 | # if (quantity != 0) and (starts < ends): 369 | # return Choice( 370 | # Swing(starts + TimeDelta('1d'), ends, underlying, quantity-1) \ 371 | # + Fixing(starts, underlying), 372 | # Swing(starts + TimeDelta('1d'), ends, underlying, quantity) 373 | # ) 374 | # else: 375 | # return 0 376 | # Swing(Date('2012-01-01'), Date('2012-01-03'), Market('#1'), 2) 377 | # """ 378 | # self.assertValuation(specification, 20.0, 2.0, 0.07, 0.06, 0.2, 0.2) 379 | # 380 | # 381 | 382 | 383 | # class TestDslFunctionDefEuropean(DslTestCase): 384 | # 385 | # def testValuation(self): 386 | # specification = """ 387 | # def Option(date, strike, underlying, alternative): 388 | # return Wait(date, Choice(underlying - strike, alternative)) 389 | # 390 | # def European(date, strike, underlying): 391 | # return Option(date, strike, underlying, 0) 392 | # 393 | # European(Date('2012-01-01'), 9, Market('#1')) 394 | # """ 395 | # self.assertValuation(specification, 2.356, 0.660, 0.068, 0.04, 0.2, 0.2) 396 | 397 | 398 | # class TestDslFunctionDefAmerican(DslTestCase): 399 | # 400 | # def testValuation(self): 401 | # specification = """ 402 | # def Option(date, strike, underlying, alternative): 403 | # return Wait(date, Choice(underlying - strike, alternative)) 404 | # 405 | # def American(starts, ends, strike, underlying, step): 406 | # Option(starts, strike, underlying, 0) if starts == ends else \ 407 | # Option(starts, strike, underlying, American(starts + step, ends, strike, underlying, step)) 408 | # 409 | # American(Date('2012-01-01'), Date('2012-01-3'), 9, Market('#1'), TimeDelta('1d')) 410 | # """ 411 | # self.assertValuation(specification, 2.356, 0.660, 0.068, 0.04, 0.2, 0.2) 412 | -------------------------------------------------------------------------------- /quantdsl/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/tests/__init__.py -------------------------------------------------------------------------------- /quantdsl/tests/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnbywater/quantdsl/3aee9fbe18dabd844b2b8c99232138a4cbddc476/quantdsl/tests/lib/__init__.py -------------------------------------------------------------------------------- /quantdsl/tests/lib/broken_python_syntax.py: -------------------------------------------------------------------------------- 1 | def f1(): 2 | 1 + 3 | 4 | def f2(): 5 | 2 6 | -------------------------------------------------------------------------------- /quantdsl/tests/test_application_with_multithreading.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | 3 | from quantdsl.application.with_multithreading_and_python_objects import \ 4 | QuantDslApplicationWithMultithreadingAndPythonObjects 5 | from quantdsl.tests.test_application import ExperimentalTests, ExpressionTests, FunctionTests, LongerTests, \ 6 | SingleTests, SpecialTests 7 | 8 | 9 | class WithMultiThreading(TestCase): 10 | def setup_application(self, **kwargs): 11 | self.app = QuantDslApplicationWithMultithreadingAndPythonObjects(**kwargs) 12 | 13 | 14 | class SingleTestsWithMultithreading(WithMultiThreading, SingleTests): 15 | pass 16 | 17 | 18 | class ExperimentalTestsWithMultithreading(WithMultiThreading, ExperimentalTests): 19 | pass 20 | 21 | 22 | class SpecialTestsWithMultithreading(WithMultiThreading, SpecialTests): 23 | pass 24 | 25 | 26 | class ExpressionTestsWithMultithreading(WithMultiThreading, ExpressionTests): 27 | pass 28 | 29 | 30 | class FunctionTestsWithMultithreading(WithMultiThreading, FunctionTests): 31 | pass 32 | 33 | 34 | class LongerTestsWithMultithreading(WithMultiThreading, LongerTests): 35 | pass 36 | -------------------------------------------------------------------------------- /quantdsl/tests/test_calc.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | 3 | import matplotlib.pyplot 4 | from eventsourcing.domain.model.events import assert_event_handlers_empty 5 | 6 | from quantdsl.calculate import calc 7 | from quantdsl.exceptions import CallLimitError 8 | from quantdsl.interfaces.results import Results 9 | 10 | # Need to plot() on Travis 11 | # - otherwise matplotlib/backends/backend_qt5.py says: RuntimeError('Invalid DISPLAY variable') 12 | matplotlib.pyplot.switch_backend('agg') 13 | 14 | 15 | class TestCalc(TestCase): 16 | def setUp(self): 17 | assert_event_handlers_empty() 18 | 19 | def tearDown(self): 20 | assert_event_handlers_empty() 21 | 22 | def test_periodisation_monthly(self): 23 | source_code = """from quantdsl.lib.storage2 import GasStorage 24 | 25 | GasStorage(Date('2011-6-1'), Date('2011-12-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 26 | """ 27 | 28 | results = calc( 29 | source_code=source_code, 30 | observation_date='2011-1-1', 31 | interest_rate=2.5, 32 | periodisation='monthly', 33 | price_process={ 34 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 35 | 'market': ['GAS'], 36 | 'sigma': [0.5], 37 | 'alpha': [0.1], 38 | 'curve': { 39 | 'GAS': ( 40 | ('2011-1-1', 13.5), 41 | ('2011-2-1', 11.0), 42 | ('2011-3-1', 10.0), 43 | ('2011-4-1', 9.0), 44 | ('2011-5-1', 7.5), 45 | ('2011-6-1', 7.0), 46 | ('2011-7-1', 6.5), 47 | ('2011-8-1', 7.5), 48 | ('2011-9-1', 8.5), 49 | ('2011-10-1', 10.0), 50 | ('2011-11-1', 11.5), 51 | ('2011-12-1', 12.0), 52 | ('2012-1-1', 13.5), 53 | ('2012-2-1', 11.0), 54 | ('2012-3-1', 10.0), 55 | ('2012-4-1', 9.0), 56 | ('2012-5-1', 7.5), 57 | ('2012-6-1', 7.0), 58 | ('2012-7-1', 6.5), 59 | ('2012-8-1', 7.5), 60 | ('2012-9-1', 8.5), 61 | ('2012-10-1', 10.0), 62 | ('2012-11-1', 11.5), 63 | ('2012-12-1', 12.0) 64 | ) 65 | } 66 | }, 67 | ) 68 | 69 | self.assertAlmostEqual(results.fair_value.mean(), 8.7977, places=0) 70 | self.assertEqual(len(results.periods), 6) 71 | 72 | def test_periodisation_alltime(self): 73 | source_code = """from quantdsl.lib.storage2 import GasStorage 74 | 75 | GasStorage(Date('2011-6-1'), Date('2011-12-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 76 | """ 77 | 78 | results = calc( 79 | source_code=source_code, 80 | observation_date='2011-1-1', 81 | interest_rate=2.5, 82 | periodisation='alltime', 83 | price_process={ 84 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 85 | 'market': ['GAS'], 86 | 'sigma': [0.5], 87 | 'curve': { 88 | 'GAS': ( 89 | ('2011-1-1', 13.5), 90 | ('2011-2-1', 11.0), 91 | ('2011-3-1', 10.0), 92 | ('2011-4-1', 9.0), 93 | ('2011-5-1', 7.5), 94 | ('2011-6-1', 7.0), 95 | ('2011-7-1', 6.5), 96 | ('2011-8-1', 7.5), 97 | ('2011-9-1', 8.5), 98 | ('2011-10-1', 10.0), 99 | ('2011-11-1', 11.5), 100 | ('2011-12-1', 12.0), 101 | ('2012-1-1', 13.5), 102 | ('2012-2-1', 11.0), 103 | ('2012-3-1', 10.0), 104 | ('2012-4-1', 9.0), 105 | ('2012-5-1', 7.5), 106 | ('2012-6-1', 7.0), 107 | ('2012-7-1', 6.5), 108 | ('2012-8-1', 7.5), 109 | ('2012-9-1', 8.5), 110 | ('2012-10-1', 10.0), 111 | ('2012-11-1', 11.5), 112 | ('2012-12-1', 12.0) 113 | ) 114 | } 115 | }, 116 | ) 117 | 118 | self.assertAlmostEqual(results.fair_value.mean(), 8.749, places=0) 119 | self.assertEqual(len(results.periods), 1) 120 | 121 | def test_periodisation_none(self): 122 | source_code = """from quantdsl.lib.storage2 import GasStorage 123 | 124 | GasStorage(Date('2011-6-1'), Date('2011-12-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 125 | """ 126 | 127 | results = calc( 128 | source_code=source_code, 129 | observation_date='2011-1-1', 130 | interest_rate=2.5, 131 | price_process={ 132 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 133 | 'market': ['GAS'], 134 | 'sigma': [0.5], 135 | 'curve': { 136 | 'GAS': ( 137 | ('2011-1-1', 13.5), 138 | ('2011-2-1', 11.0), 139 | ('2011-3-1', 10.0), 140 | ('2011-4-1', 9.0), 141 | ('2011-5-1', 7.5), 142 | ('2011-6-1', 7.0), 143 | ('2011-7-1', 6.5), 144 | ('2011-8-1', 7.5), 145 | ('2011-9-1', 8.5), 146 | ('2011-10-1', 10.0), 147 | ('2011-11-1', 11.5), 148 | ('2011-12-1', 12.0), 149 | ('2012-1-1', 13.5), 150 | ('2012-2-1', 11.0), 151 | ('2012-3-1', 10.0), 152 | ('2012-4-1', 9.0), 153 | ('2012-5-1', 7.5), 154 | ('2012-6-1', 7.0), 155 | ('2012-7-1', 6.5), 156 | ('2012-8-1', 7.5), 157 | ('2012-9-1', 8.5), 158 | ('2012-10-1', 10.0), 159 | ('2012-11-1', 11.5), 160 | ('2012-12-1', 12.0) 161 | ) 162 | } 163 | }, 164 | ) 165 | 166 | self.assertAlmostEqual(results.fair_value.mean(), 8.6359, places=0) 167 | self.assertEqual(len(results.periods), 0) 168 | 169 | def test_timeout(self): 170 | source_code = """ 171 | from quantdsl.lib.storage2 import GasStorage 172 | GasStorage(Date('2011-1-1'), Date('2011-12-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 173 | """ 174 | 175 | with self.assertRaises(SystemExit): 176 | calc( 177 | source_code=source_code, 178 | observation_date='2011-1-1', 179 | interest_rate=2.5, 180 | price_process={ 181 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 182 | 'market': ['GAS'], 183 | 'sigma': [0.5], 184 | 'curve': { 185 | 'GAS': ( 186 | ('2011-1-1', 13.5), 187 | ) 188 | } 189 | }, 190 | periodisation='monthly', 191 | timeout=.5, 192 | ) 193 | 194 | def test_dependency_graph_size_limit(self): 195 | source_code = """ 196 | from quantdsl.lib.storage2 import GasStorage 197 | GasStorage(Date('2011-1-1'), Date('2011-4-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 198 | """ 199 | 200 | with self.assertRaises(CallLimitError): 201 | calc( 202 | source_code=source_code, 203 | observation_date='2011-1-1', 204 | interest_rate=2.5, 205 | price_process={ 206 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 207 | 'market': ['GAS'], 208 | 'sigma': [0.5], 209 | 'curve': { 210 | 'GAS': ( 211 | ('2011-1-1', 13.5), 212 | ) 213 | } 214 | }, 215 | max_dependency_graph_size=1, 216 | ) 217 | 218 | def test_results(self): 219 | source_code = """ 220 | from quantdsl.lib.storage2 import GasStorage 221 | GasStorage(Date('2011-1-1'), Date('2011-4-1'), 'GAS', 0, 0, 50000, TimeDelta('1m'), 1) 222 | """ 223 | 224 | results = calc( 225 | source_code=source_code, 226 | observation_date='2011-1-1', 227 | interest_rate=2.5, 228 | periodisation='monthly', 229 | price_process={ 230 | 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 231 | 'market': ['GAS'], 232 | 'sigma': [0.5], 233 | 'curve': { 234 | 'GAS': ( 235 | ('2011-1-1', 13.5), 236 | ('2011-2-1', 16.5), 237 | ('2011-3-1', 19.5), 238 | ('2011-4-1', 17.5), 239 | ) 240 | } 241 | }, 242 | ) 243 | assert isinstance(results, Results) 244 | # self.assertIsInstance(results.cash_mean, DataFrame) 245 | 246 | # Check results can be plotted. 247 | results.plot() 248 | 249 | string = str(results) 250 | self.assertIn('Fair value', string) 251 | # Todo: Check other aspects of the string. 252 | -------------------------------------------------------------------------------- /quantdsl/tests/test_call_result_policy.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | 3 | from eventsourcing.domain.model.events import assert_event_handlers_empty 4 | from eventsourcing.infrastructure.event_store import EventStore 5 | from eventsourcing.infrastructure.persistence_subscriber import PersistenceSubscriber 6 | from eventsourcing.infrastructure.stored_events.python_objects_stored_events import PythonObjectsStoredEventRepository 7 | 8 | from quantdsl.application.call_result_policy import CallResultPolicy 9 | from quantdsl.domain.model.call_dependencies import register_call_dependencies 10 | from quantdsl.domain.model.call_dependents import register_call_dependents 11 | from quantdsl.domain.model.call_result import register_call_result 12 | from quantdsl.infrastructure.event_sourced_repos.call_dependencies_repo import CallDependenciesRepo 13 | from quantdsl.infrastructure.event_sourced_repos.call_dependents_repo import CallDependentsRepo 14 | from quantdsl.infrastructure.event_sourced_repos.call_requirement_repo import CallRequirementRepo 15 | 16 | 17 | class TestCallResultPolicy(TestCase): 18 | def setUp(self): 19 | assert_event_handlers_empty() 20 | self.es = EventStore(PythonObjectsStoredEventRepository()) 21 | self.ps = PersistenceSubscriber(self.es) 22 | # self.call_result_repo = CallResultRepo(self.es) 23 | self.call_result_repo = {} 24 | self.call_dependencies_repo = CallDependenciesRepo(self.es) 25 | self.call_dependents_repo = CallDependentsRepo(self.es) 26 | self.call_requirement_repo = CallRequirementRepo(self.es) 27 | self.policy = CallResultPolicy(call_result_repo=self.call_result_repo) 28 | 29 | def tearDown(self): 30 | self.ps.close() 31 | self.policy.close() 32 | assert_event_handlers_empty() 33 | 34 | def test_delete_result(self): 35 | # In this test, there are two "calls": call1 and call2. 36 | # It is supposed that call1 happens first, and call2 uses the result of call1. 37 | # Therefore call2 depends upon call1, call1 is a dependency of call2, and call2 is a dependent of call1. 38 | call1_id = 'call1' 39 | call2_id = 'call2' 40 | contract_valuation_id = 'val1' 41 | contract_specification_id = 'spec1' 42 | # call1_id = uuid4().hex 43 | # call2_id = uuid4().hex 44 | # contract_valuation_id = uuid4().hex 45 | # contract_specification_id = uuid4().hex 46 | 47 | register_call_dependencies(call2_id, [call1_id]) 48 | 49 | # Check the policy has the dependencies for call2. 50 | self.assertEqual(self.policy.dependencies[call2_id], [call1_id]) 51 | 52 | # Register dependents of call1, as call2. 53 | register_call_dependents(call1_id, [call2_id]) 54 | 55 | # Check the policy has the dependencies for call2. 56 | self.assertEqual(self.policy.dependents[call1_id], [call2_id]) 57 | 58 | # Register call result for call1. 59 | # - this should trigger deletion of call2 result 60 | call1_result = register_call_result(call1_id, 1.0, {}, contract_valuation_id, contract_specification_id, []) 61 | 62 | # Check the policy has the result for call1. 63 | self.assertTrue(call1_result.id in self.policy.result) 64 | 65 | # Check the result for call1 exists. 66 | self.assertTrue(call1_result.id in self.call_result_repo) 67 | 68 | # Register call result for call2. 69 | call2_result = register_call_result(call2_id, 1.0, {}, contract_valuation_id, contract_specification_id, []) 70 | 71 | # Check the policy has the result for call2. 72 | self.assertTrue(call2_result.id in self.policy.result) 73 | 74 | # Check the result for call2 exists. 75 | self.assertTrue(call2_result.id in self.call_result_repo) 76 | 77 | # Check the policy does not have the result for call1. 78 | self.assertFalse(call1_result.id in self.policy.result) 79 | 80 | # Check the result for call1 doesn't exist (because it's dependents have results). 81 | self.assertFalse(call1_result.id in self.call_result_repo) 82 | -------------------------------------------------------------------------------- /quantdsl/tests/test_dependency_graph_subscriber.py: -------------------------------------------------------------------------------- 1 | from eventsourcing.domain.model.events import publish 2 | from mock import patch 3 | from mock import MagicMock 4 | import unittest 5 | 6 | from quantdsl.domain.model.call_dependencies import CallDependenciesRepository 7 | from quantdsl.domain.model.call_dependents import CallDependentsRepository 8 | from quantdsl.domain.model.call_leafs import CallLeafsRepository 9 | from quantdsl.domain.model.call_requirement import CallRequirementRepository 10 | from quantdsl.domain.model.contract_specification import ContractSpecificationRepository, ContractSpecification 11 | from quantdsl.infrastructure.dependency_graph_subscriber import DependencyGraphSubscriber 12 | 13 | 14 | class TestDependencyGraphSubscriber(unittest.TestCase): 15 | 16 | def setUp(self): 17 | contract_specification_repo = MagicMock(spec=ContractSpecificationRepository) 18 | call_dependencies_repo = MagicMock(spec=CallDependenciesRepository) 19 | call_dependents_repo = MagicMock(spec=CallDependentsRepository) 20 | call_leafs_repo = MagicMock(spec=CallLeafsRepository) 21 | call_requirement_repo = MagicMock(spec=CallRequirementRepository) 22 | self.dependency_graph_subscriber = DependencyGraphSubscriber( 23 | contract_specification_repo, 24 | call_dependencies_repo, 25 | call_dependents_repo, 26 | call_leafs_repo, 27 | call_requirement_repo, 28 | # Todo: Test call limiting in this test case. 29 | max_dependency_graph_size=None, 30 | dsl_classes={} 31 | ) 32 | 33 | def tearDown(self): 34 | self.dependency_graph_subscriber.close() 35 | 36 | @patch('quantdsl.infrastructure.dependency_graph_subscriber.generate_dependency_graph') 37 | def test_dependency_graph_subscriber(self, generate_dependency_graph): 38 | market_simulation_created = ContractSpecification.Created(entity_id='1', specification='1') 39 | publish(market_simulation_created) 40 | self.assertEqual(generate_dependency_graph.call_count, 1) 41 | -------------------------------------------------------------------------------- /quantdsl/tests/test_domain_services.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import unittest 3 | 4 | import scipy 5 | import six 6 | from eventsourcing.infrastructure.event_store import EventStore 7 | from eventsourcing.infrastructure.persistence_subscriber import PersistenceSubscriber 8 | from eventsourcing.infrastructure.stored_events.python_objects_stored_events import PythonObjectsStoredEventRepository 9 | from mock import MagicMock, Mock, patch 10 | 11 | from quantdsl.defaults import DEFAULT_PRICE_PROCESS_NAME 12 | from quantdsl.domain.model.call_dependencies import CallDependencies, CallDependenciesRepository 13 | from quantdsl.domain.model.call_dependents import CallDependents, CallDependentsRepository 14 | from quantdsl.domain.model.call_link import CallLink, CallLinkRepository 15 | from quantdsl.domain.model.call_requirement import CallRequirement, CallRequirementRepository 16 | from quantdsl.domain.model.call_result import CallResult, CallResultRepository 17 | from quantdsl.domain.model.contract_specification import ContractSpecification, register_contract_specification 18 | from quantdsl.domain.model.market_calibration import MarketCalibration 19 | from quantdsl.domain.model.market_simulation import MarketSimulation 20 | from quantdsl.domain.services.call_links import regenerate_execution_order 21 | from quantdsl.domain.services.contract_valuations import get_dependency_results 22 | from quantdsl.domain.services.dependency_graphs import generate_dependency_graph, generate_execution_order 23 | from quantdsl.domain.services.price_processes import get_price_process 24 | from quantdsl.domain.services.simulated_prices import generate_simulated_prices, identify_simulation_requirements, \ 25 | simulate_future_prices 26 | from quantdsl.domain.services.uuids import create_uuid4 27 | from quantdsl.exceptions import DslError 28 | from quantdsl.infrastructure.event_sourced_repos.call_dependencies_repo import CallDependenciesRepo 29 | from quantdsl.infrastructure.event_sourced_repos.call_dependents_repo import CallDependentsRepo 30 | from quantdsl.infrastructure.event_sourced_repos.call_leafs_repo import CallLeafsRepo 31 | from quantdsl.infrastructure.event_sourced_repos.call_requirement_repo import CallRequirementRepo 32 | from quantdsl.priceprocess.blackscholes import BlackScholesPriceProcess 33 | 34 | 35 | class TestUUIDs(unittest.TestCase): 36 | def test_create_uuid4(self): 37 | id = create_uuid4() 38 | self.assertIsInstance(id, six.string_types) 39 | 40 | 41 | class TestDependencyGraph(unittest.TestCase): 42 | def setUp(self): 43 | self.es = EventStore(stored_event_repo=PythonObjectsStoredEventRepository()) 44 | self.ps = PersistenceSubscriber(self.es) 45 | self.call_dependencies_repo = CallDependenciesRepo(self.es) 46 | self.call_dependents_repo = CallDependentsRepo(self.es) 47 | self.call_leafs_repo = CallLeafsRepo(self.es) 48 | self.call_requirement_repo = CallRequirementRepo(self.es) 49 | 50 | def tearDown(self): 51 | self.ps.close() 52 | 53 | def test_generate_dependency_graph_with_function_call(self): 54 | contract_specification = register_contract_specification(source_code=""" 55 | def double(x): 56 | return x * 2 57 | 58 | double(1 + 1) 59 | """) 60 | 61 | generate_dependency_graph( 62 | contract_specification=contract_specification, 63 | call_dependencies_repo=self.call_dependencies_repo, 64 | call_dependents_repo=self.call_dependents_repo, 65 | call_requirement_repo=self.call_requirement_repo, 66 | ) 67 | 68 | root_dependencies = self.call_dependencies_repo[contract_specification.id] 69 | assert isinstance(root_dependencies, CallDependencies) 70 | self.assertEqual(len(root_dependencies.dependencies), 1) 71 | 72 | root_dependency = root_dependencies.dependencies[0] 73 | call_dependencies = self.call_dependencies_repo[root_dependency] 74 | self.assertEqual(len(call_dependencies.dependencies), 0) 75 | 76 | dependency = self.call_dependents_repo[root_dependency] 77 | assert isinstance(dependency, CallDependents) 78 | self.assertEqual(len(dependency.dependents), 1) 79 | 80 | self.assertEqual(dependency.dependents[0], contract_specification.id) 81 | 82 | # def test_generate_dependency_graph_recursive_functional_call(self): 83 | # contract_specification = self.app.register_contract_specification(specification=""" 84 | # def inc(x): 85 | # if x < 10: 86 | # return inc(x+1) + inc(x+2) 87 | # else: 88 | # return 100 89 | # 90 | # inc(1 + 2) 91 | # """) 92 | # 93 | # dependency_graph = self.app.generate_dependency_graph(contract_specification=contract_specification) 94 | # 95 | # call_dependencies = self.app.call_dependencies_repo[dependency_graph.id] 96 | # self.assertEqual(len(call_dependencies.requirements), 1) 97 | # dependency_id = call_dependencies.requirements[0] 98 | # dependents = self.app.call_dependents_repo[dependency_id].dependents 99 | # self.assertEqual(len(dependents), 1) 100 | # self.assertIn(dependency_graph.id, dependents) 101 | # # A circular dependency... 102 | # self.assertIn(dependency_id, dependents) 103 | 104 | 105 | 106 | def test_generate_execution_order(self): 107 | # Dependencies: 108 | # 1 -> 2 109 | # 1 -> 3 110 | # 3 -> 2 111 | 112 | # Therefore dependents: 113 | # 1 = [] 114 | # 2 = [1, 3] 115 | # 3 = [1] 116 | 117 | # 2 depends on nothing, so 2 is a leaf. 118 | # 1 depends on 3 and 2, so 1 is not next. 119 | # 3 depends only on 2, so is next. 120 | # Therefore 1 is last. 121 | # Hence evaluation order: 2, 3, 1 122 | 123 | call_dependencies_repo = MagicMock(spec=CallDependenciesRepository, 124 | __getitem__=lambda self, x: { 125 | 1: [2, 3], 126 | 2: [], 127 | 3: [2] 128 | }[x]) 129 | call_dependents_repo = MagicMock(spec=CallDependentsRepository, 130 | __getitem__=lambda self, x: { 131 | 1: [], 132 | 2: [1, 3], 133 | 3: [1] 134 | }[x]) 135 | 136 | leaf_call_ids = [2] 137 | execution_order_gen = generate_execution_order(leaf_call_ids, call_dependents_repo, call_dependencies_repo) 138 | execution_order = list(execution_order_gen) 139 | self.assertEqual(execution_order, [2, 3, 1]) 140 | 141 | def test_get_dependency_values(self): 142 | call_dependencies_repo = MagicMock(spec=CallDependenciesRepository, 143 | __getitem__=lambda self, x: { 144 | '1': CallDependencies(dependencies=['2', '3'], entity_id=123, 145 | entity_version=0, timestamp=1), 146 | }[x]) 147 | call_result1 = Mock(spec=CallResult, result_value=12, perturbed_values={}) 148 | call_result2 = Mock(spec=CallResult, result_value=13, perturbed_values={}) 149 | call_result_repo = MagicMock(spec=CallResultRepository, 150 | __getitem__=lambda self, x: { 151 | 'valuation2': call_result1, 152 | 'valuation3': call_result2, 153 | }[x]) 154 | values = get_dependency_results('valuation', '1', call_dependencies_repo, call_result_repo) 155 | self.assertEqual(len(values), 2) 156 | self.assertEqual(values['2'].result_value, 12) 157 | self.assertEqual(values['3'].result_value, 13) 158 | 159 | 160 | class TestCallLinks(unittest.TestCase): 161 | def test_regenerate_execution_order(self): 162 | contract_specification = Mock(spec=ContractSpecification, id=1) 163 | call_link_repo = MagicMock(spec=CallLinkRepository, 164 | __getitem__=lambda self, x: { 165 | 1: Mock(spec=CallLink, call_id=2), 166 | 2: Mock(spec=CallLink, call_id=3), 167 | 3: Mock(spec=CallLink, call_id=1), 168 | }[x]) 169 | order = regenerate_execution_order(contract_specification.id, call_link_repo) 170 | order = list(order) 171 | self.assertEqual(order, [2, 3, 1]) 172 | 173 | 174 | # Todo: Fix up this test to check the new behaviour: setting the market requirements. 175 | class TestListMarketNamesAndFixingDates(unittest.TestCase): 176 | def test_list_market_names_and_fixing_dates(self): 177 | contract_specification = Mock(spec=ContractSpecification, id=1) 178 | call_requirement1 = Mock(spec=CallRequirement, dsl_source="Fixing('2011-01-01', Market('1'))", 179 | present_time=datetime.datetime(2011, 1, 1), _dsl_expr=None, id=1) 180 | call_requirement2 = Mock(spec=CallRequirement, dsl_source="Fixing('2012-02-02', Market('2'))", 181 | present_time=datetime.datetime(2011, 2, 2), _dsl_expr=None, id=2) 182 | call_requirement3 = Mock(spec=CallRequirement, dsl_source="Fixing('2013-03-03', Market('3'))", 183 | present_time=datetime.datetime(2011, 3, 3), _dsl_expr=None, id=3) 184 | call_requirement_repo = MagicMock(spec=CallRequirementRepository, 185 | __getitem__=lambda self, x: { 186 | 1: call_requirement1, 187 | 2: call_requirement2, 188 | 3: call_requirement3, 189 | }[x]) 190 | 191 | self.assertEqual(call_requirement1.id, 1) 192 | self.assertEqual(call_requirement2.id, 2) 193 | self.assertEqual(call_requirement3.id, 3) 194 | call_link_repo = MagicMock(spec=CallLinkRepository, 195 | __getitem__=lambda self, x: { 196 | 1: Mock(spec=CallLink, call_id=2), 197 | 2: Mock(spec=CallLink, call_id=3), 198 | 3: Mock(spec=CallLink, call_id=1), 199 | }[x]) 200 | call_dependencies_repo = MagicMock(spec=CallDependenciesRepo, 201 | __getitem__=lambda self, x: { 202 | 1: Mock(spec=CallDependencies, dependencies=[]), 203 | 2: Mock(spec=CallDependencies, dependencies=[]), 204 | 3: Mock(spec=CallDependencies, dependencies=[]), 205 | }[x]) 206 | 207 | observation_date = datetime.datetime(2011, 1, 1) 208 | 209 | requirements = set() 210 | identify_simulation_requirements(contract_specification.id, call_requirement_repo, call_link_repo, 211 | call_dependencies_repo, observation_date, requirements) 212 | 213 | self.assertEqual(requirements, { 214 | ('1', datetime.datetime(2011, 1, 1), datetime.datetime(2011, 1, 1)), 215 | ('2', datetime.datetime(2012, 2, 2), datetime.datetime(2012, 2, 2)), 216 | ('3', datetime.datetime(2013, 3, 3), datetime.datetime(2013, 3, 3)), 217 | }) 218 | 219 | 220 | class TestSimulatedPrices(unittest.TestCase): 221 | @patch('quantdsl.domain.services.simulated_prices.register_simulated_price') 222 | @patch('quantdsl.domain.services.simulated_prices.simulate_future_prices', return_value=[ 223 | ('#1', datetime.datetime(2011, 1, 1), datetime.datetime(2011, 1, 1), 10), 224 | ('#1', datetime.datetime(2011, 1, 2), datetime.datetime(2011, 1, 2), 10), 225 | ('#1', datetime.datetime(2011, 1, 2), datetime.datetime(2011, 1, 2), 10), 226 | ]) 227 | def test_generate_simulated_prices(self, simulate_future_prices, register_simuated_price): 228 | market_calibration = Mock(spec=MarketCalibration) 229 | market_simulation = Mock(spec=MarketSimulation) 230 | prices = generate_simulated_prices(market_simulation=market_simulation, market_calibration=market_calibration) 231 | prices = list(prices) 232 | self.assertEqual(register_simuated_price.call_count, len(simulate_future_prices.return_value)) 233 | 234 | @patch('quantdsl.domain.services.simulated_prices.get_price_process', new=lambda name: Mock( 235 | spec=BlackScholesPriceProcess, 236 | simulate_future_prices=lambda observation_date, requirements, path_count, calibration_params: [ 237 | ('#1', datetime.date(2011, 1, 1), scipy.array([10.])), 238 | ('#1', datetime.date(2011, 1, 2), scipy.array([10.])), 239 | ] 240 | )) 241 | def test_simulate_future_prices(self): 242 | ms = Mock(spec=MarketSimulation) 243 | mc = Mock(spec=MarketCalibration) 244 | prices = simulate_future_prices(market_simulation=ms, market_calibration=mc) 245 | self.assertEqual(list(prices), [ 246 | ('#1', datetime.date(2011, 1, 1), scipy.array([10.])), 247 | ('#1', datetime.date(2011, 1, 2), scipy.array([10.])), 248 | ]) 249 | 250 | 251 | class TestPriceProcesses(unittest.TestCase): 252 | def test_get_price_process(self): 253 | price_process = get_price_process(DEFAULT_PRICE_PROCESS_NAME) 254 | self.assertIsInstance(price_process, BlackScholesPriceProcess) 255 | 256 | # Test the error paths. 257 | # - can't import the Python module 258 | self.assertRaises(DslError, get_price_process, 'x' + DEFAULT_PRICE_PROCESS_NAME) 259 | # - can't find the price process class 260 | self.assertRaises(DslError, get_price_process, DEFAULT_PRICE_PROCESS_NAME + 'x') 261 | -------------------------------------------------------------------------------- /quantdsl/tests/test_evaluation_subscriber.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from eventsourcing.domain.model.events import assert_event_handlers_empty, publish 4 | from mock import MagicMock, Mock, patch 5 | 6 | from quantdsl.domain.model.call_dependencies import CallDependenciesRepository 7 | from quantdsl.domain.model.call_dependents import CallDependentsRepository 8 | from quantdsl.domain.model.call_leafs import CallLeafs, CallLeafsRepository 9 | from quantdsl.domain.model.call_link import CallLinkRepository 10 | from quantdsl.domain.model.call_requirement import CallRequirementRepository 11 | from quantdsl.domain.model.call_result import CallResultRepository 12 | from quantdsl.domain.model.contract_valuation import ContractValuation, ContractValuationRepository 13 | from quantdsl.domain.model.market_simulation import MarketSimulation, MarketSimulationRepository 14 | from quantdsl.domain.model.simulated_price import SimulatedPriceRepository 15 | from quantdsl.infrastructure.evaluation_subscriber import EvaluationSubscriber 16 | from quantdsl.infrastructure.event_sourced_repos.perturbation_dependencies_repo import PerturbationDependenciesRepo 17 | from quantdsl.infrastructure.event_sourced_repos.simulated_price_dependencies_repo import \ 18 | SimulatedPriceRequirementsRepo 19 | 20 | 21 | class TestEvaluationSubscriber(unittest.TestCase): 22 | def setUp(self): 23 | assert_event_handlers_empty() 24 | contract_valuation_repo = MagicMock(spec=ContractValuationRepository) 25 | contract_valuation_repo.__getitem__.return_value = Mock(spec=ContractValuation) 26 | market_simulation_repo = MagicMock(spec=MarketSimulationRepository) 27 | market_simulation_repo.__getitem__.return_value = Mock(spec=MarketSimulation) 28 | call_leafs_repo = MagicMock(spec=CallLeafsRepository) 29 | call_leafs_repo.__getitem__.return_value = Mock(spec=CallLeafs) 30 | call_dependents_repo = MagicMock(spec=CallDependentsRepository) 31 | perturbation_dependencies_repo = MagicMock(spec=PerturbationDependenciesRepo) 32 | simulated_price_requirements_repo = MagicMock(spec=SimulatedPriceRequirementsRepo) 33 | 34 | self.evaluation_subscriber = EvaluationSubscriber( 35 | contract_valuation_repo=contract_valuation_repo, 36 | call_link_repo=MagicMock(spec=CallLinkRepository), 37 | call_dependencies_repo=MagicMock(spec=CallDependenciesRepository), 38 | call_requirement_repo=MagicMock(spec=CallRequirementRepository), 39 | call_result_repo=MagicMock(spec=CallResultRepository), 40 | simulated_price_repo=MagicMock(spec=SimulatedPriceRepository), 41 | market_simulation_repo=market_simulation_repo, 42 | call_leafs_repo=call_leafs_repo, 43 | call_evaluation_queue=None, 44 | call_dependents_repo=call_dependents_repo, 45 | perturbation_dependencies_repo=perturbation_dependencies_repo, 46 | simulated_price_requirements_repo=simulated_price_requirements_repo 47 | ) 48 | 49 | def tearDown(self): 50 | self.evaluation_subscriber.close() 51 | assert_event_handlers_empty() 52 | 53 | @patch('quantdsl.infrastructure.evaluation_subscriber.generate_contract_valuation') 54 | def test_evaluation_subscriber(self, evaluate_contract_in_series): 55 | # Check that when an event is published, the domain service is called. 56 | contract_valuation_created = ContractValuation.Created( 57 | entity_id='1', 58 | market_calibration_id='1', 59 | is_double_sided_deltas=False, 60 | ) 61 | self.assertEqual(evaluate_contract_in_series.call_count, 0) 62 | publish(contract_valuation_created) 63 | self.assertEqual(evaluate_contract_in_series.call_count, 1) 64 | -------------------------------------------------------------------------------- /quantdsl/tests/test_eventsourced_repos.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import scipy 4 | import six 5 | 6 | from quantdsl.domain.model.call_dependencies import CallDependencies 7 | from quantdsl.domain.model.call_dependents import CallDependents 8 | from quantdsl.domain.model.call_requirement import CallRequirement, register_call_requirement 9 | from quantdsl.domain.model.call_result import CallResult, make_call_result_id, register_call_result 10 | from quantdsl.domain.model.contract_specification import ContractSpecification 11 | from quantdsl.domain.model.market_calibration import MarketCalibration 12 | from quantdsl.domain.model.simulated_price import SimulatedPrice, make_simulated_price_id, register_simulated_price 13 | from quantdsl.domain.services.uuids import create_uuid4 14 | from quantdsl.defaults import DEFAULT_PRICE_PROCESS_NAME 15 | from quantdsl.tests.test_application import ApplicationTestCase 16 | 17 | 18 | class TestEventSourcedRepos(ApplicationTestCase): 19 | def test_register_market_calibration(self): 20 | price_process_name = DEFAULT_PRICE_PROCESS_NAME 21 | calibration_params = {'param1': 10, 'param2': 20} 22 | 23 | market_calibration = self.app.register_market_calibration(price_process_name, calibration_params) 24 | 25 | assert isinstance(market_calibration, MarketCalibration) 26 | assert market_calibration.id 27 | market_calibration = self.app.market_calibration_repo[market_calibration.id] 28 | assert isinstance(market_calibration, MarketCalibration) 29 | self.assertEqual(market_calibration.price_process_name, DEFAULT_PRICE_PROCESS_NAME) 30 | self.assertEqual(market_calibration.calibration_params['param1'], 10) 31 | self.assertEqual(market_calibration.calibration_params['param2'], 20) 32 | 33 | def test_register_contract_specification(self): 34 | contract_spec = self.app.register_contract_specification('1 + 1') 35 | self.assertIsInstance(contract_spec, ContractSpecification) 36 | self.assertIsInstance(contract_spec.id, six.string_types) 37 | contract_spec = self.app.contract_specification_repo[contract_spec.id] 38 | assert isinstance(contract_spec, ContractSpecification) 39 | self.assertEqual(contract_spec.source_code, '1 + 1') 40 | 41 | def test_register_call_requirements(self): 42 | call_id = create_uuid4() 43 | contract_specification_id = create_uuid4() 44 | 45 | self.assertRaises(KeyError, self.app.call_requirement_repo.__getitem__, call_id) 46 | 47 | dsl_source = '1 + 1' 48 | present_time = datetime.datetime(2015, 9, 7, 0, 0, 0) 49 | 50 | register_call_requirement( 51 | call_id=call_id, 52 | dsl_source=dsl_source, 53 | present_time=present_time, 54 | contract_specification_id=contract_specification_id, 55 | cost=1, 56 | ) 57 | 58 | call_requirement = self.app.call_requirement_repo[call_id] 59 | assert isinstance(call_requirement, CallRequirement) 60 | self.assertEqual(call_requirement.dsl_source, dsl_source) 61 | self.assertEqual(call_requirement.present_time, present_time) 62 | self.assertEqual(call_requirement.contract_specification_id, contract_specification_id) 63 | 64 | def test_register_call_dependencies(self): 65 | call_id = create_uuid4() 66 | 67 | self.assertRaises(KeyError, self.app.call_dependencies_repo.__getitem__, call_id) 68 | 69 | dependencies = ['123', '456'] 70 | 71 | self.app.register_call_dependencies(call_id=call_id, dependencies=dependencies) 72 | 73 | call_dependencies = self.app.call_dependencies_repo[call_id] 74 | assert isinstance(call_dependencies, CallDependencies) 75 | self.assertEqual(call_dependencies.dependencies, dependencies) 76 | 77 | def test_register_call_dependents(self): 78 | call_id = create_uuid4() 79 | 80 | self.assertRaises(KeyError, self.app.call_dependents_repo.__getitem__, call_id) 81 | 82 | dependents = ['123', '456'] 83 | 84 | self.app.register_call_dependents(call_id=call_id, dependents=dependents) 85 | 86 | call_dependents = self.app.call_dependents_repo[call_id] 87 | assert isinstance(call_dependents, CallDependents) 88 | self.assertEqual(call_dependents.dependents, dependents) 89 | 90 | def test_register_call_result(self): 91 | contract_specification_id = create_uuid4() 92 | contract_valuation_id = create_uuid4() 93 | call_id = create_uuid4() 94 | 95 | call_result_id = make_call_result_id(contract_valuation_id, call_id) 96 | self.assertRaises(KeyError, self.app.call_result_repo.__getitem__, call_result_id) 97 | 98 | register_call_result(call_id=call_id, result_value=123, perturbed_values={}, 99 | contract_valuation_id=contract_valuation_id, 100 | contract_specification_id=contract_specification_id, 101 | involved_market_names=[]) 102 | 103 | call_result = self.app.call_result_repo[call_result_id] 104 | assert isinstance(call_result, CallResult) 105 | self.assertEqual(call_result.result_value, 123) 106 | 107 | def test_register_simulated_price(self): 108 | price_time = datetime.datetime(2011, 1, 1) 109 | price_value = scipy.array([1.1, 1.2, 1.367345987359734598734598723459872345987235698237459862345]) 110 | simulation_id = create_uuid4() 111 | self.assertRaises(KeyError, self.app.simulated_price_repo.__getitem__, simulation_id) 112 | 113 | price = register_simulated_price(simulation_id, '#1', price_time, price_time, price_value) 114 | 115 | assert isinstance(price, SimulatedPrice), price 116 | assert price.id 117 | simulated_price_id = make_simulated_price_id(simulation_id, '#1', price_time, price_time) 118 | self.assertEqual(price.id, simulated_price_id) 119 | self.app.simulated_price_repo[price.id] = price 120 | price = self.app.simulated_price_repo[simulated_price_id] 121 | assert isinstance(price, SimulatedPrice) 122 | import numpy 123 | numpy.testing.assert_equal(price.value, price_value) 124 | -------------------------------------------------------------------------------- /quantdsl/tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | 3 | from mock import Mock 4 | 5 | from quantdsl.exceptions import DslError 6 | 7 | 8 | class TestDslError(TestCase): 9 | def test(self): 10 | error = DslError(error='error', descr='descr') 11 | self.assertEqual(error.error, 'error') 12 | self.assertEqual(error.descr, 'descr') 13 | self.assertEqual(error.node, None) 14 | self.assertEqual(error.lineno, None) 15 | self.assertEqual(repr(error), 'error: descr') 16 | 17 | node = Mock() 18 | node.lineno = 123 19 | error = DslError(error='error', descr='descr', node=node) 20 | self.assertEqual(repr(error), 'error: descr (line 123)') 21 | self.assertEqual(error.node, node) 22 | self.assertEqual(error.lineno, 123) 23 | -------------------------------------------------------------------------------- /quantdsl/tests/test_forward_curve.py: -------------------------------------------------------------------------------- 1 | from unittest.case import TestCase 2 | 3 | 4 | class SchwartzSmithForwardCurve(dict): 5 | def __init__(self, config): 6 | super(SchwartzSmithForwardCurve, self).__init__() 7 | self.config = config 8 | 9 | def keys(self): 10 | return self.config.keys() 11 | 12 | def __getitem__(self, item): 13 | config = self.config[item] 14 | return [] 15 | 16 | class TestForwardCurve(TestCase): 17 | 18 | def test(self): 19 | c = SchwartzSmithForwardCurve({}) 20 | # Check it works like a normal curve dict, with commodity names as keys 21 | # and values being a list of tuples ('YYYY-MM-DD', price). 22 | self.assertEqual(list(c.keys()), []) 23 | self.assertEqual(list(c.values()), []) 24 | self.assertEqual(list(c.items()), []) 25 | with self.assertRaises(KeyError): 26 | c['GAS'] 27 | 28 | c = SchwartzSmithForwardCurve({ 29 | 'GAS': {}, 30 | }) 31 | self.assertEqual(list(c.keys()), ['GAS']) 32 | self.assertEqual(c['GAS'], []) 33 | -------------------------------------------------------------------------------- /quantdsl/tests/test_forwardcurve.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import datetime 4 | 5 | from quantdsl.priceprocess.forwardcurve import ForwardCurve 6 | 7 | 8 | class TestForwardCurve(TestCase): 9 | def test_get_price(self): 10 | # Check without any data. 11 | curve = ForwardCurve('name', []) 12 | 13 | # Check raises KeyError for any dates. 14 | with self.assertRaises(KeyError): 15 | curve.get_price(date=datetime.datetime(2011, 12, 31)) 16 | 17 | # Check with data. 18 | curve = ForwardCurve('name', [('2011-1-1', 1), ('2011-1-3', 3)]) 19 | # Get first value using exact date. 20 | self.assertEqual(curve.get_price(date=datetime.datetime(2011, 1, 1)), 1) 21 | # Get first value using later date. 22 | self.assertEqual(curve.get_price(date=datetime.datetime(2011, 1, 2)), 1) 23 | # Get second value using exact date. 24 | self.assertEqual(curve.get_price(date=datetime.datetime(2011, 1, 3)), 3) 25 | # Get second value using later date. 26 | self.assertEqual(curve.get_price(date=datetime.datetime(2011, 1, 4)), 3) 27 | 28 | # Check raises KeyError for values before first date. 29 | with self.assertRaises(KeyError): 30 | curve.get_price(date=datetime.datetime(2010, 12, 31)) 31 | -------------------------------------------------------------------------------- /quantdsl/tests/test_least_squares.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import scipy 4 | 5 | from quantdsl.semantics import LeastSquares 6 | 7 | 8 | class TestLeastSquares(unittest.TestCase): 9 | 10 | DECIMALS = 12 11 | 12 | def assertFit(self, fixture_x, fixture_y, expected_values): 13 | assert expected_values is not None 14 | ls = LeastSquares(scipy.array(fixture_x), scipy.array(fixture_y)) 15 | fit_data = ls.fit() 16 | for i, expected_value in enumerate(expected_values): 17 | fit_value = round(fit_data[i], self.DECIMALS) 18 | msg = "expected_values value: %s, fit value: %s, expected_values data: %s, fit data: %s" % ( 19 | expected_value, fit_value, expected_values, fit_data) 20 | self.assertEqual(expected_value, fit_value, msg) 21 | 22 | def test_fit1(self): 23 | self.assertFit( 24 | fixture_x=[ 25 | [0, 1, 2], 26 | [3, 4, 5]], 27 | fixture_y=[1, 1, 1], 28 | expected_values=[1, 1, 1], 29 | ) 30 | 31 | def test_fit2(self): 32 | self.assertFit( 33 | fixture_x=[[0, 1, 2], [3, 4, 5]], 34 | fixture_y=[0, 1, 2], 35 | expected_values=[0, 1, 2], 36 | ) 37 | -------------------------------------------------------------------------------- /quantdsl/tests/test_market_calibration.py: -------------------------------------------------------------------------------- 1 | from quantdsl.tests.test_application import ApplicationTestCase 2 | 3 | # Todo: More about market calibration, especially generating the calibration params from historical data. 4 | 5 | 6 | class TestMarketCalibration(ApplicationTestCase): 7 | pass -------------------------------------------------------------------------------- /quantdsl/tests/test_market_simulation.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from quantdsl.domain.model.market_simulation import MarketSimulation 4 | from quantdsl.domain.model.simulated_price import make_simulated_price_id, SimulatedPrice 5 | from quantdsl.defaults import DEFAULT_PRICE_PROCESS_NAME 6 | from quantdsl.tests.test_application import ApplicationTestCase 7 | 8 | 9 | class TestMarketSimulation(ApplicationTestCase): 10 | 11 | NUMBER_MARKETS = 2 12 | NUMBER_DAYS = 5 13 | PATH_COUNT = 200 14 | 15 | def test_register_market_simulation(self): 16 | # Set up the market calibration. 17 | price_process_name = DEFAULT_PRICE_PROCESS_NAME 18 | calibration_params = { 19 | 'market': ['#1', '#2'], 20 | 'sigma': [0.1, 0.2], 21 | 'curve': { 22 | '#1': [ 23 | ('2011-1-1', 10), 24 | ], 25 | '#2': [ 26 | ('2011-1-1', 20), 27 | ], 28 | }, 29 | 'rho': [ 30 | [1.0, 0.5], 31 | [0.5, 1.0], 32 | ], 33 | } 34 | market_calibration = self.app.register_market_calibration(price_process_name, calibration_params) 35 | 36 | # Create a market simulation for a list of markets and fixing times. 37 | commodity_names = ['#%d' % (i+1) for i in range(self.NUMBER_MARKETS)] 38 | fixing_dates = [datetime.datetime(2011, 1, 1) + datetime.timedelta(days=i) for i in range(self.NUMBER_DAYS)] 39 | observation_date = fixing_dates[0] 40 | simulation_requirements = [] 41 | for commodity_name in commodity_names: 42 | for fixing_date in fixing_dates: 43 | simulation_requirements.append((commodity_name, fixing_date, fixing_date)) 44 | path_count = self.PATH_COUNT 45 | 46 | market_simulation = self.app.register_market_simulation( 47 | market_calibration_id=market_calibration.id, 48 | requirements=simulation_requirements, 49 | observation_date=observation_date, 50 | path_count=path_count, 51 | interest_rate=2.5, 52 | ) 53 | 54 | assert isinstance(market_simulation, MarketSimulation) 55 | assert market_simulation.id 56 | market_simulation = self.app.market_simulation_repo[market_simulation.id] 57 | assert isinstance(market_simulation, MarketSimulation) 58 | self.assertEqual(market_simulation.market_calibration_id, market_calibration.id) 59 | # self.assertEqual(market_simulation.requirements[0], ['#1', '#2']) 60 | # self.assertEqual(market_simulation.fixing_dates, [datetime.date(2011, 1, i) for i in range(2, 6)]) 61 | self.assertEqual(market_simulation.observation_date, datetime.datetime(2011, 1, 1)) 62 | self.assertEqual(market_simulation.path_count, self.PATH_COUNT) 63 | 64 | # Check there are simulated prices for all the requirements. 65 | for requirement in simulation_requirements: 66 | commodity_name = requirement[0] 67 | fixing_date = requirement[1] 68 | delivery_date = requirement[2] 69 | simulated_price_id = make_simulated_price_id(market_simulation.id, commodity_name, fixing_date, delivery_date) 70 | simulated_price = self.app.simulated_price_repo[simulated_price_id] 71 | self.assertIsInstance(simulated_price, SimulatedPrice) 72 | self.assertTrue(simulated_price.value.mean()) -------------------------------------------------------------------------------- /quantdsl/tests/test_quantdsl_lib.py: -------------------------------------------------------------------------------- 1 | from quantdsl.tests.test_application import ApplicationTestCase 2 | 3 | 4 | class ExpressionTests(ApplicationTestCase): 5 | def test_european1(self): 6 | specification = """ 7 | from quantdsl.lib.european1 import European 8 | 9 | European(Date('2012-01-11'), 9, Market('NBP')) 10 | """ 11 | self.assert_contract_value(specification, 2.397, {}, expected_call_count=None) 12 | 13 | def test_american1(self): 14 | specification = """ 15 | from quantdsl.lib.american1 import American 16 | 17 | American(Date('2012-01-01'), Date('2012-01-11'), 5, Market('NBP'), TimeDelta('1d')) 18 | """ 19 | self.assert_contract_value(specification, 5.0356, {}, expected_call_count=None) 20 | 21 | def test_storage1(self): 22 | specification_tmpl = """ 23 | from quantdsl.lib.storage1 import GasStorage 24 | 25 | GasStorage(Date('%(start_date)s'), Date('%(end_date)s'), '%(commodity)s', %(quantity)s, %(limit)s, TimeDelta('1m')) 26 | """ 27 | # No capacity. 28 | specification = specification_tmpl % { 29 | 'start_date': '2011-1-1', 30 | 'end_date': '2011-3-1', 31 | 'commodity': 'NBP', 32 | 'quantity': 0, 33 | 'limit': 0 34 | } 35 | self.assert_contract_value(specification, 0.00, {}, expected_call_count=2) 36 | 37 | # Capacity, zero inventory. 38 | specification = specification_tmpl % { 39 | 'start_date': '2011-1-1', 40 | 'end_date': '2011-3-1', 41 | 'commodity': 'NBP', 42 | 'quantity': 0, 43 | 'limit': 10 44 | } 45 | self.assert_contract_value(specification, 0.00, {}, expected_call_count=6) 46 | 47 | # Capacity, zero inventory, option in the future. 48 | specification = specification_tmpl % { 49 | 'start_date': '2013-1-1', 50 | 'end_date': '2013-3-1', 51 | 'commodity': 'NBP', 52 | 'quantity': 0, 53 | 'limit': 10 54 | } 55 | self.assert_contract_value(specification, -0.0015, {}, expected_call_count=6) 56 | 57 | # Capacity, and inventory to discharge. 58 | specification = specification_tmpl % { 59 | 'start_date': '2011-1-1', 60 | 'end_date': '2011-3-1', 61 | 'commodity': 'NBP', 62 | 'quantity': 2, 63 | 'limit': 2 64 | } 65 | self.assert_contract_value(specification, 19.982, {}, expected_call_count=10) 66 | 67 | # Capacity, and inventory to discharge in future. 68 | specification = specification_tmpl % { 69 | 'start_date': '2021-1-1', 70 | 'end_date': '2021-3-1', 71 | 'commodity': 'NBP', 72 | 'quantity': 2, 73 | 'limit': 2 74 | } 75 | self.assert_contract_value(specification, 15.971, {}, expected_call_count=10) 76 | 77 | # Capacity, zero inventory, in future. 78 | specification = specification_tmpl % { 79 | 'start_date': '2021-1-1', 80 | 'end_date': '2021-3-1', 81 | 'commodity': 'NBP', 82 | 'quantity': 0, 83 | 'limit': 2 84 | } 85 | self.assert_contract_value(specification, 0.0024, {}, expected_call_count=6) 86 | -------------------------------------------------------------------------------- /quantdsl/tests/test_readme.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from os.path import dirname, join 4 | from subprocess import PIPE, Popen 5 | from unittest.case import TestCase 6 | 7 | import quantdsl 8 | 9 | 10 | class TestReadmeFile(TestCase): 11 | def test_code_snippets_in_readme_file(self): 12 | # Extract lines of Python code from the README.md file. 13 | readme_filename = 'README.md' 14 | readme_path = join(dirname(dirname(quantdsl.__file__)), readme_filename) 15 | 16 | if not os.path.exists(readme_path): 17 | self.skipTest("Skipped test because usage instructions in README file are " 18 | "not available for testing once this package is installed") 19 | 20 | temp_filename = '.README.py' 21 | temp_path = os.path.join(os.path.dirname(readme_path), temp_filename) 22 | lines = [] 23 | count_code_lines = 0 24 | is_code = False 25 | with open(readme_path) as file: 26 | for line in file: 27 | if is_code and line.startswith('```'): 28 | is_code = False 29 | line = '' 30 | elif is_code: 31 | line = line.strip('\n') 32 | count_code_lines += 1 33 | elif line.strip() == '```python': 34 | is_code = True 35 | line = '' 36 | else: 37 | line = '' 38 | lines.append(line) 39 | 40 | self.assertTrue(count_code_lines) 41 | 42 | # Write the code into a temp file. 43 | with open(temp_path, 'w+') as readme_py: 44 | readme_py.writelines("\n".join(lines) + '\n') 45 | 46 | # Run the code and catch errors. 47 | p = Popen([sys.executable, temp_path], stdout=PIPE, stderr=PIPE) 48 | out, err = p.communicate() 49 | out = out.decode('utf8').replace(temp_filename, readme_filename) 50 | err = err.decode('utf8').replace(temp_filename, readme_filename) 51 | exit_status = p.wait() 52 | 53 | # Check for errors running the code. 54 | self.assertEqual(exit_status, 0, u"Usage exit status {}:\n{}\n{}".format(exit_status, out, err)) 55 | 56 | # Delete the temp file. 57 | os.unlink(temp_path) 58 | -------------------------------------------------------------------------------- /quantdsl/tests/test_settlements.py: -------------------------------------------------------------------------------- 1 | # Todo: Get the evaluation to return settlements by date, by picking paths. 2 | 3 | # import datetime 4 | # from unittest.case import TestCase 5 | # 6 | # from quantdsl.semantics import Settlement, Date, Number 7 | # 8 | # class TestSettlements(TestCase): 9 | # def _test(self): 10 | # e = Settlement(Date('2012-1-1'), Number(10)) 11 | # result = e.evaluate( 12 | # interest_rate=2.5, 13 | # present_time=datetime.datetime(2011, 1, 1) 14 | # ) 15 | # self.fail(result) -------------------------------------------------------------------------------- /quantdsl/tests/test_simulation_subscriber.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from eventsourcing.domain.model.events import publish, assert_event_handlers_empty 3 | from mock import patch 4 | from mock import MagicMock 5 | import unittest 6 | 7 | from quantdsl.domain.model.market_calibration import MarketCalibrationRepository 8 | from quantdsl.domain.model.market_simulation import MarketSimulation, MarketSimulationRepository 9 | from quantdsl.domain.model.simulated_price import SimulatedPriceRepository 10 | from quantdsl.infrastructure.simulation_subscriber import SimulationSubscriber 11 | 12 | 13 | class TestSimulationSubscriber(unittest.TestCase): 14 | 15 | def setUp(self): 16 | assert_event_handlers_empty() 17 | market_simulation_repo = MagicMock(spec=MarketSimulationRepository) 18 | market_calibration_repo = MagicMock(spec=MarketCalibrationRepository) 19 | simulated_price_repo = MagicMock(spec=SimulatedPriceRepository) 20 | self.simulation_subscriber = SimulationSubscriber( 21 | market_calibration_repo, 22 | market_simulation_repo, 23 | simulated_price_repo 24 | ) 25 | 26 | def tearDown(self): 27 | self.simulation_subscriber.close() 28 | assert_event_handlers_empty() 29 | 30 | @patch('quantdsl.infrastructure.simulation_subscriber.generate_simulated_prices') 31 | def test_simulation_subscriber(self, generate_simulated_prices): 32 | # Check that when an event is published, the domain service is called. 33 | market_simulation_created = MarketSimulation.Created( 34 | entity_id='1', 35 | market_calibration_id='1', 36 | market_names=[], 37 | fixing_dates=[], 38 | observation_date=datetime.date(2011, 1, 1,), 39 | path_count=2, 40 | interest_rate=2.5, 41 | ) 42 | self.assertEqual(generate_simulated_prices.call_count, 0) 43 | publish(market_simulation_created) 44 | self.assertEqual(generate_simulated_prices.call_count, 1) 45 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argh 2 | mock==1.0.1 3 | scipy 4 | python-dateutil==2.2 5 | requests 6 | six 7 | eventsourcing==0.9.4 8 | pytz 9 | blist 10 | pandas 11 | pandas_datareader 12 | #memory_profiler 13 | #psutil 14 | matplotlib 15 | #numexpr 16 | -------------------------------------------------------------------------------- /scripts/prepare-distribution.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | try: 7 | del os.environ['PYTHONPATH'] 8 | except KeyError: 9 | pass 10 | 11 | 12 | def get_version(): 13 | return [line.split('=')[1].strip().strip(",").strip("'") for line in open('../quantdsl/__init__.py').readlines() if '__version__' in line][0] 14 | 15 | 16 | def build_and_test(cwd): 17 | # Declare temporary working directory variable. 18 | tmpcwd27 = os.path.join(cwd, 'tmpve2.7') 19 | tmpcwd36 = os.path.join(cwd, 'tmpve3.6') 20 | 21 | for (tmpcwd, python_executable) in [(tmpcwd36, 'python3'), (tmpcwd27, 'python')]: 22 | 23 | # Rebuild virtualenvs. 24 | rebuild_virtualenv(cwd, tmpcwd, python_executable) 25 | 26 | # Upgrade pip. 27 | subprocess.check_call(['bin/pip', 'install', '--upgrade', 'pip'], cwd=tmpcwd) 28 | subprocess.check_call(['bin/pip', 'install', '--upgrade', 'setuptools'], cwd=tmpcwd) 29 | 30 | # Install from project folder. 31 | # - assumes we are starting in the scripts folder 32 | subprocess.check_call(['bin/pip', 'install', '..'], cwd=tmpcwd) 33 | 34 | # Check installed tests all pass. 35 | test_installation(tmpcwd) 36 | # 37 | # # Rebuild virtualenvs. 38 | # rebuild_virtualenv(cwd, tmpcwd, python_executable) 39 | # 40 | # # Build and upload to Test PyPI. 41 | # subprocess.check_call([sys.executable, 'setup.py', 'sdist', 'upload', '-r', 'pypitest'], cwd=cwd) 42 | # 43 | # # Install from Test PyPI. 44 | # subprocess.check_call(['bin/pip', 'install', '--upgrade', 'pip'], cwd=tmpcwd) 45 | # 46 | # subprocess.check_call(['bin/pip', 'install', 'quantdsl=='+get_version(), 47 | # '--index-url', 'https://testpypi.python.org/simple', 48 | # '--extra-index-url', 'https://pypi.python.org/simple' 49 | # ], 50 | # cwd=tmpcwd) 51 | 52 | # # Check installed tests all pass. 53 | # test_installation(tmpcwd) 54 | # 55 | remove_virtualenv(cwd, tmpcwd) 56 | 57 | 58 | def test_installation(tmpcwd): 59 | subprocess.check_call(['bin/python', '-m', 'dateutil.parser'], cwd=tmpcwd) 60 | subprocess.check_call(['bin/python', '-m' 'unittest', 'discover', 'quantdsl'], cwd=tmpcwd) 61 | 62 | 63 | def rebuild_virtualenv(cwd, venv_path, python_executable): 64 | remove_virtualenv(cwd, venv_path) 65 | subprocess.check_call(['virtualenv', '-p', python_executable, venv_path], cwd=cwd) 66 | 67 | 68 | def remove_virtualenv(cwd, venv_path): 69 | subprocess.check_call(['rm', '-rf', venv_path], cwd=cwd) 70 | 71 | 72 | if __name__ == '__main__': 73 | cwd = os.path.join(os.environ['HOME'], 'PyCharmProjects', 'quantdsl') 74 | build_and_test(cwd) 75 | 76 | -------------------------------------------------------------------------------- /scripts/release-distribution.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def build_and_release(cwd): 8 | # Build and upload to PyPI. 9 | subprocess.check_call(['pip', 'install', '-U', 'pip', 'setuptools', 'twine'], cwd=cwd) 10 | subprocess.check_call(['rm', '-rf', 'dist/'], cwd=cwd) 11 | subprocess.check_call([sys.executable, 'setup.py', 'sdist'], cwd=cwd) 12 | subprocess.check_call(['twine', 'upload', 'dist/*'], cwd=cwd) 13 | 14 | 15 | if __name__ == '__main__': 16 | cwd = os.path.join(os.environ['HOME'], 'PyCharmProjects', 'quantdsl') 17 | build_and_release(cwd) 18 | -------------------------------------------------------------------------------- /scripts/test-released-distribution.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import subprocess 4 | 5 | 6 | if 'PYTHONPATH' in os.environ: 7 | del os.environ['PYTHONPATH'] 8 | 9 | 10 | def get_version(): 11 | return [line.split('=')[1].strip().strip(",").strip("'") for line in open('../quantdsl/__init__.py').readlines() if '__version__' in line][0] 12 | 13 | 14 | def test_released_distribution(cwd): 15 | # Declare temporary working directory variable. 16 | tmpcwd27 = os.path.join(cwd, 'tmpve2.7') 17 | tmpcwd34 = os.path.join(cwd, 'tmpve3.4') 18 | 19 | for (tmpcwd, python_executable) in [(tmpcwd27, 'python2.7'), (tmpcwd34, 'python3.4')]: 20 | 21 | # Rebuild virtualenvs. 22 | rebuild_virtualenv(cwd, tmpcwd, python_executable) 23 | 24 | # Install from PyPI. 25 | subprocess.check_call(['bin/pip', 'install', '-U', 'pip'], cwd=tmpcwd) 26 | subprocess.check_call(['bin/pip', 'install', '--no-cache-dir', '-U', 'quantdsl[test]'], cwd=tmpcwd) 27 | 28 | # Check installed tests all pass. 29 | test_installation(tmpcwd) 30 | 31 | 32 | def test_installation(tmpcwd): 33 | subprocess.check_call(['bin/python', '-m', 'dateutil.parser'], cwd=tmpcwd) 34 | subprocess.check_call(['bin/python', '-m' 'unittest', 'discover', 'quantdsl'], cwd=tmpcwd) 35 | 36 | 37 | def rebuild_virtualenv(cwd, venv_path, python_executable): 38 | subprocess.check_call(['rm', '-rf', venv_path], cwd=cwd) 39 | subprocess.check_call(['virtualenv', '-p', python_executable, venv_path], cwd=cwd) 40 | 41 | 42 | if __name__ == '__main__': 43 | cwd = os.path.join(os.environ['HOME'], 'PyCharmProjects', 'quantdsl') 44 | test_released_distribution(cwd) 45 | 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | from setuptools import setup, find_packages 4 | 5 | long_description = """ 6 | Quant DSL is a functional programming language for modelling derivative instruments. 7 | 8 | At the heart of Quant DSL is a set of built-in elements (e.g. "Market", "Choice", "Wait") that encapsulate maths used in finance and trading (i.e. models of market dynamics, the least-squares Monte Carlo approach, time value of money calculations) and which can be composed into executable expressions of value. 9 | 10 | User defined functions are supported, and can be used to generate massive expressions. The syntax of Quant DSL expressions has been formally defined, and the semantic model is supported with mathematical proofs. The Python package quantdsl is an implementation in Python of the Quant DSL syntax and semantics. 11 | 12 | An extensive `README file is available on GitHub `_. 13 | """ 14 | 15 | from quantdsl import __version__ 16 | 17 | 18 | setup( 19 | name='quantdsl', 20 | version=__version__, 21 | packages=find_packages(), 22 | zip_safe=False, 23 | install_requires=[ 24 | 'argh', 25 | 'mock==1.0.1', 26 | 'scipy', 27 | 'python-dateutil==2.2', 28 | 'requests', 29 | 'six', 30 | 'eventsourcing==0.9.4', 31 | 'pytz', 32 | 'blist', 33 | 'pandas', 34 | 'pandas_datareader', 35 | 'matplotlib', 36 | # 'memory_profiler', 37 | # 'psutil', 38 | # 'numexpr' 39 | ], 40 | 41 | scripts=[], 42 | author='John Bywater', 43 | author_email='john.bywater@appropriatesoftware.net', 44 | license='BSD', 45 | url='https://github.com/johnbywater/quantdsl', 46 | description='Domain specific language for quantitative analytics in finance.', 47 | long_description=long_description, 48 | classifiers=[ 49 | 'Development Status :: 4 - Beta', 50 | 'Environment :: Console', 51 | 'Intended Audience :: Developers', 52 | 'Intended Audience :: Education', 53 | 'Intended Audience :: Financial and Insurance Industry', 54 | 'Intended Audience :: Science/Research', 55 | 'License :: OSI Approved :: BSD License', 56 | 'Operating System :: OS Independent', 57 | 'Programming Language :: Python', 58 | 'Programming Language :: Python :: 2.7', 59 | 'Programming Language :: Python :: 3.3', 60 | 'Programming Language :: Python :: 3.4', 61 | 'Programming Language :: Python :: 3.5', 62 | 'Topic :: Office/Business :: Financial', 63 | 'Topic :: Office/Business :: Financial :: Investment', 64 | 'Topic :: Office/Business :: Financial :: Spreadsheet', 65 | 'Topic :: Scientific/Engineering :: Information Analysis', 66 | 'Topic :: Scientific/Engineering :: Mathematics', 67 | 'Topic :: Software Development :: Libraries :: Python Modules', 68 | ], 69 | ) 70 | --------------------------------------------------------------------------------