Quant DSL
Python Jupyter Notebook
Clone or download

README.md

Quant DSL

Domain specific language for quantitative analytics in finance and trading.

Build Status Coverage Status

Example of an American option expressed in Quant DSL.

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 from the Python Package Index. You may feedback issues on GitHub.

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; geometric Brownian motion; and least squares 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.

Syntax

The syntax of Quant DSL expressions is defined with Backus–Naur Form.

<Expression> ::= <Constant>
    | "Settlement(" <Date> "," <Expression> ")"
    | "Fixing(" <Date> "," <Expression> ")"
    | "Market(" <MarketName> ")"
    | "Wait(" <Date> "," <Expression> ")"
    | "Choice(" <Expression> "," <Expression> ")"
    | "Max(" <Expression> "," <Expression> ")"
    | <Expression> "+" <Expression>
    | <Expression> "-" <Expression>
    | <Expression> "*" <Expression>
    | <Expression> "/" <Expression>
    | "-" <Expression>

<Constant> ::= <Float> | <Integer>

<Date> ::= "'"<Year>"-"<Month>"-"<Day>"'"

<MarketName> ::= "'"<MarketName><NameChar> | <NameChar>"'"

<Year> ::= <Digit><Digit><Digit><Digit>

<Month> ::= <Digit><Digit>

<Day> ::= <Digit><Digit>

<Float> ::= <Integer>"."<Integer>

<Integer> ::= <Integer><Digit> | <Digit>

<NameChar> ::= "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" | "_" | <Digit>

<Digit> ::= "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.

This blog post by Uncle Bob Martin describes very well the orthogonality between object-oriented and functional programming that is at the heart of this software.

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.

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.

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(" <Date> ", " <Expression> ")"
[[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'.

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' .

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(" <Date> "," <Expression> ")"
[[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'.

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(" <MarketName> ")"
[[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).

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.

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.

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).

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.

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.

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.

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.

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.

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(" <Date> "," <Expression> ")"
[[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.

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(" <Expression> "," <Expression> ")"
[[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.

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.

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).

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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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).

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.

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.

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.

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.

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.

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.

The from form of the import statement can be used to include function definitions from Quant DSL modules on your Python path.

from quantdsl.lib.powerplant2 import PowerPlant

Acknowledgments

The Quant DSL language was partly inspired by the paper Composing contracts: an adventure in financial engineering (functional pearl) 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. 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, where Quant DSL expression syntax and semantics were first presented.