Gooding double auction with adaptive quotes
Gooding's double auction, whose buyers and sellers move their quotes towards trade and then apart.
- Name:
gooding_double_auction - Field:
economics/markets - Formalism: agent-based system
- References: Gooding 2019
Select this system
from skcomplex.systems import create_simulation, create_system, describe
system = create_system('gooding_double_auction')
entry = describe('gooding_double_auction')
trajectory = create_simulation('gooding_double_auction').prepare().run().trajectory_
Factory
@register_system(
field='economics/markets',
tags=('double auction', 'agents', 'price discovery', 'equilibrium', 'adaptive quotes'),
references=('Gooding 2019',),
defaults={'until': 300},
)
def gooding_double_auction(
n_buyers: int = 100,
n_sellers: int = 100,
*,
value: float = 120.0,
cost: float = 20.0,
spread: float = 0.1,
units: float = 5.0,
step: float = 1.0,
unlimited: bool = False,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return Gooding's double auction, whose buyers and sellers move their quotes towards trade and then apart.
Buyers value a unit near `value` and sellers can supply it near `cost`, each differing by up to `spread` of it. As
in the experiments the model replicates, buyers start bidding near the sellers' cost and sellers start asking near
the buyers' value, and both move their quotes towards one another until they trade, after which each tries to push
the price its way (Gooding 2019, Section 11.3 and the appendix). In the short run, with sellers of a few units and
buyers that are satiated after a few, the price settles near the competitive price halfway between value and cost,
70 at the defaults. With unlimited supply and demand, the long-run price wanders between the cost and the value and
the equilibrium disappears. The source describes the rules in prose, so the meetings and the fixed steps of the
quotes are a reconstruction of them.
Args:
n_buyers:
The number of buyers.
n_sellers:
The number of sellers.
value:
The mean value of a unit to the buyers.
cost:
The mean cost of a unit to the sellers.
spread:
The largest relative difference of a trader's value or cost from the mean.
units:
The units every seller holds and every buyer wants.
step:
The change of a quote after a meeting.
unlimited:
Whether the units of the traders never run out.
random_state:
The seed or generator of the values and costs.
Returns:
system:
The system, with the groups `buyers` and `sellers` and the auction `auction`, whose reporters are the last
traded price `auction.price`, the units traded `auction.volume`, and the mean quotes `auction.mean_bid` and
`auction.mean_ask`.
Examples:
>>> from skcomplex.systems.economics.markets import gooding_double_auction
>>> [group.prepare().size_ for group in gooding_double_auction(4, 3).elements]
[4, 3]
"""
rng = np.random.default_rng(random_state)
values = value * (1 + spread * rng.uniform(-1, 1, n_buyers))
costs = cost * (1 + spread * rng.uniform(-1, 1, n_sellers))
buyers = GroupElement(
'buyers',
element=AdaptiveTraderElement,
size=n_buyers,
values={'value': values, 'quote': cost * (1 + spread * rng.uniform(-1, 1, n_buyers)), 'units': units},
)
sellers = GroupElement(
'sellers',
element=AdaptiveTraderElement,
size=n_sellers,
values={'value': costs, 'quote': value * (1 + spread * rng.uniform(-1, 1, n_sellers)), 'units': units},
)
auction = AdaptiveAuctionInteraction('auction', buyers='buyers', sellers='sellers', step=step, unlimited=unlimited)
return System(elements=[buyers, sellers], interactions=[auction])
Components
Elements:
AdaptiveTraderElement, fromElement
Interactions:
AdaptiveAuctionInteraction, fromFeedbackInteraction
class AdaptiveTraderElement(Element):
"""Buyer or seller of a double auction, with its value or cost, its current bid or ask, and its units left.
Args:
label:
The label of the agent or group.
visual:
How the element is drawn, or `None` for `default_visual_`.
value:
The value of a unit to a buyer, or its cost to a seller.
quote:
The current bid of a buyer or ask of a seller.
units:
The units a buyer still wants or a seller still holds.
Examples:
>>> from skcomplex.core.components.elements import GroupElement
>>> from skcomplex.systems.economics.markets import AdaptiveTraderElement
>>> GroupElement('buyers', element=AdaptiveTraderElement, size=3, values={'value': 120.0}).size
3
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = VisualSpec('agents', {'color_by': 'quote'}),
*,
value: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
quote: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 0.0,
units: Annotated[float, StateSpec(domain=RealSet.closed_open(0, oo))] = 5.0,
) -> None:
super().__init__(label, visual=visual)
self.value = value
self.quote = quote
self.units = units
@classmethod
def _get_view(cls) -> type[AdaptiveTraderView]:
"""Return the view of the agents of the element."""
return AdaptiveTraderView
def _get_laws(self, s: SimpleNamespace) -> list[BaseLaw]:
"""Return no laws, since the auction moves every trader.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The laws of the component.
"""
return []
class AdaptiveAuctionInteraction(FeedbackInteraction[SystemView]):
"""Double auction in which every buyer meets a random seller every iteration and both adapt their quotes.
Every iteration the buyers that still want units, in random order, meet a random seller that still holds units.
When the bid reaches the ask, one unit trades at the ask, and afterwards the buyer lowers its bid and the seller
raises its ask, to push the price their way. When it does not, the buyer raises its bid and the seller lowers its
ask, to create trade, each change a uniform draw between 0 and `step`. Bids never exceed the values and asks never
fall below the costs. With `unlimited` units, traders are resupplied and keep consuming, so trade goes on forever.
Args:
label:
The label of the auction.
rule:
Unused, since the auction overrides `act`.
phase:
The phase the auction acts in, or `None` for the default phase.
buyers:
The path of the group of buyers.
sellers:
The path of the group of sellers.
step:
The change of a quote after a meeting.
unlimited:
Whether the units of the traders never run out.
Examples:
>>> from skcomplex.systems.economics.markets import AdaptiveAuctionInteraction
>>> AdaptiveAuctionInteraction('auction', buyers='buyers', sellers='sellers').reports
('price', 'volume', 'mean_bid', 'mean_ask')
"""
reports: ClassVar[tuple[str, ...]] = ('price', 'volume', 'mean_bid', 'mean_ask')
def __init__(
self,
label: str,
*,
rule: None = None,
phase: str | None = None,
buyers: Annotated[str, RoleSpec()],
sellers: Annotated[str, RoleSpec()],
step: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 1.0,
unlimited: bool = False,
) -> None:
super().__init__(label, rule=rule, phase=phase)
self.buyers = buyers
self.sellers = sellers
self.step = step
self.unlimited = unlimited
def act(self, system: SystemView) -> None:
"""Run the meetings of one iteration.
Args:
system:
The system view of the auction.
"""
rng, step = system.rng, system.parameter('step')
buyers = sorted(system.agents('buyers', AdaptiveTraderView), key=lambda trader: trader.id)
sellers = sorted(system.agents('sellers', AdaptiveTraderView), key=lambda trader: trader.id)
prices = []
for buyer in (buyers[index] for index in rng.permutation(len(buyers))):
stocked = [seller for seller in sellers if seller.units > 0]
if buyer.units <= 0 or not stocked:
continue
seller = stocked[int(rng.integers(len(stocked)))]
bid_step, ask_step = step * rng.random(), step * rng.random()
if buyer.quote >= seller.quote:
prices.append(seller.quote)
if not self.unlimited:
buyer.units, seller.units = buyer.units - 1, seller.units - 1
buyer.quote = max(buyer.quote - bid_step, 0.0)
seller.quote += ask_step
else:
buyer.quote = min(buyer.quote + bid_step, buyer.value)
seller.quote = max(seller.quote - ask_step, seller.value)
last = system.history('price', 1)
price = prices[-1] if prices else (float(last[-1]) if len(last) else float('nan'))
system.report('price', price)
system.report('volume', float(len(prices)))
system.report('mean_bid', float(np.mean([buyer.quote for buyer in buyers])))
system.report('mean_ask', float(np.mean([seller.quote for seller in sellers])))