Skip to content

Commit

Permalink
Merge 1a5c887 into cc1d400
Browse files Browse the repository at this point in the history
  • Loading branch information
johnbywater committed Apr 10, 2017
2 parents cc1d400 + 1a5c887 commit 4e25606
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 125 deletions.
3 changes: 3 additions & 0 deletions quantdsl/domain/model/simulated_price.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import six
from eventsourcing.domain.model.entity import EventSourcedEntity, EntityRepository
from eventsourcing.domain.model.events import publish
from quantdsl.priceprocess.base import datetime_from_date


class SimulatedPrice(EventSourcedEntity):
Expand Down Expand Up @@ -33,6 +34,8 @@ def make_simulated_price_id(simulation_id, commodity_name, fixing_date, delivery
assert isinstance(commodity_name, six.string_types), commodity_name
assert isinstance(fixing_date, (datetime.datetime, datetime.date)), (fixing_date, type(fixing_date))
assert isinstance(delivery_date, (datetime.datetime, datetime.date)), (delivery_date, type(delivery_date))
fixing_date = datetime_from_date(fixing_date)
delivery_date = datetime_from_date(delivery_date)
price_id = ("PriceId(simulation_id='{}' commodity_name='{}' fixing_date='{}', delivery_date='{}')"
"".format(simulation_id, commodity_name, fixing_date, delivery_date))
return price_id
Expand Down
2 changes: 0 additions & 2 deletions quantdsl/domain/services/simulated_prices.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ def identify_simulation_requirements(dependency_graph_id, call_requirement_repo,
perturbation_dependencies = set()
dsl_expr.identify_perturbation_dependencies(perturbation_dependencies, present_time=present_time)

# if perturbation_dependencies:
# raise Exception("Blah")
# Add the expression's perturbation dependencies to the perturbation dependencies of its call dependencies.
call_dependencies = call_dependencies_repo[call_id]
assert isinstance(call_dependencies, CallDependencies), call_dependencies
Expand Down
28 changes: 25 additions & 3 deletions quantdsl/priceprocess/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from __future__ import division

import datetime
from abc import ABCMeta, abstractmethod
import six

DAYS_PER_YEAR = 365
SECONDS_PER_DAY = 86400


class PriceProcess(six.with_metaclass(ABCMeta)):

Expand All @@ -11,10 +16,27 @@ def simulate_future_prices(self, observation_date, requirements, path_count, cal
Returns a generator that yields a sequence of simulated prices.
"""

def get_commodity_names_and_fixing_dates(self, observation_date, requirements):
# Get an ordered list of all the commodity names and fixing dates.
commodity_names = sorted(set([r[0] for r in requirements]))
observation_date = datetime_from_date(observation_date)

requirement_datetimes = [datetime_from_date(r[1]) for r in requirements]

fixing_dates = sorted(set([observation_date] + requirement_datetimes))
return commodity_names, fixing_dates

def get_duration_years(start_date, end_date, days_per_year=365):

def get_duration_years(start_date, end_date, days_per_year=DAYS_PER_YEAR):
try:
time_delta = end_date - start_date
time_delta = datetime_from_date(end_date) - datetime_from_date(start_date)
except TypeError as inst:
raise TypeError("%s: start: %s end: %s" % (inst, start_date, end_date))
return time_delta.days / float(days_per_year)
return time_delta.total_seconds() / float(days_per_year * SECONDS_PER_DAY)


def datetime_from_date(observation_date):
if isinstance(observation_date, datetime.date):
return datetime.datetime(observation_date.year, observation_date.month, observation_date.day)
else:
return observation_date
32 changes: 24 additions & 8 deletions quantdsl/priceprocess/blackscholes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
from __future__ import division

import datetime
import math

import scipy
import scipy.linalg
from scipy.linalg import LinAlgError

from quantdsl.exceptions import DslError
from quantdsl.priceprocess.base import PriceProcess
from quantdsl.priceprocess.base import get_duration_years
import datetime


class BlackScholesPriceProcess(PriceProcess):
Expand All @@ -22,33 +25,46 @@ def simulate_future_prices(self, observation_date, requirements, path_count, cal
# Compute simulated market prices using the correlated Brownian
# motions, the actual historical volatility, and the last price.
for commodity_name, brownian_motions in all_brownian_motions:
last_price = calibration_params['%s-LAST-PRICE' % commodity_name]
actual_historical_volatility = calibration_params['%s-ACTUAL-HISTORICAL-VOLATILITY' % commodity_name]
# Get the 'last price' for this commodity.
param_name = '%s-LAST-PRICE' % commodity_name
last_price = self.get_calibration_param(param_name, calibration_params)

# Get the 'actual historical volatility' for this commodity.
param_name = '%s-ACTUAL-HISTORICAL-VOLATILITY' % commodity_name
actual_historical_volatility = self.get_calibration_param(param_name, calibration_params)

sigma = actual_historical_volatility / 100.0
for fixing_date, brownian_rv in brownian_motions:
T = get_duration_years(observation_date, fixing_date)
simulated_value = last_price * scipy.exp(sigma * brownian_rv - 0.5 * sigma * sigma * T)
yield commodity_name, fixing_date, fixing_date, simulated_value

def get_calibration_param(self, param_name, calibration_params):
try:
actual_historical_volatility = calibration_params[param_name]
except KeyError:
msg = "Calibration parameter '{}' not found.".format(param_name)
raise KeyError(msg)
return actual_historical_volatility

def get_brownian_motions(self, observation_date, requirements, path_count, calibration_params):
assert isinstance(observation_date, datetime.date), observation_date
assert isinstance(observation_date, datetime.datetime), observation_date
assert isinstance(requirements, list), requirements
assert isinstance(path_count, int), path_count

# Get an ordered list of all the commodity names and fixing dates.
commodity_names = sorted(set([r[0] for r in requirements]))
commodity_names, fixing_dates = self.get_commodity_names_and_fixing_dates(observation_date, requirements)

len_commodity_names = len(commodity_names)
if len_commodity_names == 0:
return []

fixing_dates = sorted(set([observation_date] + [r[1] for r in requirements]))
len_fixing_dates = len(fixing_dates)
if len_fixing_dates == 0:
return []

# Check the observation date equals the first fixing date.
assert observation_date == fixing_dates[0], "Observation date {} not equal to first fixing date: {}" \
"".format(observation_date, fixing_dates)
"".format(observation_date, fixing_dates[0])

# Diffuse random variables through each date for each market (uncorrelated increments).
brownian_motions = scipy.zeros((len_commodity_names, len_fixing_dates, path_count))
Expand Down
144 changes: 109 additions & 35 deletions quantdsl/semantics.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,15 @@ def validate(self, args):
pass

# Todo: Rework validation, perhaps by considering a declarative form in which to express the requirements.
def assert_args_len(self, args, required_len=None, min_len=None):
def assert_args_len(self, args, required_len=None, min_len=None, max_len=None):
if min_len != None and len(args) < min_len:
error = "%s is broken" % self.__class__.__name__
descr = "requires at least %s arguments (%s were given)" % (min_len, len(args))
raise DslSyntaxError(error, descr, self.node)
if max_len != None and len(args) > max_len:
error = "%s is broken" % self.__class__.__name__
descr = "requires at most %s arguments (%s were given)" % (max_len, len(args))
raise DslSyntaxError(error, descr, self.node)
if required_len != None and len(args) != required_len:
error = "%s is broken" % self.__class__.__name__
descr = "requires %s arguments (%s were given)" % (required_len, len(args))
Expand Down Expand Up @@ -230,7 +234,7 @@ def parse(self, value):
date_str = value
try:
year, month, day = [int(i) for i in date_str.split('-')]
return datetime.date(year, month, day)
return datetime.datetime(year, month, day)
# return dateutil.parser.parse(date_str).replace()
except ValueError:
raise DslSyntaxError("invalid date string", date_str, node=self.node)
Expand Down Expand Up @@ -439,7 +443,8 @@ def op(self, left, right):
return left / right


class Max(BinOp):
class NonInfixedBinOp(BinOp):

def op(self, a, b):
# Assume a and b have EITHER type ndarray, OR type int or float.
# Try to 'balance' the sides.
Expand All @@ -450,7 +455,7 @@ def op(self, a, b):
bIsaNumber = isinstance(b, (int, float))
if aIsaNumber and bIsaNumber:
# Neither are vectors.
return max(a, b)
return self.scalar_op(a, b)
elif (not aIsaNumber) and (not bIsaNumber):
# Both are vectors.
if len(a) != len(b):
Expand All @@ -462,8 +467,23 @@ def op(self, a, b):
elif bIsaNumber and (not aIsaNumber):
# Todo: Optimise with scipy.zeros() when b equals zero?
b = scipy.array([b] * len(a))
c = scipy.array([a, b])
return c.max(axis=0)
return self.vector_op(a, b)


class Min(NonInfixedBinOp):
def vector_op(self, a, b):
return scipy.array([a, b]).min(axis=0)

def scalar_op(self, a, b):
return min(a, b)


class Max(NonInfixedBinOp):
def vector_op(self, a, b):
return scipy.array([a, b]).max(axis=0)

def scalar_op(self, a, b):
return max(a, b)


# Todo: Pow, Mod, FloorDiv don't have proofs, so shouldn't really be used for combining random variables? Either prevent usage with ndarray inputs, or do the proofs. :-)
Expand Down Expand Up @@ -1018,6 +1038,78 @@ def date(self):
return self._date


class Lift(DslExpression):

def validate(self, args):
self.assert_args_len(args, min_len=2, max_len=3)
# Name of a commodity to be perturbed.
self.assert_args_arg(args, posn=0, required_type=(String, Name))
if len(args) == 2:
# Expression to be perturbed.
self.assert_args_arg(args, posn=1, required_type=DslExpression)
elif len(args) == 3:
# Periodization of the perturbation.
self.assert_args_arg(args, posn=1, required_type=String)
# Expression to be perturbed.
self.assert_args_arg(args, posn=2, required_type=DslExpression)

@property
def commodity_name(self):
return self._args[0]

@property
def mode(self):
return self._args[1] if len(self._args) == 3 else String('alltime')

@property
def expr(self):
return self._args[-1]

def identify_perturbation_dependencies(self, dependencies, **kwds):
perturbation = self.get_perturbation(**kwds)
dependencies.add(perturbation)
super(Lift, self).identify_perturbation_dependencies(dependencies, **kwds)

def get_perturbation(self, **kwds):
try:
present_time = kwds['present_time']
except KeyError:
raise DslSyntaxError(
"'present_time' not found in evaluation kwds" % self.market_name,
", ".join(kwds.keys()),
node=self.node
)
commodity_name = self.commodity_name.evaluate(**kwds)
mode = self.mode.evaluate(**kwds)
if mode.startswith('alltime'):
perturbation = commodity_name
elif mode.startswith('year'):
# perturbation = json.dumps((commodity_name, present_time.year))
perturbation = "{}-{}".format(commodity_name, present_time.year)
elif mode.startswith('mon'):
# perturbation = json.dumps((commodity_name, present_time.year, present_time.month))
perturbation = "{}-{}-{}".format(commodity_name, present_time.year, present_time.month)
elif mode.startswith('da'):
# perturbation = json.dumps((commodity_name, present_time.year, present_time.month, present_time.day))
perturbation = "{}-{}-{}-{}".format(commodity_name, present_time.year, present_time.month,
present_time.day)
else:
raise Exception("Unsupported mode: {}".format(mode))
return perturbation

def evaluate(self, **kwds):
# Get the perturbed market name, if set.
active_perturbation = kwds.get('active_perturbation', None)

# If this is a perturbed market, perturb the simulated value.
expr_value = self.expr.evaluate(**kwds)
if self.get_perturbation(**kwds) == active_perturbation:
evaluated_value = expr_value * (1 + Market.PERTURBATION_FACTOR)
else:
evaluated_value = expr_value
return evaluated_value


functionalDslClasses = {
'Add': Add,
'And': And,
Expand All @@ -1031,7 +1123,9 @@ def date(self):
'FunctionDef': FunctionDef,
'If': If,
'IfExp': IfExp,
'Lift': Lift,
'Max': Max,
'Min': Min,
'Mod': Mod,
'Module': Module,
'SnapToMonth': SnapToMonth,
Expand All @@ -1055,9 +1149,6 @@ class AbstractMarket(StochasticObject, DslExpression):
PERTURBATION_FACTOR = 0.001

def evaluate(self, **kwds):
# Get the perturbed market name, if set.
active_perturbation = kwds.get('active_perturbation', None)

# Get the effective present time (needed to form the simulated_value_id).
try:
present_time = kwds['present_time']
Expand Down Expand Up @@ -1087,12 +1178,7 @@ def evaluate(self, **kwds):
except KeyError:
raise DslError("Simulated price not found ID: {}".format(simulated_price_id))

# If this is a perturbed market, perturb the simulated value.
if self.get_perturbation(present_time) == active_perturbation:
evaluated_value = simulated_price_value * (1 + Market.PERTURBATION_FACTOR)
else:
evaluated_value = simulated_price_value
return evaluated_value
return simulated_price_value

@property
def market_name(self):
Expand All @@ -1104,7 +1190,14 @@ def delivery_date(self):

@property
def commodity_name(self):
return self._args[0].evaluate() if isinstance(self._args[0], String) else self._args[0]
name = self._args[0].evaluate() if isinstance(self._args[0], String) else self._args[0]
# Disallow '-' in market names (it's used to compose / split perturbation names,
# which probably should work differently, so that this restriction can be removed.
# Todo: Review perturbation names (now hyphen separated, were JSON strings).
if '-' in name:
raise DslSyntaxError("hyphen character '-' not allowed in market names (sorry): {}"
"".format(name), node=self.node)
return name

def identify_price_simulation_requirements(self, requirements, **kwds):
assert isinstance(requirements, set)
Expand All @@ -1122,25 +1215,6 @@ def identify_price_simulation_requirements(self, requirements, **kwds):
requirements.add(requirement)
super(AbstractMarket, self).identify_price_simulation_requirements(requirements, **kwds)

def identify_perturbation_dependencies(self, dependencies, **kwds):
try:
present_time = kwds['present_time']
except KeyError:
raise DslSyntaxError(
"'present_time' not found in evaluation kwds" % self.market_name,
", ".join(kwds.keys()),
node=self.node
)
perturbation = self.get_perturbation(present_time)
dependencies.add(perturbation)
super(AbstractMarket, self).identify_perturbation_dependencies(dependencies, **kwds)

def get_perturbation(self, present_time):
# For now, just bucket by commodity name and month of delivery.
delivery_date = self.delivery_date or present_time
perturbation = json.dumps((self.commodity_name, delivery_date.year, delivery_date.month))
return perturbation


class Market(AbstractMarket):

Expand Down
Loading

0 comments on commit 4e25606

Please sign in to comment.