Source code for extensions.buffer_stock.events
"""Buffer-stock consumption events.
This module provides two replacement events for the buffer-stock consumption
extension (Section 3.9.4 of Delli Gatti et al., 2011).
The extension replaces the baseline mean-field MPC with an individual adaptive
rule based on buffer-stock saving theory. Each household maintains a personal
desired savings-income ratio and adjusts consumption to keep that ratio constant.
Key Equations
-------------
**Buffer-Stock MPC (Equation 3.20):**
.. math::
c_t = 1 + \\frac{d_t - h \\cdot g_t}{1 + g_t}
Where:
- :math:`h` is the desired savings-income ratio (config parameter)
- :math:`g_t = W_t / W_{t-1} - 1` is the income growth rate
- :math:`d_t = S_t / W_{t-1} - h` is the divergence from desired ratio
**Fresh Start Formula (derived):**
.. math::
c_t = 1 - h + S_t / W_t
Applied when a household has just been re-employed (no previous income).
"""
from __future__ import annotations
import numpy as np
import bamengine as bam
from bamengine import event, ops
[docs]
@event(replace="consumers_calc_propensity")
class ConsumersCalcBufferStockPropensity:
"""Compute individual MPC using buffer-stock adaptive rule.
Three-case logic:
1. **Normal** (prev_income > 0 and income > 0):
Full buffer-stock formula (Eq. 3.20).
2. **Fresh start** (prev_income <= 0 and income > 0):
Just re-employed: ``c = 1 - h + S/W``.
3. **Unemployed** (income <= 0):
``c = 1/h`` (gradual savings drawdown).
"""
[docs]
def execute(self, sim: bam.Simulation) -> None:
"""Execute buffer-stock propensity calculation."""
con = sim.get_role("Consumer")
buf = sim.get_role("BufferStock")
income = con.income
prev_income = buf.prev_income
savings = con.savings
h = sim.buffer_stock_h
# Case masks
normal = (prev_income > 0) & (income > 0)
fresh = (prev_income <= 0) & (income > 0)
# unemployed = income <= 0 (handled by default c=1/h)
c = np.full(len(income), 1.0 / h) # default: c=1/h (unemployed drawdown)
# Normal case: Eq. 3.20
if np.any(normal):
g = income[normal] / prev_income[normal] - 1.0
g = np.maximum(g, -0.99) # clamp singularity at g=-1
d = savings[normal] / prev_income[normal] - h
c[normal] = 1.0 + (d - h * g) / (1.0 + g)
# Fresh start: just re-employed
if np.any(fresh):
c[fresh] = 1.0 - h + savings[fresh] / income[fresh]
# Floor at 0 (no negative consumption)
c = np.maximum(c, 0.0)
ops.assign(buf.propensity, c)
[docs]
@event(replace="consumers_decide_income_to_spend")
class ConsumersDecideBufferStockSpending:
"""Allocate spending budget using buffer-stock MPC.
Two-path consumption:
- **Employed** (income > 0): budget = c * income
- **Unemployed** (income <= 0): budget = c * savings
Budget is capped at total wealth (savings + income) and floored at 0.
Savings are updated and floored at 0 (non-negative).
"""
[docs]
def execute(self, sim: bam.Simulation) -> None:
"""Execute buffer-stock spending allocation."""
con = sim.get_role("Consumer")
buf = sim.get_role("BufferStock")
c = buf.propensity
income = con.income
savings = con.savings
# Two-path consumption: employed from income, unemployed from savings
employed = income > 0
budget = np.where(employed, c * income, c * savings)
budget = np.minimum(budget, savings + income) # cap at wealth
budget = np.maximum(budget, 0.0) # safety floor
# Save prev_income before zeroing
ops.assign(buf.prev_income, income)
# Allocate spending budget
ops.assign(con.income_to_spend, budget)
# Unified savings update: works for both employed and unemployed
# For employed: savings + income - budget (retain unspent income)
# For unemployed: savings + 0 - budget (debit savings)
new_savings = savings + income - budget
new_savings = np.maximum(new_savings, 0.0) # floor at 0
ops.assign(con.savings, new_savings)
# Zero income for next period
ops.assign(con.income, 0.0)