Skip to content

Commit

Permalink
Added support for forward markets, so that delivery dates can be take…
Browse files Browse the repository at this point in the history
…n into account.

(cherry picked from commit d04bd2a)
  • Loading branch information
johnbywater committed Apr 10, 2017
1 parent 02576b8 commit f7d94f3
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 107 deletions.
2 changes: 1 addition & 1 deletion quantdsl/__init__.py
Expand Up @@ -15,7 +15,7 @@
# Todo: Improve the multiprocessing code - currently it runs slower that the single threaded, and seems to grind to a halt for stacks > 5000 expressions (IPC bandwidth? rounding errors?).
# Todo: Improve separation of expression stack/dependency graph from results and notifications, so results from different runs can be reused when calculating greeks.
# Todo: Separate multiprocessing from ExpressionStack, self-evaluation of ExpressionStack can just be single threaded.
# Todo: Figure out how best to make alternative set of DSL classes available to workers (module name that is imported, rather than a dict of classes).
# Todo: Figure out how best to make alternative set of DSL classes available to workers (module commodity_name that is imported, rather than a dict of classes).
# Todo: Optimization for parallel execution, so if there are four cores, then it might make sense only to stub four large branches?
# Todo: Optimize network traffic by creating a single message containing all data required to evaluate a stubbed expression.
# Todo: Decouple the cli from the runner more, make the workers put things directly on the queue, so that the cli just waits for the final result and clocks the intermediate results as they occur in an event stream.
Expand Down
4 changes: 2 additions & 2 deletions quantdsl/domain/model/contract_specification.py
Expand Up @@ -27,8 +27,8 @@ def register_contract_specification(specification):
return contract_specification


def make_simulated_price_id(simulation_id, market_name, price_time, delivery_time=''):
return simulation_id + market_name + str(price_time) + str(delivery_time)
# Todo: Rename market_name to commodity_name?


class ContractSpecificationRepository(EntityRepository):
pass
3 changes: 2 additions & 1 deletion quantdsl/domain/model/market_simulation.py
Expand Up @@ -11,7 +11,8 @@ class Created(EventSourcedEntity.Created):
class Discarded(EventSourcedEntity.Discarded):
pass

def __init__(self, market_calibration_id, market_names, fixing_dates, observation_date, path_count, interest_rate, **kwargs):
def __init__(self, market_calibration_id, market_names, fixing_dates, observation_date, path_count, interest_rate,
**kwargs):
super(MarketSimulation, self).__init__(**kwargs)
self._market_calibration_id = market_calibration_id
self._market_names = market_names
Expand Down
11 changes: 6 additions & 5 deletions quantdsl/domain/model/simulated_price.py
Expand Up @@ -29,12 +29,13 @@ def register_simulated_price(market_simulation_id, market_name, fixing_date, pri
return simulated_price


def make_simulated_price_id(market_simulation_id, market_name, price_time):
# assert isinstance(market_simulation_id, six.string_types), market_simulation_id
# assert isinstance(market_name, six.string_types), market_name
# assert isinstance(price_time, datetime.date), price_time
return market_simulation_id + market_name + str(price_time)
def make_simulated_price_id(simulation_id, market_name, quoted_on, delivery_date=''):
price_id = ("PriceId(simulation_id='{}' market_name='{}' delivery_date='{}' quoted_on='{}')"
"".format(simulation_id, market_name, quoted_on, delivery_date))
return price_id


class SimulatedPriceRepository(EntityRepository):
pass


2 changes: 1 addition & 1 deletion quantdsl/domain/services/contract_valuations.py
Expand Up @@ -13,7 +13,7 @@
from quantdsl.domain.model.call_requirement import CallRequirement
from quantdsl.domain.model.call_result import register_call_result, make_call_result_id, CallResult, \
CallResultRepository
from quantdsl.domain.model.contract_specification import make_simulated_price_id
from quantdsl.domain.model.simulated_price import make_simulated_price_id
from quantdsl.domain.model.contract_valuation import ContractValuation
from quantdsl.domain.services.call_links import regenerate_execution_order
from quantdsl.domain.services.parser import dsl_parse
Expand Down
2 changes: 1 addition & 1 deletion quantdsl/domain/services/simulated_prices.py
Expand Up @@ -70,4 +70,4 @@ def list_market_names_and_fixing_dates(dependency_graph_id, call_requirement_rep
# Return sorted lists.
all_market_names = sorted(list(all_market_names))
all_fixing_dates = sorted(list(all_fixing_dates))
return all_market_names, all_fixing_dates
return all_market_names, all_fixing_dates
2 changes: 1 addition & 1 deletion quantdsl/priceprocess/base.py
Expand Up @@ -8,7 +8,7 @@ class PriceProcess(six.with_metaclass(ABCMeta)):
@abstractmethod
def simulate_future_prices(self, market_names, fixing_dates, observation_date, path_count, calibration_params):
"""
Returns dict (keyed by market name) of dicts (keyed by fixing date) with correlated random future prices.
Returns dict (keyed by market commodity_name) of dicts (keyed by fixing date) with correlated random future prices.
"""


Expand Down
6 changes: 3 additions & 3 deletions quantdsl/priceprocess/blackscholes.py
Expand Up @@ -20,8 +20,8 @@ def simulate_future_prices(self, market_names, fixing_dates, observation_date, p
# Compute simulated market prices using the correlated Brownian
# motions, the actual historical volatility, and the last price.
for market_name, brownian_motions in all_brownian_motions:
last_price = calibration_params['%s-LAST-PRICE' % market_name.upper()]
actual_historical_volatility = calibration_params['%s-ACTUAL-HISTORICAL-VOLATILITY' % market_name.upper()]
last_price = calibration_params['%s-LAST-PRICE' % market_name]
actual_historical_volatility = calibration_params['%s-ACTUAL-HISTORICAL-VOLATILITY' % market_name]
sigma = actual_historical_volatility / 100.0
for fixing_date, brownian_rv in brownian_motions:
T = get_duration_years(observation_date, fixing_date)
Expand Down Expand Up @@ -103,7 +103,7 @@ def get_brownian_motions(self, market_names, fixing_dates, observation_date, pat
brownian_motions_correlated = brownian_motions_correlated.transpose()
brownian_motions = brownian_motions_correlated

# Put random variables into a nested Python dict, keyed by market name and fixing date.
# Put random variables into a nested Python dict, keyed by market commodity_name and fixing date.
all_brownian_motions = []
for i, market_name in enumerate(market_names):
market_rvs = []
Expand Down
130 changes: 79 additions & 51 deletions quantdsl/semantics.py
@@ -1,6 +1,6 @@
from __future__ import division

from abc import ABCMeta, abstractmethod
from abc import ABCMeta, abstractmethod, abstractproperty
from collections import namedtuple
import datetime
import itertools
Expand All @@ -15,7 +15,7 @@


from quantdsl.domain.model.call_requirement import StubbedCall
from quantdsl.domain.model.contract_specification import make_simulated_price_id
from quantdsl.domain.model.simulated_price import make_simulated_price_id

from quantdsl.domain.services.uuids import create_uuid4
from quantdsl.exceptions import DslSystemError, DslSyntaxError, DslNameError, DslError
Expand Down Expand Up @@ -261,6 +261,33 @@ def parse(self, value, regex=re.compile(r'((?P<days>\d+?)d)?')):
raise DslSystemError("shouldn't get here", value, node=self.node)


class SnapToMonth(DslExpression):

def __str__(self, indent=0):
return "SnapToMonth({})".format(str(self._args[0]))

def validate(self, args):
self.assert_args_len(args, required_len=2)
self.assert_args_arg(args, posn=0, required_type=(Date, Name))
self.assert_args_arg(args, posn=1, required_type=(Number, six.integer_types))
assert args[1].evaluate() < 29, DslSyntaxError("Snap day must be less than or equal to 28", node=self.node)

def evaluate(self, **kwds):
value = self._args[0].evaluate(**kwds)
snap_day = self._args[1].evaluate()
assert isinstance(value, datetime.date)
year = value.year
month = value.month
day = value.day
if day < snap_day:
if month == 1:
month = 12
year -= 1
else:
month += 1
return datetime.date(year, month, snap_day)


class UnaryOp(DslExpression):
opchar = None

Expand Down Expand Up @@ -475,7 +502,7 @@ def name(self):

def reduce(self, dsl_locals, dsl_globals, effective_present_time=None, pending_call_stack=False):
"""
Replace name with named value in context (kwds).
Replace commodity_name with named value in context (kwds).
"""

combined_namespace = DslNamespace(itertools.chain(dsl_globals.items(), dsl_locals.items()))
Expand Down Expand Up @@ -971,7 +998,7 @@ def date(self):
date = String(date)
if isinstance(date, String):
date = Date(date, node=date.node)
if isinstance(date, (Date, BinOp)):
if isinstance(date, (SnapToMonth, Date, BinOp)):
date = date.evaluate()
if not isinstance(date, datetime.date):
raise DslSyntaxError("date value should be a datetime.datetime by now, but it's a %s" % date, node=self.node)
Expand All @@ -995,6 +1022,7 @@ def date(self):
'Max': Max,
'Mod': Mod,
'Module': Module,
'SnapToMonth': SnapToMonth,
'Mult': Mult,
'Name': Name,
'Number': Number,
Expand All @@ -1011,22 +1039,9 @@ def date(self):

# Todo: Add something to Map a contract function to a sequence of values (range, list comprehension).

class Market(StochasticObject, DslExpression):

class AbstractMarket(StochasticObject, DslExpression):
PERTURBATION_FACTOR = 0.001

def validate(self, args):
self.assert_args_len(args, required_len=1)
self.assert_args_arg(args, posn=0, required_type=(six.string_types, String, Name))

@property
def name(self):
return self._args[0].evaluate() if isinstance(self._args[0], String) else self._args[0]

@property
def delivery_time(self):
return ''

def evaluate(self, **kwds):
# Get the perturbed market name, if set.
perturbed_market_name = kwds.get('perturbed_market_name', '') or ''
Expand All @@ -1036,7 +1051,7 @@ def evaluate(self, **kwds):
present_time = kwds['present_time']
except KeyError:
raise DslSyntaxError(
"Can't evaluate Market '%s' without 'present_time' in context variables" % self.name,
"'present_time' not found in evaluation kwds" % self.market_name,
", ".join(kwds.keys()),
node=self.node
)
Expand All @@ -1046,41 +1061,64 @@ def evaluate(self, **kwds):
simulated_value_dict = kwds['simulated_value_dict']
except KeyError:
raise DslError(
"Can't evaluate Market '%s' without 'simulated_value_dict' in context variables" % self.name,
"Not found 'simulated_value_dict' in context variables" % self.market_name,
", ".join(kwds.keys()),
node=self.node
)

# Make the simulated price ID.
simulated_price_id = make_simulated_price_id(kwds['simulation_id'],
market_name=self.name,
price_time=present_time,
delivery_time=self.delivery_time)
market_name=self.market_name,
quoted_on=present_time,
)

# Get the value from the dict of simulated values.
try:
simulated_price_value = simulated_value_dict[simulated_price_id]
except KeyError:
raise DslError("Can't find simulated price at '%s' for market '%s' using simulated price ID '%s'." % (present_time, self.name, simulated_price_id))
raise DslError("Simulated price not found ID: {}".format(simulated_price_id))

# If this is a perturbed market, perturb the simulated value.
if self.name == perturbed_market_name:
if self.market_name == perturbed_market_name:
simulated_price_value = simulated_price_value * (1 + Market.PERTURBATION_FACTOR)

return simulated_price_value

@abstractproperty
def market_name(self):
pass

@property
def commodity_name(self):
return self._args[0].evaluate() if isinstance(self._args[0], String) else self._args[0]


class ForwardMarket(Market):
class Market(AbstractMarket):

def validate(self, args):
self.assert_args_len(args, required_len=1)
self.assert_args_arg(args, posn=0, required_type=(six.string_types, String, Name))

@property
def market_name(self):
return self.commodity_name


class ForwardMarket(AbstractMarket):

def validate(self, args):
self.assert_args_len(args, required_len=2)
self.assert_args_arg(args, posn=0, required_type=(six.string_types, String, Name))
self.assert_args_arg(args, posn=1, required_type=(String, Date, Name, BinOp))
self.assert_args_arg(args, posn=1, required_type=(String, Date, Name, BinOp, SnapToMonth))

@property
def delivery_time(self):
# Refactor this w.r.t. the Settlement.date property.
if not hasattr(self, '_delivery_time'):
def market_name(self):
return "{}-{}".format(self.commodity_name, self.delivery_date.strftime('%Y-%m'))

@property
def delivery_date(self):
# Todo: Refactor this w.r.t. the Settlement.date property.
if not hasattr(self, '_delivery_date'):
date = self._args[1]
if isinstance(date, Name):
raise DslSyntaxError("date value name '%s' must be resolved to a datetime before it can be used" % date.name, node=self.node)
Expand All @@ -1090,13 +1128,12 @@ def delivery_time(self):
date = String(date)
if isinstance(date, String):
date = Date(date, node=date.node)
if isinstance(date, (Date, BinOp)):
if isinstance(date, (SnapToMonth, Date, BinOp)):
date = date.evaluate()
if not isinstance(date, datetime.date):
raise DslSyntaxError("delivery date value should be a datetime.datetime by now, but it's a %s" % date, node=self.node)
self._delivery_time = date
return self._delivery_time

self._delivery_date = date
return self._delivery_date


class Settlement(StochasticObject, DatedDslObject, DslExpression):
Expand All @@ -1106,7 +1143,7 @@ class Settlement(StochasticObject, DatedDslObject, DslExpression):

def validate(self, args):
self.assert_args_len(args, required_len=2)
self.assert_args_arg(args, posn=0, required_type=(String, Date, Name, BinOp))
self.assert_args_arg(args, posn=0, required_type=(String, Date, SnapToMonth, Name, BinOp))
self.assert_args_arg(args, posn=1, required_type=DslExpression)

def evaluate(self, **kwds):
Expand All @@ -1119,7 +1156,7 @@ class Fixing(StochasticObject, DatedDslObject, DslExpression):
A fixing defines the 'present_time' used for evaluating its expression.
"""

def __str__(self):
def __str__(self, indent=0):
return "%s('%04d-%02d-%02d', %s)" % (
self.__class__.__name__,
self.date.year,
Expand Down Expand Up @@ -1163,6 +1200,7 @@ class On(Fixing):
A shorter name for Fixing.
"""


class Wait(Fixing):
"""
A fixing with discounting of the resulting value from date arg to present_time.
Expand All @@ -1184,17 +1222,9 @@ def validate(self, args):
self.assert_args_arg(args, posn=i, required_type=DslExpression)

def evaluate(self, **kwds):
# Check the results cache, to see whether this function
# has already been evaluated with these args.
# Todo: Check if this cache is actually working.
# if not hasattr(self, 'results_cache'):
# self.results_cache = {}
# cache_key_kwd_items = [(k, hash(tuple(sorted(v))) if isinstance(v, dict) else v) for (k, v) in kwds.items()]
# kwds_hash = hash(tuple(sorted(cache_key_kwd_items)))
# if kwds_hash not in self.results_cache:
# Run the least-squares monte-carlo routine.
present_time = kwds['present_time']
# Todo: Check this way of getting 'first market name' is the correct way to do it.
# Todo: Check this way of getting 'first market commodity_name' is the correct way to do it.
first_market_name = kwds['first_market_name']
simulated_value_dict = kwds['simulated_value_dict']
simulation_id = kwds['simulation_id']
Expand All @@ -1204,8 +1234,6 @@ def evaluate(self, **kwds):
simulated_value_dict, simulation_id)
result = longstaff_schwartz.evaluate(**kwds)
return result
# self.results_cache[kwds_hash] = result
# return self.results_cache[kwds_hash]


class LongstaffSchwartz(object):
Expand Down Expand Up @@ -1240,7 +1268,7 @@ def evaluate(self, **kwds):
regression_variables = []
# Todo: Need to make sure that, after subbing, all the pertaining markets are returned here.
dsl_markets = subsequent_state.dsl_object.list_instances(Market)
market_names = set([m.name for m in dsl_markets])
market_names = set([m.commodity_name for m in dsl_markets])
for market_name in market_names:
market_price = self.get_simulated_value(market_name, state.time)
regression_variables.append(market_price)
Expand Down Expand Up @@ -1625,9 +1653,9 @@ def find_fixing_dates(dsl_expr):
def find_market_names(dsl_expr):
# Find all unique market names.
all_market_names = set()
for dsl_market in dsl_expr.find_instances(dsl_type=Market):
for dsl_market in dsl_expr.find_instances(dsl_type=AbstractMarket):
# assert isinstance(dsl_market, Market)
market_name = dsl_market.name
market_name = dsl_market.market_name
if market_name not in all_market_names: # Deduplicate.
all_market_names.add(market_name)
yield dsl_market.name
yield market_name

0 comments on commit f7d94f3

Please sign in to comment.