Exchange rates in international trade
Hamill and Gilbert's staged adjustment of trade and the exchange rate to a rise in home prices.
- Name:
exchange_rates - Field:
economics/macroeconomics - Formalism: discrete map
- References: Hamill and Gilbert 2016
Factory
@register_model(
field='economics/macroeconomics',
tags=('international trade', 'exchange rate', 'discrete map'),
references=('Hamill and Gilbert 2016',),
defaults={'until': 2, 'dt': 1},
observables=_trade_observables,
)
def exchange_rates(
exports: float = 32.0,
imports: float = 34.0,
*,
home_price: float = 1.2,
inflation: float = 0.1,
export_elasticity: float = -1.0,
import_elasticity: float = -1.0,
floating: bool = True,
demand: float = 52.0,
government: float = 50.0,
world_price: float = 1.0,
) -> System:
"""Return Hamill and Gilbert's staged adjustment of trade and the exchange rate to a rise in home prices.
The economy starts in balance at the exchange rate `E = M * PW / (X * PH)`, in units of World currency per unit of
Home currency, with the export and import volumes and prices of the United Kingdom in 1999 by default. Home prices
rise by `inflation` at the first stage. At every stage a floating rate takes the value that balances the currency
market at the current volumes, a fixed rate stays, and the volumes respond to the prices of the stage before with
constant price elasticities. The book follows two stages, the default.
Args:
exports:
The initial export volume.
imports:
The initial import volume.
home_price:
The home price of home-produced goods before the rise.
inflation:
The relative rise of the home prices.
export_elasticity:
The price elasticity of the exports.
import_elasticity:
The price elasticity of the imports.
floating:
Whether the exchange rate floats, or is fixed as in the Eurozone.
demand:
The private domestic demand, consumption and investment.
government:
The government spending.
world_price:
The world price level.
Returns:
system:
The model, with the economy `home`, the balance of trade at home prices `balance`, the real exchange rate
`real_rate`, and the output `output`.
Examples:
>>> from skcomplex.models.economics.macroeconomics import exchange_rates
>>> round(exchange_rates().elements[0].E, 3)
0.885
"""
rate = imports * world_price / (exports * home_price)
home = TradingEconomyElement(
'home',
E=rate,
X=exports,
M=imports,
PH=home_price,
home_price=home_price,
inflation=inflation,
world_price=world_price,
exports=exports,
imports=imports,
export_price=home_price * rate,
import_price=world_price / rate,
export_elasticity=export_elasticity,
import_elasticity=import_elasticity,
floating=float(floating),
import_share=imports / (demand + government + exports - imports),
demand=demand,
government=government,
)
return System(elements=[home], interactions=_adjustments())
Components
class TradingEconomyElement(Element):
"""Open economy that trades with the World at an exchange rate, floating or fixed.
Its states are the exchange rate `E` in World currency per unit of home currency, the export and import volumes
`X` and `M`, and the home price `PH`; its parameters are the prices, volumes, and elasticities of the balanced
start, the rise `inflation` of home prices, whether the rate floats, and the shares of the output.
Args:
label:
The label of the economy.
visual:
How the element is drawn, or `None` for `default_visual_`.
E:
The exchange rate.
X:
The export volume.
M:
The import volume.
PH:
The home price of home-produced goods.
home_price:
The home price before the rise.
inflation:
The relative rise of the home prices.
world_price:
The world price level.
exports:
The export volume of the balanced start.
imports:
The import volume of the balanced start.
export_price:
The World price of the exports at the start.
import_price:
The home price of the imports at the start.
export_elasticity:
The price elasticity of the exports.
import_elasticity:
The price elasticity of the imports.
floating:
1 when the exchange rate floats, 0 when it is fixed.
import_share:
The share of the imports in the output.
demand:
The private domestic demand.
government:
The government spending.
Examples:
>>> from skcomplex.models.economics.macroeconomics import TradingEconomyElement
>>> TradingEconomyElement('home').floating
1.0
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
E: Annotated[object, StateSpec(domain=RealSet.open(0, oo))] = 0.885,
X: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 32.0,
M: Annotated[object, StateSpec(domain=RealSet.closed_open(0, oo))] = 34.0,
PH: Annotated[object, StateSpec(domain=RealSet.open(0, oo))] = 1.2,
home_price: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.2,
inflation: Annotated[object, ParameterSpec()] = 0.1,
world_price: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
exports: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 32.0,
imports: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 34.0,
export_price: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.062,
import_price: Annotated[object, ParameterSpec(domain=RealSet.open(0, oo))] = 1.13,
export_elasticity: Annotated[object, ParameterSpec()] = -1.0,
import_elasticity: Annotated[object, ParameterSpec()] = -1.0,
floating: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 1.0,
import_share: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.34,
demand: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 52.0,
government: Annotated[object, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 50.0,
) -> None:
super().__init__(label, visual=visual)
self.E = E
self.X = X
self.M = M
self.PH = PH
self.home_price = home_price
self.inflation = inflation
self.world_price = world_price
self.exports = exports
self.imports = imports
self.export_price = export_price
self.import_price = import_price
self.export_elasticity = export_elasticity
self.import_elasticity = import_elasticity
self.floating = floating
self.import_share = import_share
self.demand = demand
self.government = government
class MapInteraction(Interaction):
"""Map of a state of discrete time, whose next value is a function, or the state plus it.
In the mode `'replace'` the next value of the state is `f`, and in the mode `'increment'` it is the state plus `f`.
Under the `'synchronous'` schedule every member of a bound group updates at once, from the states at the start of
the step. Under the `'sequential'` schedule the members update one after the other, in their order, and under the
`'random-sequential'` schedule in a new random order every step, each member reading the values the members before
it set. Under the `'member'` schedule of the agent form, every member updates in its own activation, together with
the rules of its element and the other member interactions, in the order the system lists them.
In the `'symbolic'` form, `function(namespace)` returns a symbolic expression, or a vector of them for a vector
state, where `namespace.target` holds the states and parameters of the target, `namespace.sources` the namespaces
of the sources, and `namespace.t` the time, and may hold draws such as `normal('noise')`. In the `'agent'` form,
the target is an agent group and `function(agent, model)` returns the value as a number, or as an array of the
shape of a vector state, so it can read the agent, its neighbours, and the history and storage of the model view.
Args:
label:
The label of the interaction, unique among the parts of its system.
target:
The path of the element or group whose state the map sets.
state:
The name of the state of the target.
function:
The function `f(namespace)` in the symbolic form, or `f(agent, model)` in the agent form.
sources:
The paths of the elements, or members of groups such as `cells[0]`, that the function reads.
mode:
`'replace'` or `'increment'`.
schedule:
`'synchronous'`, `'sequential'`, `'random-sequential'`, or `'member'` for the agent form.
form:
`'symbolic'` for update laws of the states of the target, or `'agent'` for a rule over an agent group.
phase:
The phase of the step the agent form runs in, or `None` for the default phase.
Raises:
ValidationError:
The mode, the schedule, or the form is unknown, the symbolic form has the schedule `'member'`, the
function is not callable, or the agent form returns a value that does not match the state, raised by
`run` of the simulation.
Examples:
>>> from skcomplex.core.elements import Element
>>> from skcomplex.core.interactions import MapInteraction
>>> from skcomplex.core.spaces import DiscreteTime
>>> from skcomplex.core.system import System
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.specs import StateSpec
>>> from typing import Annotated, SupportsFloat
>>> class ValueElement(Element):
... def __init__(self, label, x: Annotated[float, StateSpec()] = 0.5):
... super().__init__(label)
... self.x = x
>>> logistic = MapInteraction(
... 'map', target='value', state='x', function=lambda s: 3 * s.target.x * (1 - s.target.x)
... )
>>> system = System(elements=[ValueElement('value')], interactions=[logistic], time=DiscreteTime())
>>> Simulation(system).check().prepare().run(until=2).trajectory_.values('value.x').tolist()
[0.5, 0.75, 0.5625]
>>> agents = MapInteraction('map', target='walkers', state='x', function=lambda a, m: a.x + 1, form='agent')
>>> type(agents.check().prepare().run(SimpleNamespace()).laws_[0]).__name__
'RuleLaw'
>>> from skcomplex.core.elements import GroupElement
>>> class WeightsElement(Element):
... def __init__(self, label, visual=None, *, w: Annotated[object, StateSpec(vector=True)] = 0.0):
... super().__init__(label, visual=visual)
... self.w = w
>>> group = GroupElement('weights', element=WeightsElement, size=1, values={'w': [1.0, 2.0]})
>>> doubling = MapInteraction(
... 'double', target='weights', state='w', function=lambda a, m: [2 * x for x in a.w], form='agent'
... )
>>> trajectory = Simulation(System(elements=[group], interactions=[doubling])).check().prepare().run(until=2)
>>> trajectory.trajectory_.agents('weights', 2).values['w'].tolist()
[[4.0, 8.0]]
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
state: str,
function: Callable[..., object],
sources: Annotated[Sequence[str], RoleSpec(variadic=True)] = (),
mode: str = 'replace',
schedule: str = 'synchronous',
form: str = 'symbolic',
phase: str | None = None,
) -> None:
super().__init__(label)
self.target = target
self.state = state
self.function = function
self.sources = sources
self.mode = mode
self.schedule = schedule
self.form = form
self.phase = phase
def _check(self) -> None:
"""Raise an error for an unknown mode, schedule, or form, or a function that cannot be called.
Raises:
ValidationError:
The mode, the schedule, or the form is unknown, the symbolic form has the schedule `'member'`, or the
function is not callable.
"""
super()._check()
self._check_variant('mode', self.mode, MODES, (BaseSpec,))
for name, value, variants in (
('schedule', self.schedule, SCHEDULES),
('form', self.form, FORMS),
):
if value not in variants:
error_msg = f'The {name} of map {self.label!r} is {value!r}. Use one of {", ".join(variants)}.'
raise ValidationError(error_msg)
if self.schedule == 'member' and self.form != 'agent':
error_msg = (
f'The map {self.label!r} updates every member in its activation, which only the agent form does.'
)
raise ValidationError(error_msg)
if not callable(self.function):
error_msg = f'The function of map {self.label!r} must be callable, but it is {self.function!r}.'
raise ValidationError(error_msg)
def _apply_mode(self, current: Value, value: Value) -> Value:
"""Return the next value of a state from its current value and the value of the function, by the mode."""
if self.mode == 'increment':
return current + value
if self.mode == 'replace':
return value
raise self._get_variant_error(self.mode, 'mode', '_apply_mode')
def _get_vector(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> Value:
"""Return the next value of a vector state of an agent."""
current = np.asarray(getattr(agent, self.state), dtype=np.float64)
value = np.asarray(self.function(agent, model), dtype=np.float64)
if value.shape != current.shape:
error_msg = (
f'The function of map {self.label!r} returns the shape {value.shape} for the state {self.state!r} of '
f'the shape {current.shape}. Return an array of the shape of the state.'
)
raise ValidationError(error_msg)
return self._apply_mode(current, value)
def _get_value(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> Value:
"""Return the next value of the state of an agent, a number or the array of a vector state."""
if np.ndim(getattr(agent, self.state)) > 0:
return self._get_vector(agent, model)
found = self.function(agent, model)
if not isinstance(found, SupportsFloat):
error_msg = f'The function of map {self.label!r} returns {found!r} for an agent. Return a number.'
raise ValidationError(error_msg)
value = float(found)
return self._apply_mode(float(getattr(agent, self.state)), value)
def _set_value(self, agent: AgentViewProtocol, value: Value) -> None:
"""Set the state of an agent to its next value."""
setattr(agent, self.state, value)
def _update_member(self, agent: AgentViewProtocol, model: ModelViewProtocol) -> None:
"""Set the state of one member to its next value."""
if agent.alive:
self._set_value(agent, self._get_value(agent, model))
def update(self, model: ModelViewProtocol) -> None:
"""Set the state of every agent of the target to its next value, by the schedule.
Args:
model:
The model view of the interaction.
"""
self._update_agents(model, self.schedule, self._get_value, self._set_value)
def _get_order(self) -> object:
"""Return the order of the members of the symbolic form: none, their own order, or a uniform draw."""
if self.schedule == 'synchronous':
return None
if self.schedule == 'sequential':
return SR(0)
return uniform(re.sub(r'\W', '_', f'order_{self.label}'))
def _get_laws(self, namespace: SimpleNamespace) -> list[BasePart]:
"""Return the update of the state to its next value, or the rule of the agent form.
Args:
namespace:
The namespace of the symbols the laws can use, with `namespace.target` and `namespace.sources`.
Returns:
laws:
The update law of the state, or the rule that runs the map of the agents.
"""
if self.form == 'agent' and self.schedule == 'member':
return [RuleLaw(self._update_member, phase=self.phase, members=self._get_role('target'))]
if self.form == 'agent':
return [RuleLaw(self.update, phase=self.phase)]
value = getattr(self._get_member(namespace, 'target'), self.state)
mapped = cast(Value, self.function(namespace))
next_value = self._apply_mode(value, mapped)
return [UpdateLaw(value, next_value, increment=self.mode == 'increment', order=self._get_order())]
Equations
home.E: E_home(t + 1) == where(floating_home > 0, world_price_home*M_home(t)/(home_price_home*(inflation_home + 1)*X_home(t)), E_home(t))
home.X: X_home(t + 1) == (export_elasticity_home*(E_home(t)*PH_home(t)/export_price_home - 1) + 1)*exports_home
home.M: M_home(t + 1) == (import_elasticity_home*(world_price_home/(import_price_home*E_home(t)) - 1) + 1)*imports_home
home.PH: PH_home(t + 1) == home_price_home*(inflation_home + 1)