Edgeworth and Red Cross barter
Barter of chocolate and cigarettes, in an Edgeworth box of two barterers or among many prisoners.
- Name:
barter - Field:
economics/markets - Formalism: agent-based model
- References: Hamill and Gilbert 2016 ch. 5; Radford 1945
Factory
@register_model(
field='economics/markets',
tags=('market', 'agents', 'barter', 'Edgeworth box', 'contract curve'),
references=('Hamill and Gilbert 2016 ch. 5', 'Radford 1945'),
defaults={'until': 20},
observables=_barter_observables,
)
def barter(
n_barterers: int = 2,
*,
chocolate: tuple[float, float] = (9.0, 1.0),
cigarettes: tuple[float, float] = (2.0, 8.0),
weights: tuple[float, float] = (0.6, 0.3),
quantity: float = 0.02,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return barter of chocolate and cigarettes, in an Edgeworth box of two barterers or among many prisoners.
Every step the barterers are paired at random, and every pair trades small quantities of one good for the other
at the geometric mean of their marginal rates of substitution while both gain, so the totals of both goods are
unchanged. Two barterers with the given endowments and weights of chocolate trade within the Edgeworth box until
no trade improves both, which is a point of the contract curve, where their marginal rates of substitution are
equal. More barterers are prisoners of war who receive random Red Cross parcels and random tastes, and their
trades settle on a common price, as Radford (1945) observed (Hamill and Gilbert 2016 ch. 5).
Args:
n_barterers:
The number of barterers, 2 for the Edgeworth box.
chocolate:
The chocolate of the two barterers of the Edgeworth box, and the range of the parcels of more barterers.
cigarettes:
The cigarettes of the two barterers of the Edgeworth box, and the range of the parcels of more
barterers.
weights:
The weights of chocolate of the two barterers of the Edgeworth box, and the range of the weights of more
barterers.
quantity:
The quantity of the bought good in every trade.
random_state:
The seed or generator of the parcels and the weights of more than two barterers.
Returns:
system:
The model, with the group `barterers` and the exchange `exchange`, whose reporters are the price of
cigarettes in chocolate `exchange.price`, its `exchange.dispersion`, and `exchange.volume`.
Raises:
ValueError:
There are fewer than two barterers.
Examples:
>>> from skcomplex.models.economics.markets import barter
>>> barter().elements[0].check().prepare().size_
2
"""
if n_barterers < EDGEWORTH_BARTERERS:
error_msg = f'Barter needs at least two barterers, but got {n_barterers}.'
raise ValueError(error_msg)
rng = np.random.default_rng(random_state)
if n_barterers == EDGEWORTH_BARTERERS:
holdings: dict[str, list[float] | npt.NDArray[np.float64]] = {
'chocolate': list(chocolate),
'cigarettes': list(cigarettes),
}
shares = np.asarray(weights, dtype=float)
else:
holdings = {
'chocolate': rng.uniform(min(chocolate), max(chocolate), n_barterers),
'cigarettes': rng.uniform(min(cigarettes), max(cigarettes), n_barterers),
}
shares = rng.uniform(min(weights), max(weights), n_barterers)
barterers = GroupElement(
'barterers',
element=BartererElement,
size=n_barterers,
values={'chocolate_weight': shares, 'cigarettes_weight': 1 - shares, **holdings},
variable=True,
)
return System(
elements=[barterers],
interactions=[
ExchangeInteraction('exchange', source='barterers', target='barterers', goods=GOODS, quantity=quantity),
],
time=DiscreteTime(schedule=PhasesScheduleSpec([PhaseSpec('trade')])),
)
Components
class BartererElement(Element):
"""Barterer who holds chocolate and cigarettes and values them with Cobb-Douglas weights.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
chocolate:
The holding of chocolate.
cigarettes:
The holding of cigarettes.
chocolate_weight:
The Cobb-Douglas weight of chocolate.
cigarettes_weight:
The Cobb-Douglas weight of cigarettes.
Examples:
>>> from skcomplex.models.economics.markets import BartererElement
>>> GroupElement('barterers', element=BartererElement, size=2, values={'chocolate': [8.0, 2.0]}).size
2
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'chocolate_weight', 'color_range': (0.0, 1.0)}),
*,
chocolate: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
cigarettes: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
chocolate_weight: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
cigarettes_weight: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.5,
) -> None:
super().__init__(label, visual=visual)
self.chocolate = chocolate
self.cigarettes = cigarettes
self.chocolate_weight = chocolate_weight
self.cigarettes_weight = cigarettes_weight
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return no laws, since the exchange moves the goods.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return []
class ExchangeInteraction(TransferInteraction):
"""Bilateral barter between paired traders, a transfer whose trades repeat while both traders gain.
Every step the transfer pairs the traders, and every pair barters small quantities of every pair of goods at the
geometric mean of their marginal rates of substitution while both gain. A trader holds every good in the state of
its name and its Cobb-Douglas weight in the state `<good>_weight`. Every trade moves goods between the two traders,
so the total of every good stays the same, which the default run checks as invariants. The exchange reports the
mean `price` of the second good in units of the first, the standard deviation `dispersion` of its logarithm over
the trades of the step, and the number of trades `volume`.
Args:
label:
The label of the exchange.
source:
The path of the group of traders.
target:
The path of the group of traders, the source again.
goods:
The names of the traded goods.
pairing:
How the transfer pairs the traders, such as `'neighbor-pairs'` for disjoint pairs of neighbours or
`'pairs'` for disjoint random pairs.
quantity:
The quantity of the bought good in every trade.
phase:
The phase the trades run in.
Examples:
>>> from skcomplex.models.economics.markets import ExchangeInteraction
>>> ExchangeInteraction('exchange', source='traders', target='traders').reports
('price', 'dispersion', 'volume')
"""
reports: ClassVar[tuple[str, ...]] = ('price', 'dispersion', 'volume')
def __init__(
self,
label: str,
*,
source: str,
target: str,
goods: tuple[str, ...] = GOODS,
pairing: str = 'pairs',
quantity: float = 0.1,
phase: str = 'trade',
) -> None:
super().__init__(
label,
source=source,
target=target,
state=goods[0],
amount=quantity,
pairing=pairing,
form='agent',
phase=phase,
)
self.goods = goods
self.quantity = quantity
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return the barter of every pair of traders and the report of the trades of the step.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return [*super()._get_laws(s), RuleLaw(self.report_trades, phase=self.phase)]
def transfer(self, source: AgentViewProtocol, target: AgentViewProtocol, model: ModelViewProtocol) -> bool:
"""Barter between two traders while both gain, and keep the trades of the step for its report.
Args:
source:
The first trader.
target:
The second trader.
model:
The model view of the exchange.
Returns:
traded:
Whether the traders made a trade.
"""
weights = [f'{good}_weight' for good in self.goods]
trades = trade_goods(source, target, self.goods, weights, quantity=self.quantity)
logs = model.storage.setdefault('trades', [])
if isinstance(logs, list):
logs.extend(trades)
return bool(trades)
def report_trades(self, model: ModelViewProtocol) -> None:
"""Report the mean price of the second good in the first, its dispersion, and the trades of the step.
Args:
model:
The model view of the exchange.
"""
trades = model.storage.pop('trades', [])
trades = trades if isinstance(trades, list) else []
logs = [math.log(price) for bought, paid, price in trades if (bought, paid) == (0, 1)]
model.report('price', math.exp(float(np.mean(logs))) if logs else math.nan)
model.report('dispersion', float(np.std(logs)) if len(logs) > 1 else math.nan)
model.report('volume', len(trades))