Static oligopoly equilibria
The equilibria of a Cournot duopoly and of a pricing game of firms with loyal customers and switchers.
- Name:
oligopoly_equilibria - Field:
economics/markets - Formalism: static solution
- References: Forrest 2018; Brown 1951
Factory
@register_model(
field='economics/markets',
tags=('oligopoly', 'equilibrium', 'static', 'game theory'),
references=('Forrest 2018', 'Brown 1951'),
observables=_price_observables,
)
def oligopoly_equilibria(
n_firms: int = 2,
*,
alpha: float = 0.25,
n_prices: int = 21,
demand: float = 10.0,
c_i: float = 1.0,
c_j: float = 2.0,
lambda_i: float = 0.0,
lambda_j: float = 0.0,
dynamics: str = 'static',
) -> System:
"""Return the equilibria of a Cournot duopoly and of a pricing game of firms with loyal customers and switchers.
The static model solves the Cournot best responses (Forrest 2018 Eqs. 21.5-21.6) and, on a grid of prices, the
symmetric mixed equilibrium of `m` firms of loyal share `alpha` and switchers `beta = 1 - m alpha`, whose price
distribution makes a firm indifferent among its prices, at the profit `alpha` (Lemma 20.1). With
`dynamics='fictitious-play'`, two firms learn to price on the grid instead: every round each plays a best response
to the prices of its rival so far, and the prices they charge are distributed near the mixed equilibrium.
Args:
n_firms:
The number of firms of the pricing game, two under fictitious play.
alpha:
The loyal share of every firm.
n_prices:
The number of prices of the grid from `alpha / (alpha + beta)` to 1.
demand:
The intercept of the inverse demand of the Cournot duopoly.
c_i:
The unit cost of the first Cournot firm.
c_j:
The unit cost of the second Cournot firm.
lambda_i:
The cross holding of the first Cournot firm.
lambda_j:
The cross holding of the second Cournot firm.
dynamics:
`'static'` for the equilibria, or `'fictitious-play'` for the learning of two pricing firms.
Returns:
system:
The static model, with the states `cournot.q_i`, `cournot.q_j`, `prices.price`, and `prices.distribution`
and the observables `lowest_profit` and `highest_profit`, or the learning model, with the group `firms` and
the observables `lower_price` and `higher_price`. The catalog lists the observables, so a run passes them
in `observables`.
Raises:
ValidationError:
The dynamics are unknown, or fictitious play has other than two firms.
Examples:
>>> from skcomplex.engine.runtime import Simulation
>>> from skcomplex.models.economics.markets import oligopoly_equilibria
>>> trajectory = Simulation(oligopoly_equilibria()).check().prepare().run(1.0).trajectory_
>>> round(float(trajectory.values('cournot.q_i')[0]), 6)
3.333333
"""
if dynamics not in DYNAMICS or (dynamics == 'fictitious-play' and n_firms != DUOPOLY):
error_msg = (
f'oligopoly_equilibria has dynamics={dynamics!r} and n_firms={n_firms!r}. Use one of {DYNAMICS}, and '
f'{DUOPOLY} firms under fictitious play.'
)
raise ValidationError(error_msg)
beta = 1 - n_firms * alpha
prices = np.linspace(alpha / (alpha + beta), 1.0, n_prices)
if dynamics == 'fictitious-play':
return _fictitious_play(prices, alpha, beta)
points = GroupElement(
'prices',
element=PricePointElement,
size=n_prices,
values={'price': prices, 'alpha': alpha, 'beta': beta, 'n_firms': float(n_firms)},
)
cournot = CournotBestResponseElement(
'cournot',
demand=demand,
c_i=c_i,
c_j=c_j,
lambda_i=lambda_i,
lambda_j=lambda_j,
)
return System(elements=[cournot, points], time=StaticTime())
Components
class CournotBestResponseElement(Element):
"""Quantities of two firms at the best responses of a Cournot game, where each firm holds a share of the other.
The conditions are `alpha - 2 q_i - q_j = c_i / (1 + lambda_i)` and its mirror (Forrest 2018 Eq. 21.5), whose
solution is `q_i = (alpha - 2 c_i / (1 + lambda_i) + c_j / (1 + lambda_j)) / 3` (Eq. 21.6).
Args:
label:
The label of the element.
q_i:
The initial guess of the quantity of the first firm, which the equations replace.
q_j:
The initial guess of the quantity of the second firm, which the equations replace.
demand:
The intercept `alpha` of the inverse demand.
c_i:
The unit cost of the first firm.
c_j:
The unit cost of the second firm.
lambda_i:
The cross holding of the first firm.
lambda_j:
The cross holding of the second firm.
Examples:
>>> from skcomplex.models.economics.markets import CournotBestResponseElement
>>> CournotBestResponseElement('duopoly', demand=8.0).demand
8.0
"""
def __init__(
self,
label: str,
*,
q_i: Annotated[float, StateSpec()] = 0.0,
q_j: Annotated[float, StateSpec()] = 0.0,
demand: Annotated[float, ParameterSpec()] = 10.0,
c_i: Annotated[float, ParameterSpec()] = 1.0,
c_j: Annotated[float, ParameterSpec()] = 2.0,
lambda_i: Annotated[float, ParameterSpec()] = 0.0,
lambda_j: Annotated[float, ParameterSpec()] = 0.0,
) -> None:
super().__init__(label)
self.q_i = q_i
self.q_j = q_j
self.demand = demand
self.c_i = c_i
self.c_j = c_j
self.lambda_i = lambda_i
self.lambda_j = lambda_j
def _get_laws(self, s: SimpleNamespace) -> Sequence[BasePart]:
"""Return the two conditions of the best responses.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The equations of the two quantities.
"""
return [
EquationLaw(s.demand - 2 * s.q_i - s.q_j == s.c_i / (1 + s.lambda_i), target=s.q_i),
EquationLaw(s.demand - 2 * s.q_j - s.q_i == s.c_j / (1 + s.lambda_j), target=s.q_j),
]
class PricePointElement(Element):
"""Equilibrium distribution of prices at one price, which makes a firm indifferent among its prices.
A firm that charges `P` sells to its loyal share `alpha`, and to the switchers `beta` when it undercuts all its
`m - 1` rivals, each with the probability `1 - F(P)`, for the expected profit `P (alpha + beta (1 - F)^(m - 1))`.
In equilibrium every price gives the profit `alpha` of selling to the loyal share at the highest price 1, which
`F(P) = 1 - (alpha (1 - P) / (beta P))^(1 / (m - 1))` solves (Forrest 2018 Lemma 20.1).
Args:
label:
The label of the point or the group.
distribution:
The initial guess of `F(P)`, which the equations replace.
price:
The price `P`.
alpha:
The loyal share of every firm.
beta:
The share of switchers.
n_firms:
The number of firms `m`.
Examples:
>>> from skcomplex.models.economics.markets import PricePointElement
>>> PricePointElement('point', price=0.5).price
0.5
"""
def __init__(
self,
label: str,
*,
distribution: Annotated[float, StateSpec()] = 0.5,
price: Annotated[float, StateSpec(domain=RealSet.open_closed(0, 1))] = 1.0,
alpha: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.25,
beta: Annotated[float, ParameterSpec(domain=RealSet.open(0, oo))] = 0.5,
n_firms: Annotated[float, ParameterSpec(domain=RealSet.closed_open(2, oo))] = 2.0,
) -> None:
super().__init__(label)
self.distribution = distribution
self.price = price
self.alpha = alpha
self.beta = beta
self.n_firms = n_firms
@staticmethod
def get_profit(member: SimpleNamespace) -> Expression:
"""Return the expected profit of a firm at the price of a member, for the observables of a run.
Args:
member:
The namespace of the states and parameters of the member.
Returns:
profit:
The expected profit `P (alpha + beta (1 - F)^(m - 1))`.
"""
return member.price * (member.alpha + member.beta * (1 - member.distribution) ** (member.n_firms - 1))
def _get_laws(self, s: SimpleNamespace) -> Sequence[BasePart]:
"""Return the equation of the distribution.
Args:
s:
The namespace of the symbols the laws can use.
Returns:
laws:
The equation of the distribution.
"""
distribution = 1 - (s.alpha * (1 - s.price) / (s.beta * s.price)) ** (1 / (s.n_firms - 1))
return [EquationLaw(s.distribution == distribution, target=s.distribution)]