# Quant DSL ***Domain specific language for quantitative analytics in finance and trading.*** [![Build Status](https://secure.travis-ci.org/johnbywater/quantdsl.png)](https://travis-ci.org/johnbywater/quantdsl) [![Coverage Status](https://coveralls.io/repos/github/johnbywater/quantdsl/badge.svg)](https://coveralls.io/github/johnbywater/quantdsl) Example of an American option expressed in Quant DSL. ```python skip-test AmericanOption(Date('2011-1-1'), Date('2012-1-1'), Market('Copper'), 7000, TimeDelta('1d')) def AmericanOption(start, expiry, underlying, strike, step): if start <= expiry: Option(start, underlying, strike, AmericanOption( start + step, expiry, underlying, strike, step )) else: 0 def Option(expiry, underlying, strike, alternative): Wait(expiry, Choice(underlying - strike, alternative)) ``` ## Install Use pip to install the [latest distribution](https://pypi.python.org/pypi/quantdsl) from the Python Package Index. You may feedback [issues on GitHub](https://github.com/johnbywater/quantdsl/issues). ``` pip install quantdsl ``` Please note, this library depends on SciPy, which can fail to install with some older versions of pip. In case of difficulty, please try again after upgrading pip. ``` pip install --upgrade pip ``` After successfully installing this library, the test suite should pass. ``` python -m unittest discover quantdsl ``` ## Overview Quant DSL is domain specific language for quantitative analytics in finance and trading. At the heart of Quant DSL is a set of elements (e.g. "Settlement", "Fixing", "Market", "Choice"). The elements involve mathematical expressions commonly used within quantitative analytics, such as: [present value discounting](https://en.wikipedia.org/wiki/Present_value); [geometric Brownian motion](https://en.wikipedia.org/wiki/Geometric_Brownian_motion); and [least squares Monte Carlo](https://en.wikipedia.org/wiki/Monte_Carlo_methods_for_option_pricing#Least_Square_Monte_Carlo). The elements of the language can be freely composed into expressions of value. The validity of Monte Carlo simulation for all possible expressions in the language is [proven by induction](http://www.appropriatesoftware.org/quant/docs/quant-dsl-definition-and-proof.pdf). ### Syntax The syntax of Quant DSL expressions is defined with [Backus–Naur Form](https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form). ``` ::= | "Settlement(" "," ")" | "Fixing(" "," ")" | "Market(" ")" | "Wait(" "," ")" | "Choice(" "," ")" | "Max(" "," ")" | "+" | "-" | "*" | "/" | "-" ::= | ::= "'""-""-""'" ::= "'" | "'" ::= ::= ::= ::= "." ::= | ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" | "U" | "V" | "W" | "X" | "Y" | "Z" | "_" | ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" ``` ### Semantics In the definitions below, Quant DSL expression `v` defines a function `[[v]](t)` from present time `t` to a random variable in a probability space. Constant interest rate `r` is used in discounting settlements. For market `i`, the last price `Si` and volatility `σi` are determined using only market price data generated before `t0`. Brownian motion `z` is used in diffusion. In the software, this `Market` semantic is a user option, so that other market models can be used to simulate prices, for example models with multiple factors, mean reversion, and jump diffusion. Choices are made using conditioning, expectation `E` is conditioned on filtration `F` at `t`, so that choices are made only with information at the time of the choice. In practice, information is "lost" from included expressions by fitting the value of the expression (a random variable in a probability space) to the underlying simulated prices at that time using least squares. This leads to a quantitative difference from maximisation ("Max"). ``` [[Settlement(d, x)]](t) = e ** (r * (t−d)) * [[x]](t) ``` ``` [[Fixing(d, x)]](t) = [[x]](d) ``` ``` [[Market(i)]](t) = Si * e ** (σi * z(t − t0)) − 0.5 * σi ** 2 * (t − t0) ``` ``` [[Wait(d, x)]](t) = [[Settlement(d, Fixing(d, x))]](t) ``` ``` [[Choice(x, y)]](t) = max(E[[[x]](t) | F(t)], E[[[y]](t) | F(t)]) ``` ``` [[Max(x, y)]](t) = max([[x]](t), [[y]](t)) ``` ``` [[x + y]](t) = [[x]](t) + [[y]](t) ``` ``` [[x - y]](t) = [[x]](t) - [[y]](t) ``` ``` [[x * y]](t) = [[x]](t) * [[y]](t) ``` ``` [[x / y]](t) = [[x]](t) / [[y]](t) ``` ``` [[-x]](t) = -[[x]](t) ``` Todo: Format the above as proper math symbols. Please note, these default semantics can be substituted using the `dsl_classes` arg of `calc()` below. ### Software The scope of the work of a quantitative analyst involves modelling optionality, simulating future prices, and evaluating the model against the simulation. In implementing the syntax and semantics of Quant DSL, this software provides an application object class `QuantDslApplication` which has methods that support this work: `compile()`, `simulate()`, and `evaluate()`. During *compilation* of Quant DSL source code, the application `QuantDslApplication` constructs a graph of Quant DSL expressions. The *simulation* is generated by a calibrated price process, according to requirements derived from the compiled dependency graph. During *evaluation*, the nodes of the dependency graph are evaluated when they are ready to be evaluated. Intermediate results are discarded as soon as they are no longer required, such that memory usage is mostly constant during evaluation. For the "greeks", nodes are selectively re-evaluated with perturbed values, according to the periods and markets they involve, avoiding unnecessary computation. In addition to the Quant DSL expressions above, function `def` statements are supported. User defined functions can be used to refactor complex Quant DSL expressions, in order to model complex optionality concisely. The `import` statement is also supported, so Quant DSL function definitions and expressions can be developed and maintained as Python files. Since Quant DSL syntax is a strict subset of Python, the full power of Python IDEs can be used to write, navigate and refactor Quant DSL source code. ## Examples ### calc() The examples below use the library function `calc()` to evaluate Quant DSL source code. `calc()` uses the methods of the `QuantDslApplication` mentioned above. ```python from quantdsl.calculate import calc ``` When called, the function `calc()` returns a results object, with an attribute `fair_value` that is the simulated value, under the semantics, of the given Quant DSL expression. ```python results = calc(source_code="2 + 3") assert results.fair_value == 5 ``` The function `calc()` has many optional arguments that can be used to control the evaluation of an expression. **`source_code`** The Quant DSL module that will be evaluated. It must contain one expression, and may contain many function definitions. It may also contain import statements that import functions from Quant DSL modules saved as normal Python files. **`observation_date`** Sets the time `t0` in the semantics from when the forward curve is evolved by the price process. Also conditions the effective present time `t` of the outermost element in an evaluation. **`interest_rate`** The continuously compounding short run risk free rate, used for discounting (default `0`). **`price_process`** The name and configuration parameters for the price process that will estimate (by simulation) prices that could be agreed in the future. **`periodisation`** Delta granularity can be set with the `periodisation` arg of `calc()` e.g. `'daily'` or `'monthly'`. Todo: Rename the arg to `delta_granularity`. **`is_double_sided_deltas`** Evaluate with either single- or double-sided deltas (default `True`). **`path_count`** Determines the accuracy of the simulated random variables (default `20000`). **`perturbation_factor`** Used to calculate "greeks". If the `path_count` is larger, a smaller perturbation factor may give better result (default `0.01`). **`max_dependency_graph_size`** Sets the limit on the maximum number of nodes the can be compiled from Quant DSL source with the `max_dependency_graph_size` arg of `calc()`. **`timeout`** You can set a calculation to `timeout` after a given number of seconds. **`dsl_classes`** Custom DSL classes can be passed in using the `dsl_classes` argument of `calc()`. **`is_verbose`** Setting `is_verbose` will cause progress of a calculation to be printed to standard output. ### Settlement The `Settlement` element discounts the value of the included `Expression` from its given `Date` to the effective present time `t` when the element is evaluated. ``` ::= "Settlement(" ", " ")" ``` ``` [[Settlement(d, x)]](t) = e ** (r * (t−d)) * [[x]](t) ``` For example, with a continuously compounding `interest_rate` of `2.5` percent per year, the value `10` settled on `'2111-1-1'` has a present value of `82.08` on `'2011-1-1'`. ```python results = calc("Settlement('2111-1-1', 1000)", observation_date='2011-1-1', interest_rate=2.5, ) assert round(results.fair_value, 2) == 82.08 ``` Similarly, the value of `82.085` settled in `'2011-1-1'` has a present value of `1000.00` on `'2111-1-1'` . ```python results = calc("Settlement('2011-1-1', 82.085)", observation_date='2111-1-1', interest_rate=2.5, ) assert round(results.fair_value, 2) == 1000.00 ``` Discounting is a function of the `interest_rate` and the duration in time between the date of the `Settlement` element and the effective present time `t` of its evaluation. The formula used for discounting by the `Settlement` element is `e**-rt`. The `interest_rate` is the therefore the continuously compounding risk free rate (not the annual equivalent rate). ### Fixing The `Fixing` element simply conditions the effective present time of its included `Expression` with its given `Date`. ``` ::= "Fixing(" "," ")" ``` ``` [[Fixing(d, x)]](t) = [[x]](d) ``` For example, if a `Fixing` element includes a `Settlement` element, then the effective present time of the included `Settlement` element will be the given date of the `Fixing`. The expression below represents the present value in `'2051-1-1'` of the value of `1000` to be settled on `'2111-1-1'`. ```python results = calc("Fixing('2051-1-1', Settlement('2111-1-1', 1000))", interest_rate=2.5, ) assert round(results.fair_value, 2) == 223.13 ``` ### Market The `Market` element effectively estimates prices that could be agreed in the future. ``` ::= "Market(" ")" ``` ``` [[Market(i)]](t) = Si * e ** (σi * z(t − t0)) − 0.5 * σi ** 2 * (t − t0) ``` When a `Market` element is evaluated, it returns a random variable selected from a simulation of market prices. Selecting an estimated price from the simulation requires the name of the market, a *fixing date* (when the price would be agreed), and a *delivery date* (when the goods would be delivered). The name of the `Market` is included in the element (e.g. `'GAS'` or `'POWER'`). Both the fixing date and the delivery date are determined by the effective present time when the element is evaluated. #### Simulation The price simulation is generated by a price process. In this example, the library's one-factor multi-market Black Scholes price process `BlackScholesPriceProcess` is used to generate correlated geometric Brownian motions. The calibration parameters required by `BlackScholesPriceProcess` are `market` (a list of market names), and `sigma`, (a list of annualised historical volatilities, expressed as a fraction of 1, rather than as a percentage). ```python price_process = { 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 'market': ['GAS', 'POWER'], 'sigma': [0.02, 0.02], 'rho': [ [1.0, 0.8], [0.8, 1.0] ], 'curve': { 'GAS': [ ('2011-1-1', 10), ('2111-1-1', 1000) ], 'POWER': [ ('2011-1-1', 11), ('2111-1-1', 1100) ] }, } ``` When a simulation generated by this price process involves two or more markets, an additional parameter `rho` is required, which represents the correlation between the markets (a symmetric matrix expressed as a list of lists). #### Forward curve A forward `curve` is required by the price process to provide estimates of current prices for each market at the given `observation_date`. The prices in the forward curve are prices that can be agreed at the `observation_date` for delivery at the specified dates. These prices are then diffused according to the risk-neutral dynamics of the price process. Requirements for the simulation (dates and markets) are derived from the expression to be evaluated. If the expression involves observing at a future date the price for particular goods to be delivered at a particular date, then the simulation will provide a random variable which simulates that observation. A `Market` element evaluated at the `observation_date` will simply return the last value from the given forward curve for that market at the given `observation_date`. ```python results = calc("Market('GAS')", observation_date='2011-1-1', price_process=price_process, ) ``` Since the `Market` element uses random variables from the price simulation, so the results are random variables, and we need to take the `mean()` to obtain a scalar value. ```python assert results.fair_value.mean() == 10 ``` If the forward curve doesn't contain a price at the required delivery date, a price at an earlier delivery date is used (with zero order hold). ```python results = calc("Market('GAS')", observation_date='2012-3-4', price_process=price_process, ) assert results.fair_value.mean() == 10 ``` Evaluating at a later observation date will return the later value from the forward curve. ```python results = calc("Market('GAS')", observation_date='2111-1-1', price_process=price_process, ) assert results.fair_value.mean() == 1000 ``` #### Stochastic evolution In the examples so far, there has been no difference between the fixing date of the `Market` element and the `observation_date` of the evaluation. Therefore, there is no stochastic evolution of the forward curve, and the standard deviation of the result value is zero. ```python assert results.fair_value.std() == 0.0 ``` If a `Market` element is included within a `Fixing` element, the effective present time will be different from the observation date, so the fixing date used to select a price from the simulation will be different from the observation date. The simulated price will be taken from the forward curve, but will also be subjected to stochastic evolution. With Brownian motion provided by the `BlackScholesPriceProcess`, the random variable used to estimate a price observed in the future has a statistical distribution with non-zero standard deviation. ```python results = calc("Fixing('2051-1-1', Market('GAS'))", observation_date='2011-1-1', price_process=price_process, ) assert results.fair_value.std() > 0.0 ``` #### Accuracy The number of samples from the distribution in the simulated random variable defaults to `20000`. ```python assert len(results.fair_value) == 20000 ``` The number can be adjusted by setting `path_count`. The accuracy of results can be doubled by increasing the path count by a factor of four. ```python results = calc("Market('GAS')", observation_date='2011-1-1', price_process=price_process, path_count=80000, ) assert len(results.fair_value) == 80000 ``` #### Different fixing and delivery dates The `ForwardMarket` element can be used to specify a delivery date that is different from the fixing date (when the price for that delivery would be agreed). This can be used to model trading in forward markets, and also to express the value of an index at maturity (see below). ### Wait The `Wait` element combines `Settlement` and `Fixing`, so that its `Date` is used both to condition the effective present time of the included `Expression`, and also the value of that expression is discounted to the effective present time when evaluating the `Wait` element. ``` ::= "Wait(" "," ")" ``` ``` [[Wait(d, x)]](t) = [[Settlement(d, Fixing(d, x))]](t) ``` For example, the present value at the `observation_date` of `'2011-1-1'` of one unit of `'GAS'` delivered on `'2111-1-1'` is approximately `82.18`. The `Wait` element sets the delivery date of the `Market` element, which is used to pick the value `1000` from the forward curve. The `Wait` element also sets the fixing date of the `Market` element, which is used to control the stochastic evolution of the forward value. The evolved value is then discounted by the `Wait` element from `2111` to the observation date `2011`. ```python import scipy # Setting random seed makes test results repeatable. scipy.random.seed(1234) results = calc("Wait('2111-1-1', Market('GAS'))", price_process=price_process, observation_date='2011-1-1', interest_rate=2.5, ) assert round(results.fair_value.mean(), 2) == 82.18 assert round(results.fair_value.std(), 2) == 16.46 ``` ### Choice The `Choice` element uses the least-squares Monte Carlo approach (Longstaff Schwartz, 1998) to compare the conditional expected value of each alternative `Expression`. ``` ::= "Choice(" "," ")" ``` ``` [[Choice(x, y)]](t) = max(E[[[x]](t) | F(t)], E[[[y]](t) | F(t)]) ``` For example, the value of the choice at `observation_date` of `'2011-1-1'` between one unit of `'GAS'` either on `'2051-1-1'` or `'2111-1-1'` is `217.39`. ```python source_code = """ Choice( Wait('2051-1-1', Market('GAS')), Wait('2111-1-1', Market('GAS')) ) """ results = calc(source_code, observation_date='2011-1-1', price_process=price_process, interest_rate=2.5, ) assert round(results.fair_value.mean(), 2) == 82.06 ``` When the `Choice` element is evaluated, the value of each alternative is regressed as a random variable by least squares with second order polynomial regression to the underlying simulated values at the effective present time of the choice. The choice of alternative on each path is then made using the regressed value (the "conditional expected value") but the chosen value on each path is taken from the unregressed value of the chosen alternative for that path (the "expected continuation value"). The result is a new simulated value that combines the expected continuation value of the alternatives, according to information in the simulation at the time of the choice. This conditioning gives a quantitatively different result from simple maximisation of the alternative expected continuation values (`Max`). ### Function definitions Quant DSL source code can include function definitions. Expressions can involve calls to functions. When evaluating an expression that involves a call to a function definition, the call to the function definition is effectively replaced with the expression returned by the function definition, so that a larger expression is formed. Hence, the body of a function can have only one statement. The call args of the function definition can be used as names in the function definition's expressions. The call arg values will be substituted for those names in the expression when the expression is returned by the function. ```python results = calc(""" def Function(a): 2 * a Function(10) """) assert results.fair_value == 20 ``` Although the function body can have only one statement, that statement can be an if-else block. The call args of the function definition can be used in an if-else block, to select different expressions according to the value of the function call arguments, which effectively implements a "case branch". Each function call becomes a node on a dependency graph. For efficiency, each call is cached, so if a function is called many times with the same argument values (and at the same effective present time), the function is only evaluated once, and the result reused. This allows branched calculations to recombine efficiently. For example, the following Fibonacci function definition will evaluate in linear time (proportional to `n`). ```python results = calc(""" def Fib(n): if n > 1: Fib(n-1) + Fib(n-2) else: n Fib(60) """) assert results.fair_value == 1548008755920 ``` Function definitions can be used to refactor complex expressions. For example, if the expression is the sum of a series of settlements on different dates, the expression without a function definition might be: ```python source_code = """ Settlement(Date('2011-1-1'), 10) + Settlement(Date('2011-2-1'), 10) + Settlement(Date('2011-3-1'), 10) + \ Settlement(Date('2011-4-1'), 10) + Settlement(Date('2011-5-1'), 10) + Settlement(Date('2011-6-1'), 10) + \ Settlement(Date('2011-8-1'), 10) + Settlement(Date('2011-8-1'), 10) + Settlement(Date('2011-9-1'), 10) + \ Settlement(Date('2011-10-1'), 10) + Settlement(Date('2011-11-1'), 10) + Settlement(Date('2011-12-1'), 10) """ results = calc(source_code, observation_date='2011-1-1', interest_rate=10, ) assert round(results.fair_value, 2) == 114.59 ``` Instead the expression could be refactored with a function definition. ```python source_code = """ def Settlements(start, end, installment): if start <= end: Settlement(start, installment) + Settlements(start + TimeDelta('1m'), end, installment) else: 0 Settlements(Date('2011-1-1'), Date('2011-12-1'), 10) """ results = calc(source_code, observation_date='2011-1-1', interest_rate=10, ) assert round(results.fair_value, 2) == 114.67 ``` ### European and American options In general, an option can be expressed as waiting until an `expiry` date to choose between, on one hand, the difference between the value of an `underlying` expression and a `strike` expression, and, on the other hand, an `alternative` expression. ```python def Option(expiry, strike, underlying, alternative): Wait(expiry, Choice(underlying - strike, alternative)) ``` A European option can then be expressed simply as an `Option` with zero alternative. ```python def EuropeanOption(expiry, strike, underlying): Option(expiry, strike, underlying, 0) ``` An American option can be expressed as an `Option` to exercise at a given `strike` price on the `start` date, with the alternative being another `AmericanOption` starting on the next date - and so on until the `expiry` date, when the `alternative` becomes zero. ```python def AmericanOption(start, expiry, strike, underlying, step): if start <= expiry: Option( start, strike, underlying, AmericanOption( start + step, expiry, strike, underlying, step ) ) else: 0 ``` A European put option can be expressed as a `EuropeanOption`, with negated underlying and strike expressions. ```python def EuropeanPut(expiry, strike, underlying): EuropeanOption(expiry, -strike, -underlying) ``` A European stock option can be expressed as a `EuropeanOption`, with the `underlying` being the index at maturity. The `IndexAtMaturity` has the spot price at the observation date (using `ForwardMarket`) observed at a time in the future, discounted forward from the observation date (due to the `Settlement`) to a time in the future. This "time in the future" is the effective present time set by the `Wait` element of the `Option` definition above. ```python def EuropeanStockOption(expiry, strike, stock): EuropeanOption(expiry, strike, IndexAtMaturity(stock)) ``` The expression `IndexAtMaturity` is passed into the `Option` definition and, due to the `Wait` element in that definition, is evaluated with `expiry` as the present time. Therefore the spot price is discounted forward to the `expiry`, and the fixing date of the `ForwardMarket` is `expiry`, so that the spot price is subjected to stochastic evolution as well as interest rates. ```python def IndexAtMaturity(stock): Settlement(ObservationDate(), ForwardMarket(ObservationDate(), stock)) ``` The built-in `ObservationDate` element evaluates to the `observation_date` passed to the the `calc()` function. Let's evaluate a European stock option at different strike prices, volatilities, and interest rates. The following function `calc_european` will make it easier to evaluate the option several times. ```python def calc_european(spot, strike, sigma, rate): source_code = """ def Option(expiry, strike, underlying, alternative): Wait(expiry, Choice(underlying - strike, alternative)) def EuropeanOption(expiry, strike, underlying): Option(expiry, strike, underlying, 0) def EuropeanStockOption(expiry, strike, stock): EuropeanOption(expiry, strike, IndexAtMaturity(stock)) def IndexAtMaturity(stock): Settlement(ObservationDate(), ForwardMarket(ObservationDate(), stock)) EuropeanStockOption(Date('2012-1-1'), {strike}, 'ACME') """.format(strike=strike) results = calc( source_code=source_code, observation_date='2011-1-1', price_process={ 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 'market': ['ACME'], 'sigma': [sigma], 'curve': { 'ACME': [ ('2011-1-1', spot), ] }, }, interest_rate=rate, ) return round(results.fair_value.mean(), 2) ``` If the strike price of a European option is the same as the price of the underlying, without any volatility (`sigma` is `0`) the value is zero. ```python assert calc_european(spot=10, strike=10, sigma=0, rate=0) == 0.0 ``` If the strike price is less than the underlying, without any volatility, the value is the difference between the strike and the underlying. ```python assert calc_european(spot=10, strike=8, sigma=0, rate=0) == 2.0 ``` If the strike price is greater than the underlying, without any volatility, the value is zero. ```python assert calc_european(spot=10, strike=12, sigma=0, rate=0) == 0.0 ``` If the strike price is the same as the underlying, with some volatility in the price of the underlying, there is some value in the option. ```python assert calc_european(spot=10, strike=10, sigma=0.9, rate=0) == 3.42 ``` If the strike price is less than the underlying, with some volatility in the price of the underlying (`sigma`) there is more value in the option than without volatility. ```python assert calc_european(spot=10, strike=8, sigma=0.9, rate=0) == 4.23 ``` If the strike price is greater than the underlying, with some volatility in the price of the underlying (`sigma`) there is still a little bit of value in the option. ```python assert calc_european(spot=10, strike=12, sigma=0.9, rate=0) == 2.90 ``` These results compare well with results from the Black-Scholes analytic formula for European stock options. ### Gas storage An evaluation of a gas storage facility. The value of the gas storage facility follows from the difference between the price when gas is injected and the price when gas is withdrawn. The Quant DSL source code below models a gas storage facility as a lattice of choices to inject or withdraw a quantity of gas. If the facility is full, injecting gas is not an option. Similarly, if the facility is empty, withdrawing gas is not an option. ```python gas_storage = """ def GasStorage(start, end, market, quantity, target, limit, step): if ((start < end) and (limit > 0)): if quantity <= 0: Wait(start, Choice( Continue(start, end, market, quantity, target, limit, step), Inject(start, end, market, quantity, target, limit, step, 1), )) elif quantity >= limit: Wait(start, Choice( Continue(start, end, market, quantity, target, limit, step), Inject(start, end, market, quantity, target, limit, step, -1), )) else: Wait(start, Choice( Continue(start, end, market, quantity, target, limit, step), Inject(start, end, market, quantity, target, limit, step, 1), Inject(start, end, market, quantity, target, limit, step, -1), )) else: if target < 0 or target == quantity: 0 else: BreachOfContract() @inline def Continue(start, end, market, quantity, target, limit, step): GasStorage(start + step, end, market, quantity, target, limit, step) @inline def Inject(start, end, market, quantity, target, limit, step, vol): Continue(start, end, market, quantity + vol, target, limit, step) - \ vol * market @inline def BreachOfContract(): -10000000000000000 @inline def Empty(): 0 @inline def Full(): 50000 GasStorage(Date('2011-4-1'), Date('2012-4-1'), Market('GAS'), Empty(), Empty(), Full(), TimeDelta('1m')) """ ``` This example uses a forward curve that has seasonal variation (prices are high in winter and low in summer). ```python gas = { 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 'market': ['GAS'], 'sigma': [0.3], 'curve': { 'GAS': ( ('2011-1-1', 13.5), ('2011-2-1', 11.0), ('2011-3-1', 10.0), ('2011-4-1', 9.0), ('2011-5-1', 7.5), ('2011-6-1', 7.0), ('2011-7-1', 6.5), ('2011-8-1', 7.5), ('2011-9-1', 8.5), ('2011-10-1', 10.0), ('2011-11-1', 11.5), ('2011-12-1', 12.0), ('2012-1-1', 13.5), ('2012-2-1', 11.0), ('2012-3-1', 10.0), ('2012-4-1', 9.0), ('2012-5-1', 7.5), ('2012-6-1', 7.0), ('2012-7-1', 6.5), ('2012-8-1', 7.5), ('2012-9-1', 8.5), ('2012-10-1', 10.0), ('2012-11-1', 11.5), ('2012-12-1', 12.0) ) } } ``` Because the `periodisation` argument is set to `'monthly'`, the deltas for each market in each month will be calculated, and estimated risk neutral hedge positions will be printed for each market in each period, along with the overall fair value. ```python results = calc( source_code=gas_storage, observation_date='2011-1-1', interest_rate=2.5, periodisation='monthly', price_process=gas, verbose=True, ) assert round(results.fair_value.mean(), 2) == 20.78 print(results) ``` The results, showing deltas for each month for each market, and the fair value. ``` Compiled 92 nodes Compilation in 0.463s Simulation in 0.061s Starting 844 node evaluations, please wait... 844/844 100.00% complete 99.68 eval/s running 9s eta 0s Evaluation in 8.468s 2011-04-01 GAS Price: 9.00 Delta: -0.99 Hedge: 1.00 ± 0.00 Cash: -8.94 ± 0.03 2011-05-01 GAS Price: 7.50 Delta: -0.99 Hedge: 1.00 ± 0.00 Cash: -7.44 ± 0.03 2011-06-01 GAS Price: 7.00 Delta: -0.99 Hedge: 1.00 ± 0.00 Cash: -6.93 ± 0.03 2011-07-01 GAS Price: 6.49 Delta: -0.99 Hedge: 1.00 ± 0.00 Cash: -6.41 ± 0.03 2011-08-01 GAS Price: 7.49 Delta: -0.99 Hedge: 1.00 ± 0.00 Cash: -7.38 ± 0.04 2011-09-01 GAS Price: 8.49 Delta: -0.98 Hedge: 1.00 ± 0.00 Cash: -8.35 ± 0.04 2011-10-01 GAS Price: 9.97 Delta: 0.98 Hedge: -1.00 ± 0.00 Cash: 9.79 ± 0.05 2011-11-01 GAS Price: 11.47 Delta: 0.98 Hedge: -1.00 ± 0.00 Cash: 11.23 ± 0.07 2011-12-01 GAS Price: 11.98 Delta: 0.98 Hedge: -1.00 ± 0.00 Cash: 11.70 ± 0.07 2012-01-01 GAS Price: 13.46 Delta: 0.98 Hedge: -1.00 ± 0.00 Cash: 13.13 ± 0.09 2012-02-01 GAS Price: 10.98 Delta: 0.97 Hedge: -1.00 ± 0.00 Cash: 10.68 ± 0.07 2012-03-01 GAS Price: 9.99 Delta: 0.97 Hedge: -1.00 ± 0.00 Cash: 9.70 ± 0.07 Net hedge GAS: 0.00 ± 0.00 Net hedge cash: 20.78 ± 0.28 Fair value: 20.78 ± 0.28 ``` The value obtained is the extrinsic value. The intrinsic value can be obtained by setting the volatility `sigma` to `0`, and can be evaluated with `path_count` of `1`. The recommended hedge positions suggest injecting gas when the price is low, and withdrawing when the price is high. ### Gas fired power station An evaluation of a gas fired power station. The value of a gas fired power station follows from selling generated power whilst paying for gas. The Quant DSL source code below models a gas fired power station as a lattice of choices whether or not to run the power station. The efficiency of generation is modelled to be lower if the power station has been stopped. Dispatch decisions are made daily, with gas and power traded one day ahead. ```python power_plant = """ from quantdsl.semantics import Choice, Market, TimeDelta, Wait, inline, Min def PowerPlant(start, end, temp): if (start < end): Wait(start, Choice( PowerPlant(Tomorrow(start), end, Hot()) + ProfitFromRunning(start, temp), PowerPlant(Tomorrow(start), end, Stopped(temp)) )) else: return 0 @inline def Power(start): DayAhead(start, 'POWER') @inline def Gas(start): DayAhead(start, 'GAS') @inline def DayAhead(start, name): ForwardMarket(Tomorrow(start), name) @inline def Tomorrow(start): start + TimeDelta('1d') @inline def ProfitFromRunning(start, temp): if temp == Cold(): return 0.3 * Power(start) - Gas(start) elif temp == Warm(): return 0.6 * Power(start) - Gas(start) else: return Power(start) - Gas(start) @inline def Stopped(temp): if temp == Hot(): Warm() else: Cold() @inline def Hot(): 2 @inline def Warm(): 1 @inline def Cold(): 0 PowerPlant(Date('2012-1-1'), Date('2012-1-5'), Cold()) """ ``` The prices process is calibrated with two correlated markets. ```python gas_and_power = { 'name': 'quantdsl.priceprocess.blackscholes.BlackScholesPriceProcess', 'market': ['GAS', 'POWER'], 'sigma': [0.3, 0.3], 'rho': [[1.0, 0.8], [0.8, 1.0]], 'curve': { 'GAS': [ ('2012-1-1', 11.0), ('2012-1-2', 11.0), ('2012-1-3', 1.0), ('2012-1-4', 1.0), ('2012-1-5', 11.0), ], 'POWER': [ ('2012-1-1', 1.0), ('2012-1-2', 1.0), ('2012-1-3', 11.0), ('2012-1-4', 11.0), ('2012-1-5', 11.0), ] } } ``` Because the `periodisation` is set to `'daily'`, the deltas for each market in each day will be calculated, and estimated risk neutral hedge positions will be printed for each market in each period, along with the overall fair value. ```python results = calc( source_code=power_plant, observation_date='2011-1-1', interest_rate=2.5, periodisation='daily', price_process=gas_and_power, verbose=True ) assert round(results.fair_value.mean(), 2) == 12.82 print(results) ``` These are the results, showing monthly deltas for each of the two markets. The recommended hedge positions suggest running the plant when the price of power is high and the price of gas is low. The relative inefficiency of running the plant from `Cold()` is reflected in the delta for `POWER` in `2012-01-03`. ``` Compiled 16 nodes Compilation in 0.038s Simulation in 0.043s Starting 112 node evaluations, please wait... 112/112 100.00% complete 115.57 eval/s running 1s eta 0s Evaluation in 0.970s 2012-01-02 GAS Price: 10.98 Delta: -0.05 Hedge: 0.05 ± 0.00 Cash: -0.41 ± 0.04 2012-01-02 POWER Price: 1.00 Delta: 0.02 Hedge: -0.02 ± 0.00 Cash: 0.02 ± 0.00 2012-01-03 GAS Price: 1.00 Delta: -0.98 Hedge: 1.00 ± 0.00 Cash: -0.97 ± 0.01 2012-01-03 POWER Price: 10.98 Delta: 0.32 Hedge: -0.33 ± 0.00 Cash: 3.64 ± 0.05 2012-01-04 GAS Price: 1.00 Delta: -0.98 Hedge: 1.00 ± 0.00 Cash: -0.97 ± 0.01 2012-01-04 POWER Price: 10.98 Delta: 0.98 Hedge: -1.00 ± 0.00 Cash: 10.71 ± 0.07 2012-01-05 GAS Price: 10.98 Delta: -0.49 Hedge: 0.50 ± 0.00 Cash: -4.95 ± 0.11 2012-01-05 POWER Price: 10.98 Delta: 0.49 Hedge: -0.50 ± 0.00 Cash: 5.76 ± 0.13 Net hedge GAS: 2.55 ± 0.01 Net hedge POWER: -1.85 ± 0.01 Net hedge cash: 12.82 ± 0.10 Fair value: 12.82 ± 0.10 ``` ## Jupyter notebooks It's easy to use Quant DSL in a Jupyter notebook. See [example_notebook.ipynb](./example_notebook.ipynb). Jupyter notebooks can be executed on a Jupyter hub. ## Library There is a small collection of Quant DSL modules in a library under `quantdsl.lib`. Putting Quant DSL source code in dedicated Python files makes it much easier to use a Python IDE to develop and maintain Quant DSL function definitions. ## Acknowledgments The Quant DSL language was partly inspired by the paper *[Composing contracts: an adventure in financial engineering (functional pearl)]( http://research.microsoft.com/en-us/um/people/simonpj/Papers/financial-contracts/contracts-icfp.htm )* by Simon Peyton Jones and others. The idea of orchestrating evaluations with a dependency graph, to help with parallel and distributed execution, was inspired by a [talk about dependency graphs by Kirat Singh](https://www.youtube.com/watch?v=lTOP_shhVBQ). This implementation uses NumPy and SciPy packages, and the Python ast ("Absract Syntax Trees") module. We have also been encourged by members of the [London Financial Python User Group](https://www.google.co.uk/search?q=London+Financial+Python+User+Group), where Quant DSL expression syntax and semantics were first presented.