Level 05 · Economics

Mechanisms and Policy

Auctions, stable matching and externalities; policy rules, expectations and the Phillips curve.

Level 5 · Economics: Mechanisms and Policy

Until now the rules of each market were given, and the questions were about what follows from them. The micro half of this level designs the rules. An auction turns sealed bids, or a rising price, into a winner and a payment, and the payment rule decides whether bidders should tell the truth. A matching market (apprentices and workshops, pupils and schools) has no prices at all, and a clearing house must find a placement that nobody wants to break. And some markets get the quantity wrong because part of the cost or benefit falls on people outside the trade: smoke over a town, fireworks that everybody can watch. A tax, a market for permits or a well-built auction can repair that. Each of these is a mechanism: a set of rules that turns what people report into an outcome and payments.

The macro half follows the central bank. You will separate nominal from real interest rates, link money growth to inflation, write the bank's reaction as a Taylor rule, and tie inflation to unemployment and to what people expect. Then the pieces go into one small model simulated quarter by quarter: demand, inflation and the bank's rule, after a shock. You will score rules against each other, see what happens when the rate cannot fall below zero, and see why a bank's promise of low inflation may not be believed.

The paths meet in specific places. Deferred acceptance is the Gale-Shapley algorithm of the Algorithms path, and choosing the best set of bundle bids is a backtracking search over bitmasks. Expected auction revenue is estimated by a seeded Monte Carlo simulation, as on the Statistics path. Above all, a policy rule is a feedback controller of the Signals & Control path: it measures two errors (inflation off target, output off normal) and acts on the economy one quarter later. The economy's path after a shock is an impulse response, scoring rules by simulation is controller tuning (and grid search, as on the Machine Learning path), and the zero lower bound is saturation, an actuator that cannot go past its limit. Minimising a quadratic loss by setting its slope to zero is the Machine Learning path's condition too.

Sealed bids: values, payoffs and two ways to pay

Each bidder has a value v: the most the item is worth to them, known only to them. A bidder who wins and pays a price p gets the payoff v - p; a bidder who loses pays nothing and gets 0. A payoff can be negative: winning at a price above your value is a loss.

In a sealed-bid auction everyone hands in one bid without seeing the others, and the highest bid wins. Two common rules say what the winner pays:

  • first price: the winner pays their own bid;
  • second price (a Vickrey auction): the winner pays the highest bid among the others.

A seller may set a reserve price: bids below it are thrown out, and if no bid reaches it the item is not sold. Under the second-price rule the winner then pays the highest other valid bid, or the reserve itself if there is none, so never less than the reserve. Every auction also needs a stated tie rule. A sort key can do both orderings at once: (-bid, index) puts the highest bid first and, among equal bids, the earlier envelope first.

values = [230, 160, 300, 100, 290]          # what the item is worth to each bidder (private)
reserve = 150
for bids in ([210, 145, 260, 90, 240], [210, 145, 260, 90, 260]):
    order = sorted(range(len(bids)), key=lambda i: (-bids[i], i))
    valid = [i for i in order if bids[i] >= reserve]
    w = valid[0]
    second = bids[valid[1]] if len(valid) > 1 else reserve
    print(order, "winner", w, " first price", bids[w], "payoff", values[w] - bids[w],
          " second price", second, "payoff", values[w] - second)
[2, 4, 0, 1, 3] winner 2  first price 260 payoff 40  second price 240 payoff 60
[2, 4, 0, 1, 3] winner 2  first price 260 payoff 40  second price 260 payoff 40

In the second auction bidders 2 and 4 tie at 260. Bidder 2's envelope came first, and the second price is the tied amount: the runner-up offered just as much. Bidders 1 and 3 are below the reserve and never count.

Why truthful bidding is safe under the second price. Fix everyone else's bids and call the highest of them top. Under the second-price rule your bid decides only whether you win; what you pay, top, is set by someone else. If your value is above top, every winning bid gives the same v - \text{top} > 0, and bidding your value is one of them. If your value is below top, every winning bid loses money, and bidding your value loses the auction and gets 0. Either way no bid does better than your value. Check it against every bid from 0 to 200 for a bidder whose value is 80 (a tie with top loses):

value = 80

def second_price(bid, top):
    return value - top if bid > top else 0

def first_price(bid, top):
    return value - bid if bid > top else 0

for top in (40, 65, 79, 95):
    bids = range(0, 201)
    print("top", top, " second: truthful", second_price(value, top), "best", max(second_price(b, top) for b in bids),
          "  first: truthful", first_price(value, top), "best", max(first_price(b, top) for b in bids))
top 40  second: truthful 40 best 40   first: truthful 0 best 39
top 65  second: truthful 15 best 15   first: truthful 0 best 14
top 79  second: truthful 1 best 1   first: truthful 0 best 0
top 95  second: truthful 0 best 0   first: truthful 0 best 0

Under the second-price rule the truthful bid always reaches the best payoff there is, whatever the others bid. Under the first-price rule bidding your value always earns 0, win or lose: to gain anything a bidder must shade, bid below their value, and how far depends on guesses about the others. There is no bid that is best whatever they do.

Common mistakes: taking the second-highest distinct bid, which gets ties wrong. Letting a bid below the reserve win, or charging less than the reserve. Treating a tie with the highest other bid as a win when the rule says it loses. Charging the second-price winner their own bid.

Practice: Envelopes for the Lighthouse Painting, Bidding What the Bike Is Worth

The ascending auction

In a sale room the auctioneer calls rising prices, and bidders drop out once the price passes what the item is worth to them. Staying in exactly while your value is at least the called price is safe for the same reason as truthful bidding: you never pay more than your value, and leaving earlier could only lose you a profitable win. So the bidding stops when the second-keenest bidder drops out, near the second-highest value, and the keenest bidder wins: the outcome of a second-price sealed-bid auction, reached in public.

With a bid increment (the step between called prices) the end is only near that value. Three bidders, prices called from 60 in steps of 15, and who stays in at each:

values = [140, 95, 122]
start, step = 60, 15
for p in range(start, 151, step):
    staying = [i for i, v in enumerate(values) if v >= p]
    print(p, staying)
60 [0, 1, 2]
75 [0, 1, 2]
90 [0, 1, 2]
105 [0, 2]
120 [0, 2]
135 [0]
150 []

At 135 only bidder 0 is left, and the item sells there: 13 above the second-highest value, 122, which is less than one step. A smaller step ends closer to it. Two cases need a stated rule, and a simulation must follow it. Nobody may stay in at the opening price (no sale). And the keenest bidders may have the same value, so they all drop out at the same call and nobody is left: a careful loop asks who would stay in at the next price before it raises the price, and applies the rule for that case.

Common mistakes: raising the price first and only then noticing that nobody stays in, which sells at a price nobody accepted. Comparing with the second-highest distinct value. Forgetting the auction where nobody stays in at the start.

Practice: Calling Up the Price of a Rowing Boat

Expected revenue: random values, one seeded generator

Which payment rule earns the seller more? It depends on the bids, and the bids depend on values the seller cannot see. A standard model: each of n bidders' values is drawn independently, spread evenly between 0 and top. Under the second-price rule everyone bids their value, so the revenue is the second-highest value. Under the first-price rule, economic theory finds that in this model each bidder's equilibrium bid is

b(v) = \frac{n-1}{n} \, v

(take it as given; deriving it needs more probability than this path uses). The highest value still places the highest bid, so the revenue is \frac{n-1}{n} times the highest value.

Averaging over many simulated auctions estimates the expected revenue: a Monte Carlo simulation. To make the result reproducible, create one generator rng = random.Random(seed) and draw every number from it, in a stated order. The same seed gives the same sequence:

import random
rng = random.Random(2024)
print([round(rng.random(), 4) for _ in range(4)])
rng = random.Random(2024)
print([round(rng.random(), 4) for _ in range(4)])
[0.4701, 0.7283, 0.3038, 0.8873]
[0.4701, 0.7283, 0.3038, 0.8873]

rng.random() is spread evenly over [0, 1), so top * rng.random() is spread over [0, \text{top}); rng.uniform(0, top) gives the same numbers. The order of drawing matters: the same six numbers, dealt morning by morning or bidder by bidder, make different auctions. Each line below lists three mornings of two values:

import random
rng = random.Random(5)
by_morning = [[round(100 * rng.random(), 1) for bidder in range(2)] for morning in range(3)]
rng = random.Random(5)
by_bidder = [[round(100 * rng.random(), 1) for morning in range(3)] for bidder in range(2)]
print(by_morning)
print([[by_bidder[b][m] for b in range(2)] for m in range(3)])
[[62.3, 74.2], [79.5, 94.2], [74.0, 92.2]]
[[62.3, 94.2], [74.2, 74.0], [79.5, 92.2]]

Re-creating the generator inside the loop is the other classic bug: every morning then gets the same values. Here is a simulation of the highest and the lowest of three values with top = 60:

import random
n, top, rounds = 3, 60, 20000
rng = random.Random(11)
high = low = 0.0
for _ in range(rounds):
    values = [top * rng.random() for _ in range(n)]
    high += max(values)
    low += min(values)
print("highest", round(high / rounds, 3), " formula", top * n / (n + 1))
print("lowest ", round(low / rounds, 3), " formula", top * 1 / (n + 1))
highest 44.825  formula 45.0
lowest  14.885  formula 15.0

The formula behind both: n values spread evenly over [0, \text{top}] cut it into n + 1 pieces of the same expected length, so the k-th smallest value has expected value \text{top} \cdot k / (n + 1). The second-highest is the (n-1)-th smallest, so

E[\text{second-price revenue}] = \text{top}\,\frac{n-1}{n+1}, \qquad E[\text{first-price revenue}] = \frac{n-1}{n} \cdot \text{top}\,\frac{n}{n+1} = \text{top}\,\frac{n-1}{n+1}

The same number. This is revenue equivalence, a result of the model (values independent and from the same spread, bidders following the equilibrium bids, the highest value winning): the two rules earn the same on average, although on any one day either may earn more. A short simulation can be far from the formula; a long one lands close.

Common mistakes: creating the generator inside the loop, or using the module's random.random() instead of rng. Drawing the numbers in a different order from the one stated. Charging the first-price winner their value instead of their shaded bid. Expecting a few rounds to match the formula.

Practice: Same Takings, Different Rules

Preference lists, rank tables and blocking pairs

Some markets have no prices: pupils and schools, junior doctors and hospitals, volunteers and animal shelters. Each side ranks the other, and a clearing house makes the placement. A preference list names the options from favourite to least favourite. A rank table answers the opposite question: where is this option in my list? Inverting a list gives the table:

prefs = [2, 0, 3, 1]            # shelter 2 first, then 0, 3 and 1
rank = [0] * len(prefs)
for r, s in enumerate(prefs):
    rank[s] = r
print(rank, {s: r for r, s in enumerate(prefs)})
print("prefers 3 to 1?", rank[3] < rank[1], "  prefers 1 to 0?", rank[1] < rank[0])
[1, 3, 0, 2] {2: 0, 0: 1, 3: 2, 1: 3}
prefers 3 to 1? True   prefers 1 to 0? False

prefs[0] is the favourite shelter; rank[0] is the position of shelter 0. Mixing the two up is the most common bug in matching code. With ranks, "prefers x to y" is one comparison; calling prefs.index(x) every time also works but walks the list each time.

A placement is stable when no volunteer and shelter that are not placed together would both rather be together than where they are. Such a pair is a blocking pair: it would quietly break the placement. Three volunteers and three shelters with one place each; volunteer v works at shelter match[v]:

vol_prefs = [[1, 0, 2], [1, 2, 0], [0, 1, 2]]       # shelters, favourite first
shel_prefs = [[0, 2, 1], [2, 0, 1], [1, 0, 2]]      # volunteers, favourite first
match = [0, 1, 2]

vol_rank = [{s: r for r, s in enumerate(p)} for p in vol_prefs]
shel_rank = [{v: r for r, v in enumerate(p)} for p in shel_prefs]
given = [None] * len(match)
for v, s in enumerate(match):
    given[s] = v                                    # the volunteer shelter s was given

def blocks(v, s):
    return vol_rank[v][s] < vol_rank[v][match[v]] and shel_rank[s][v] < shel_rank[s][given[s]]

print(blocks(0, 1), blocks(2, 0), blocks(1, 2))
True False False

Volunteer 0 prefers shelter 1 to their own, and shelter 1 prefers volunteer 0 to the volunteer it was given: a blocking pair, so this placement is not stable. Volunteer 2 would also love shelter 0, but shelter 0 already has its favourite, and volunteer 1 already has their favourite shelter, so no pair with volunteer 1 can block. Note what each side compares against: the volunteer against their own shelter, the shelter against its own volunteer (given, the inverse of match).

Common mistakes: requiring only one side to want the switch. Comparing the shelter's view of v with v's own partner instead of the shelter's. Reading a preference list as a rank table. Looking up list.index inside a double loop over all pairs, which turns O(n^2) work into O(n^3).

Practice: Cooks Who Would Rather Swap

Deferred acceptance

How do you find a stable placement? Deferred acceptance (David Gale and Lloyd Shapley's algorithm) lets one side propose while the other holds offers. With volunteers proposing to shelters, and shelter s having seats[s] places:

  1. Every volunteer who is not currently held applies to their favourite shelter among those that have not yet turned them down.
  2. Each shelter looks at everyone it holds plus its new applicants, keeps the seats[s] it likes best, and turns down the rest. Being held is tentative: someone held now can be turned down later, when a better applicant arrives.
  3. When a round ends with every volunteer held, the placement is final.

One shelter's answer in a round is one sort by its rank table:

def answer(held, applicants, seats, rank):
    pool = sorted(held + applicants, key=lambda v: rank[v])
    return pool[:seats], pool[seats:]            # kept, turned down

rank = {v: r for r, v in enumerate([2, 4, 3, 0, 1])}     # this shelter's list, favourite first
print(answer([], [0, 1, 3], 2, rank))
print(answer([3, 0], [2], 2, rank))
([3, 0], [1])
([2, 3], [0])

Five volunteers and three shelters with 2, 1 and 2 seats:

favourite first favourite first
volunteer 0 0, 1, 2 shelter 0 (2 seats) 2, 4, 3, 0, 1
volunteer 1 0, 2, 1 shelter 1 (1 seat) 3, 0, 4, 2, 1
volunteer 2 1, 0, 2 shelter 2 (2 seats) 1, 0, 2, 3, 4
volunteer 3 0, 1, 2
volunteer 4 1, 2, 0
round applications answers
1 0, 1, 3 to shelter 0; 2, 4 to shelter 1 shelter 0 holds 3 and 0, turns down 1; shelter 1 holds 4, turns down 2
2 1 to shelter 2; 2 to shelter 0 shelter 0 holds 2 and 3 and turns down 0, whom it held since round 1; shelter 2 holds 1
3 0 to shelter 1 shelter 1 prefers 0 to 4: holds 0, turns down 4
4 4 to shelter 2 shelter 2 has a free seat: holds 1 and 4

Everyone is held after round 4: volunteers 2 and 3 at shelter 0, volunteer 0 at shelter 1, volunteers 1 and 4 at shelter 2. Round 2 is the heart of it: volunteer 0 was held, then bumped by a better applicant, and moved down their own list.

Why it works. Each volunteer only moves down their own list, so the process ends after at most (volunteers × shelters) applications. What a shelter holds only gets better from its point of view. And the result is stable: if volunteer v prefers shelter s to where v ended, v applied to s earlier and was turned down, which happens only when s held enough applicants it likes more, and it never lets those go except for even better ones. The order in which applications are handled inside a round does not change the result.

Common mistakes: letting a turned-down volunteer apply to the same shelter again. Comparing only the new applicants and forgetting the ones already held. Filling seats first come, first served. Letting held volunteers apply again each round.

Practice: Apprentices Knock, Workshops Hold

Externalities: the cost the market leaves out

A sale is a deal between a buyer and a seller, but a factory's dust falls on people who are part of neither: an externality. Take Level 1's market with inverse demand P = a - bQ (what a buyer will pay for the Q-th unit) and the sellers' supply curve, their marginal private cost MPC = c + dQ. Each unit also does damage e to bystanders, the marginal external cost (constant here). The marginal social cost counts both:

MSC(Q) = c + dQ + e

The market stops where demand meets the private cost: the market quantity Q_m. Society's best stopping point, the efficient quantity Q^*, is where demand meets the social cost. Every unit between them costs society more than it is worth to its buyer, and the sum of those losses is the deadweight loss: the area between the MSC line and the demand line from Q^* to Q_m. A cement works, with inverse demand P = 90 - 1.5Q (dollars per tonne), private cost 15 + 0.5Q and dust damage 12 per tonne:

a, b, c, d, e = 90, 1.5, 15, 0.5, 12
q_m = (a - c) / (b + d)                       # a - bQ = c + dQ
q_s = (a - c - e) / (b + d)                   # a - bQ = c + dQ + e

def gap(q):                                   # social cost of unit q minus its value to a buyer
    return (c + d * q + e) - (a - b * q)

print(q_m, q_s, gap(q_s), gap(q_m))
print("triangle", 0.5 * (q_m - q_s) * gap(q_m))
step = (q_m - q_s) / 100000
print("thin strips", round(sum(gap(q_s + (k + 0.5) * step) * step for k in range(100000)), 6))
37.5 31.5 0.0 12.0
triangle 36.0
thin strips 36.0

The gap is 0 at Q^* and exactly e at Q_m: there a buyer's value equals the private cost, and the social cost is e more. Adding up thin strips (a sum standing in for the integral \int_{Q^*}^{Q_m} \text{gap}(Q)\,dQ) agrees with the triangle.

A Pigouvian tax. A tax of t per tonne raises the firms' cost to c + t + dQ. With t = e their cost line is the social one, and the market stops at Q^* by itself: the tax makes the firms feel the damage they do.

Corners. Quantities cannot be negative. If even the first tonne's social cost, c + e, is above what any buyer will pay, a, the efficient quantity is 0. The deadweight loss is still the area between the two lines, now from 0 to Q_m; but at 0 the gap is not 0, it is c + e - a > 0. The area is a trapezoid: the average of its two parallel sides times its width. With damage 100 per tonne instead of 12:

a, b, c, d, e = 90, 1.5, 15, 0.5, 100
q_m = max(0.0, (a - c) / (b + d))
q_s = max(0.0, (a - c - e) / (b + d))

def gap(q):
    return (c + d * q + e) - (a - b * q)

print(q_m, q_s, gap(q_s), gap(q_m))
print("trapezoid", (gap(q_s) + gap(q_m)) / 2 * (q_m - q_s), "  the triangle would say", 0.5 * e * (q_m - q_s))
37.5 0.0 25.0 100.0
trapezoid 2343.75   the triangle would say 1875.0

A corner changes the tax too. The aim is now to stop all sales, and any tax that lifts the first tonne's private cost to the buyers' top value does it: c + t \ge a. A tax of 100 works, but so does 75, the smallest; anything between them only changes a number on paper, since nothing is sold. And if the market trades nothing even without a tax (a \le c), no tax is needed at all.

Common mistakes: forgetting to clip a negative quantity at 0. Using the triangle \frac12 e (Q_m - Q^*) at a corner, where the area is a trapezoid. Answering t = e at a corner, where a smaller tax already stops every sale. Counting tax revenue as part of the loss.

Practice: Smoke Over the Brickworks

Public goods: free riders and the Samuelson condition

A public good is non-rival: one person's enjoyment takes nothing from anyone else's (a lighthouse beam, a fireworks show, a lifeguard on a lake beach), and often nobody can be kept from it. Three households by a lake can pay for lifeguard hours H, at c = 10 dollars an hour. Household i values one more hour at

MB_i(H) = \max(0,\ a_i - b_i H)

its marginal benefit, which falls as the hours grow and is never negative.

Voluntary contributions. Each household chooses how many hours h_i \ge 0 it pays for, and H = \sum_i h_i. A household alone would buy hours until its marginal benefit fell to the price: its own best level solves MB_i(H) = c, or is 0 if a_i \le c. Now use Level 4's Nash equilibrium, where nobody gains by changing only their own choice. If the others already provide more than your own best level, your next hour costs 10 and is worth less than 10 to you: you add nothing. If the total is below your own best level, you would add hours. So in equilibrium the total is the highest own best level, the keenest household pays for all of it, and everyone else free-rides.

a, b, c = [12, 9, 4], [1, 1.5, 2], 10
own = [max(0.0, (a[i] - c) / b[i]) for i in range(len(a))]
H = max(own)
mbs = [max(0, a[i] - b[i] * H) for i in range(len(a))]
print("own best levels", own, " voluntary total", H)
print("marginal benefits there", mbs, " their sum", sum(mbs))
own best levels [2.0, 0.0, 0.0]  voluntary total 2.0
marginal benefits there [10.0, 6.0, 0]  their sum 16.0

At 2 hours the keenest household values one more hour at exactly 10, the others at 6 and 0. Another hour costs 10 but is worth 16 to the three households together.

The efficient level. An hour of lifeguard is enjoyed by all three at once, so its value is the sum of the marginal benefits. Hours should be added while that sum exceeds the cost, which gives the Samuelson condition:

\sum_i MB_i(H^*) = c

For a private good each unit goes to one person, and each buyer stops at their own MB_i = c; for a public good the benefits add up. The sum of the max(0, ...) lines is piecewise linear: it falls, with a kink at each H = a_i / b_i where one household's benefit runs out (here at 2, 6 and 12). Walk the kinks, as with Level 3's kinked demand: up to H = 2 all three count, and 25 - 4.5H = 10 gives H = 3.33, past the kink, so not on this stretch. From 2 to 6 only households 0 and 1 count: 21 - 2.5H = 10 gives H = 4.4, inside. Or bisect, as in Levels 1 and 2: the sum falls as H grows, so halving an interval traps the crossing.

a, b, c = [12, 9, 4], [1, 1.5, 2], 10

def total_mb(H):
    return sum(max(0, a[i] - b[i] * H) for i in range(len(a)))

lo, hi = 0.0, max(a[i] / b[i] for i in range(len(a)))      # total_mb(lo) > c, total_mb(hi) = 0
while hi - lo > 1e-12:
    mid = (lo + hi) / 2
    if total_mb(mid) > c:
        lo = mid
    else:
        hi = mid
print(round(lo, 6), round(total_mb(4.4), 6))
4.4 10.0

Together the households would want 4.4 hours; voluntary giving buys 2. Bisection needs total_mb(0) > c to start; if even the first hour is worth no more than c to everyone together, the efficient level is 0. Press Run to see the kinked sum against the cost:

a, b, c = [12, 9, 4], [1, 1.5, 2], 10
hs = [k / 20 for k in range(161)]
plot(hs, [sum(max(0, a[i] - b[i] * h) for i in range(len(a))) for h in hs], label="sum of marginal benefits", xlabel="hours H", ylabel="dollars per hour")
plot(hs, [c for h in hs], label="cost per hour")

Common mistakes: adding up everyone's own best level as the voluntary total (the others' hours count for you too, so they are not extra). Forgetting the max(0, ...), so a household's benefit turns negative and drags the sum down. Applying MB_i = c person by person to find the efficient level of a public good.

Practice: The Village Fireworks Fund

Cutting pollution at least cost: permits

A regulator wants a total cut in emissions. Firm i cutting x tonnes pays \frac12 k_i x^2, so by Level 3's slope rule the cost of one more tonne, its marginal abatement cost, is k_i x. A cut is cost-effective (the total reached as cheaply as possible) when every firm that cuts some, but not all, of its emissions has the same marginal cost. If one firm's next tonne cost 30 and another's 20, moving one tonne of cutting from the first to the second would save about 10.

Two firms with k = 3 and k = 1 must cut 20 tonnes together. Equal marginal costs, 3 x_0 = x_1, with x_0 + x_1 = 20, give 5 and 15 tonnes: a cost of \frac12 \cdot 3 \cdot 25 + \frac12 \cdot 1 \cdot 225 = 150, against 200 for cutting 10 each.

A permit market finds this without the regulator knowing anyone's costs. It issues permits only for the emissions it allows, and firms trade them at a price p per tonne. Cutting a tonne saves a permit worth p, so each firm cuts until its marginal cost reaches p, and all of them end with the same marginal cost, p. But a firm cannot cut more than it emits:

x_i(p) = \min\left(e_i, \ \frac{p}{k_i}\right)

The total cut grows with p, with a kink at p = k_i e_i where firm i runs out of emissions to cut. The permit price is where the total equals the target: walk the kinks, or bisect. Three firms emit 6, 40 and 25 tonnes and must cut 45:

emissions, slopes, target = [6, 40, 25], [3, 1, 2], 45

def cuts(p):
    return [min(emissions[i], p / slopes[i]) for i in range(len(emissions))]

print("ignoring the caps:", round(target / sum(1 / k for k in slopes), 4), " kinks:", [slopes[i] * emissions[i] for i in range(3)])
lo, hi = 0.0, float(max(slopes[i] * emissions[i] for i in range(3)))
while hi - lo > 1e-12:
    mid = (lo + hi) / 2
    if sum(cuts(mid)) < target:
        lo = mid
    else:
        hi = mid
x = cuts(hi)
print("price", round(hi, 6), " cuts", [round(v, 6) for v in x], " cost", round(sum(slopes[i] * x[i] ** 2 / 2 for i in range(3)), 4))
ignoring the caps: 24.5455  kinks: [18, 40, 50]
price 26.0  cuts [6, 26.0, 13.0]  cost 561.0

Without the caps the price would be 24.55, but firm 0 runs out at 18: it cuts all 6 tonnes, and firms 1 and 2 share the other 39, p/1 + p/2 = 39, so p = 26. At the solution every firm that still emits has marginal cost 26, and firm 0's last tonne cost only 18. If the target is all emissions, every price at or above the highest kink works, and a problem must say which one to report.

Common mistakes: letting a firm cut more than it emits. Taking the price from the formula without caps. Splitting the cut equally, or in proportion to emissions, and calling it cheapest.

Practice: Smoke Permits for Three Foundries

Bundles of items and VCG payments

Some items are worth more together: two neighbouring market stalls, a pair of meeting rooms, a block of radio frequencies. In a combinatorial auction bidders bid on bundles. The seller accepts a set of bids in which no item appears twice, and the natural choice is the set with the largest total value, the welfare.

A bundle is a set of items, and a set of up to a few dozen items fits in one integer, a bitmask: item j is bit j, 1 << j. Two bundles clash exactly when mask1 & mask2 is not 0, and used | mask adds a bundle to the items already taken. To find the best set, backtrack over the bids: for each bid, try taking it (if it clashes with nothing taken), then try skipping it. That visits every compatible set and never builds a clashing one.

rooms = "XYZ"
bids = [("ivy", "XY", 50), ("jon", "X", 30), ("kim", "YZ", 35), ("lou", "Z", 20)]

def mask(bundle):
    return sum(1 << rooms.index(r) for r in bundle)

print([bin(mask(bundle)) for _, bundle, _ in bids], mask("XY") & mask("YZ"), mask("X") & mask("YZ"))

def best(bids):
    masks = [mask(bundle) for _, bundle, _ in bids]
    top = 0

    def search(i, used, total):
        nonlocal top
        if i == len(bids):
            top = max(top, total)
            return
        if not used & masks[i]:                          # take bid i if its rooms are free
            search(i + 1, used | masks[i], total + bids[i][2])
        search(i + 1, used, total)                       # or skip it

    search(0, 0, 0)
    return top

W = best(bids)
print("best welfare", W)
for name, value in (("ivy", 50), ("lou", 20)):           # the winners: ivy's XY and lou's Z
    without = best([bid for bid in bids if bid[0] != name])
    print(name, " best without them", without, " others get now", W - value, " pays", without - (W - value))
['0b11', '0b1', '0b110', '0b100'] 2 0
best welfare 70
ivy  best without them 65  others get now 20  pays 45
lou  best without them 65  others get now 50  pays 15

The best set is ivy's XY with lou's Z, 70 in all (jon with kim reach only 65). To report the set and not just its value, carry the list of chosen indices through the search and keep it whenever a better total turns up, with a stated rule for ties.

What should winners pay? Charging bids gives bidders a reason to shade, as in a first-price auction. The Vickrey-Clarke-Groves (VCG) rule charges each winner the harm their presence does to the others:

\text{pay}_t = W_{-t} - (W - v_t)

W_{-t} is the best welfare the others could reach if t were not there; W - v_t is what the others actually get. Without ivy, jon and kim would get 65; with ivy the others get only lou's 20, so ivy pays 45. Without lou the others could again reach 65; with lou they get 50, so lou pays 15. With a single item, W_{-t} is the second-highest bid and the others get nothing, so VCG is exactly the second-price rule, and for the same reason bidding true values is safe: your bid changes whether you win, not the harm you cause. A payment is never negative and never above the winner's own value; one outside that range signals a bug. If a bidder sends several bids, throw away all of them to compute W_{-t}.

Common mistakes: removing only the winning bid of a bidder who sent several. Charging winners their own bids. Forgetting that the empty set (welfare 0) is a valid choice. Recomputing the masks in every call of the search instead of once.

Practice: Market Stalls Sold in Bundles

Real rates and money growth: exact factors

Now the macro half, with every rate written as a fraction: 4 % is 0.04. A savings account pays a nominal rate i: one crown grows to 1 + i crowns. Prices rise by the inflation rate \pi: a basket that cost one crown now costs 1 + \pi. What the saver can buy grows by the ratio, which defines the real rate r, the Fisher relation:

1 + r = \frac{1 + i}{1 + \pi}

Multiplying out 1 + i = (1 + r)(1 + \pi) = 1 + r + \pi + r\pi and dropping the small product r\pi gives the common shortcut r \approx i - \pi. It is close for rates of a few per cent and poor when inflation is high:

rates = [0.03, 0.06, 0.40]
inflation = [0.01, 0.07, 0.30]
prices = 1.0
for i, p in zip(rates, inflation):
    print(i, p, " exact", round((1 + i) / (1 + p) - 1, 6), " shortcut", round(i - p, 6))
    prices *= 1 + p
print("prices over the three years: x", round(prices, 6), " rise", round(prices - 1, 6), " (adding the rates says", round(sum(inflation), 6), ")")
0.03 0.01  exact 0.019802  shortcut 0.02
0.06 0.07  exact -0.009346  shortcut -0.01
0.4 0.3  exact 0.076923  shortcut 0.1
prices over the three years: x 1.40491  rise 0.40491  (adding the rates says 0.38 )

zip walks two lists side by side. Over several years growth factors multiply, as compound interest does: prices rose by 40.5 % here, not by the 38 % that adding the yearly rates suggests. A real return over several years works the same way, with the yearly factors (1 + i)/(1 + \pi).

The quantity equation. Money M changes hands V times a year on average (the velocity), and that spending buys real output Y at the price level P:

M V = P Y

So P = MV/Y, and in growth factors from one year to the next,

(1 + g_M)(1 + g_V) = (1 + \pi)(1 + g_Y)

where g_X = X_t / X_{t-1} - 1. The shortcut adds and subtracts rates: \pi \approx g_M + g_V - g_Y. With money up 8 %, velocity down 2 % and output up 3 %:

M, V, Y = [400, 432], [5, 4.9], [1000, 1030]
P = [M[t] * V[t] / Y[t] for t in range(2)]
gM, gV, gY = M[1] / M[0] - 1, V[1] / V[0] - 1, Y[1] / Y[0] - 1
print("price levels", [round(x, 6) for x in P], " inflation", round(P[1] / P[0] - 1, 6))
print("from the factors", round((1 + gM) * (1 + gV) / (1 + gY) - 1, 6), " shortcut", round(gM + gV - gY, 6))
price levels [2.0, 2.055146]  inflation 0.027573
from the factors 0.027573  shortcut 0.03

The factor equation also runs backwards: to get a chosen inflation rate when velocity and output grow at known rates, solve it for 1 + g_M = (1 + \pi)(1 + g_Y) / (1 + g_V).

Common mistakes: writing (1 + p) / (1 + i) or i / p. Adding yearly rates instead of multiplying factors. Using the shortcut where the exact rate is asked for. Dividing by velocity instead of multiplying.

Practice: What the Savings Account Really Earned, More Coins, Same Harvest

The Taylor rule: a policy rate from two errors

A central bank sets a short-term policy rate i. Two things guide it: inflation \pi against its target \pi^*, and the output gap y, how far output is above its normal (potential) level, as a fraction of normal output (y = -0.02 means output is 2 % below normal). A Taylor rule writes the reaction as a formula:

i = r^* + \pi + a\,(\pi - \pi^*) + b\,y

r^* is the neutral real rate, the real rate that neither speeds the economy up nor slows it down. When inflation is on target and the gap is 0, the rule sets i = r^* + \pi^*: a real rate of exactly r^*. Subtracting \pi from both sides shows what the rule does to the real rate (the shortcut Fisher relation):

i - \pi = r^* + a\,(\pi - \pi^*) + b\,y
r_star, pi_star, a, b = 0.015, 0.025, 0.6, 0.4
for pi, y in [(0.025, 0.0), (0.035, 0.01), (0.01, -0.03)]:
    i = r_star + pi + a * (pi - pi_star) + b * y
    print(pi, y, " rate", round(i, 6), " real rate", round(i - pi, 6))
0.025 0.0  rate 0.04  real rate 0.015
0.035 0.01  rate 0.06  real rate 0.025
0.01 -0.03  rate 0.004  real rate -0.006

With inflation and output above normal the real rate rises above neutral (0.025 against 0.015), which cools spending; in a slump with low inflation it falls below, here even below zero. In control terms the rule is a proportional controller acting on two errors, \pi - \pi^* and y, with gains a and b. The lone \pi term matters: without it the rule would set a real rate, not a nominal one.

The Taylor principle. With the gap unchanged, one more point of inflation raises the nominal rate by 1 + a points and the real rate by a points. A rule obeys the Taylor principle when the real rate goes up, strictly: 1 + a > 1, that is a > 0.

r_star, pi_star, b, y = 0.015, 0.025, 0.4, 0.0
for a in (-0.3, 0, 0.6):
    rule = lambda pi: r_star + pi + a * (pi - pi_star) + b * y
    di = rule(0.04) - rule(0.03)
    print("a =", a, " inflation +0.01: rate", round(di, 6), " real rate", round(di - 0.01, 6))
a = -0.3  inflation +0.01: rate 0.007  real rate -0.003
a = 0  inflation +0.01: rate 0.01  real rate 0.0
a = 0.6  inflation +0.01: rate 0.016  real rate 0.006

With a = 0 the nominal rate keeps pace with inflation, but the real rate does not rise at all, and nothing pushes inflation back. With a < 0 the real rate falls as inflation rises, like a thermostat that turns the heating up when the room gets warmer.

Aside (optional): do central banks follow a rule? Real central banks set their rate by committee, after looking at many indicators, and none follows a published formula mechanically; Taylor rules serve as benchmarks that central banks and observers compare decisions with. Some economists argue that policy should stay close to a stated rule, because a rule is predictable, easy to hold to account, and protects against the temptation described in the last section of this lesson. Others reply that the neutral rate and the output gap cannot be observed and are estimated with large and shifting errors, that a formula with two inputs leaves out financial crises and other shocks, and that judgment is needed. How closely policy should follow rules is still debated. Problems here state the rule and ask what it implies.

Common mistakes: dropping the lone \pi term. Using \pi^* where \pi belongs. Treating a = 0 as obeying the principle: the real rate must rise, strictly. Flooring a negative rate at zero when the problem does not ask for it.

Practice: The Rate the Rulebook Asks For

The Phillips curve and the natural rate

Wages and prices are set partly on what people expect inflation to be, \pi^e. When jobs are scarce, workers settle for less and firms raise prices less; when workers are scarce, the reverse. The expectations-augmented Phillips curve puts that in one line, with u the unemployment rate:

\pi = \pi^e + \kappa\,(u_n - u)

u_n is the natural rate of unemployment: at u = u_n inflation is exactly what was expected. Each point of unemployment below it adds \kappa points of inflation; each point above takes \kappa away.

pi_e, kappa, u_n = 0.025, 0.4, 0.055
for u in (0.035, 0.045, 0.055, 0.075):
    print(u, round(pi_e + kappa * (u_n - u), 6))
u = u_n - (0.015 - pi_e) / kappa             # the unemployment rate that gives inflation 0.015
print("for 0.015:", round(u, 6), " check:", round(pi_e + kappa * (u_n - u), 6))
0.035 0.033
0.045 0.029
0.055 0.025
0.075 0.017
for 0.015: 0.08  check: 0.015

The curve is a straight line in u, so reading it backwards is solving a linear equation: move \pi^e across, divide by \kappa, subtract from u_n. Lower inflation than expected needs unemployment above the natural rate. The same unemployment gives higher inflation when people expect more: the curve moves with expectations, which the next two sections follow over time.

Aside (optional): how reliable is the Phillips curve? In the 1960s many economists read the data as a stable trade-off: a little more inflation for permanently lower unemployment. The 1970s, with high inflation and high unemployment together, favoured the version with expectations and a natural rate, in which the trade-off lasts only while expectations lag behind. Since the 1990s, in many countries, inflation has moved little while unemployment swung widely, so the curve looks flat. Some read that as expectations being well anchored by credible central banks; others as a weaker link between slack and prices, for example because of global competition; others point to the natural rate being hard to measure and possibly moving over time. The 2021-2023 surge of inflation renewed the debate. Problems here state the curve and its parameters.

Common mistakes: writing u - u_n, so the curve slopes the wrong way. Forgetting \pi^e, which leaves only the change from expectations. Multiplying by \kappa instead of dividing when solving for u.

Practice: Jobs Now, Prices Later

Expectations that chase inflation

Where do expectations come from? Level 3 used adaptive expectations: people expect what happened last period, \pi^e_t = \pi_{t-1}. A more general rule pulls them partly towards an anchor, the inflation rate the central bank has promised:

\pi^e_t = \theta\,\pi_{t-1} + (1 - \theta)\,\text{anchor}, \qquad 0 \le \theta \le 1

With \theta = 1 expectations are fully adaptive; with \theta = 0 they never leave the anchor. A simulation carries last year's inflation in one variable, and updates it only after this year is computed and recorded. A four-year boom with unemployment one point below the natural rate, then four years back at the natural rate:

anchor, kappa, u_n = 0.02, 0.5, 0.05
us = [0.04] * 4 + [0.05] * 4
for theta in (1, 0.6):
    prev = 0.02                                  # inflation in the year before the first
    path = []
    for u in us:
        pi_e = theta * prev + (1 - theta) * anchor
        pi = pi_e + kappa * (u_n - u)
        path.append(round(pi, 6))
        prev = pi                                # only now does this year become "last year"
    print(theta, path)
1 [0.025, 0.03, 0.035, 0.04, 0.04, 0.04, 0.04, 0.04]
0.6 [0.025, 0.028, 0.0298, 0.03088, 0.026528, 0.023917, 0.02235, 0.02141]

With fully adaptive expectations every boom year adds the same 0.005, and when unemployment returns to normal, inflation stays at 4 %: each year's surprise became next year's expectation. Holding unemployment below the natural rate for longer would make inflation climb year after year, which is why this version is called accelerationist, and why u_n is also called the non-accelerating-inflation rate of unemployment. With anchored expectations (\theta = 0.6) only 60 % of each year's inflation carries over, and once the boom ends inflation drifts back towards the anchor.

Holding u fixed, the anchored version has a rest point, where this year's inflation equals last year's: x = \theta x + (1 - \theta)\,\text{anchor} + \kappa(u_n - u), so x = \text{anchor} + \kappa(u_n - u)/(1 - \theta). With \theta = 0.6 and the boom held for ever that is 0.02 + 0.005/0.4 = 0.0325. With \theta = 1 there is none: inflation climbs without end. Add plot(path, label=str(theta)) after the inner loop and press Run to see the two shapes.

Common mistakes: using this year's inflation as this year's expectation. Updating prev before the year is recorded. Swapping \theta and 1 - \theta. Counting years from 1 when the problem counts from 0. When a problem asks for the first year past a threshold, recording later crossings too.

Practice: The Boom That Keeps Costing More

The cost of bringing inflation down

Run the Phillips curve the other way: to bring inflation down, a central bank in this model holds unemployment above the natural rate by x_t \ge 0 points (the excess unemployment), and with \pi^e_t = \pi_{t-1} each year removes \kappa x_t of inflation. Okun's law turns unemployment into lost output: output falls below normal by \text{okun} \cdot x_t (a fraction of normal output). The sacrifice ratio is the total output lost divided by the fall in inflation it bought.

With fully adaptive expectations the speed does not matter. Three plans, each with 0.06 of excess unemployment in total, starting from 6 % inflation:

kappa, okun = 0.4, 2.5
for plan in ([0.03, 0.02, 0.01], [0.06], [0.01] * 6):
    pi = 0.06
    for x in plan:
        pi = pi - kappa * x                    # expectations = last year's inflation
    loss = sum(okun * x for x in plan)
    print(plan, " inflation", round(pi, 6), " output lost", round(loss, 6), " ratio", round(loss / (0.06 - pi), 6))
[0.03, 0.02, 0.01]  inflation 0.036  output lost 0.15  ratio 6.25
[0.06]  inflation 0.036  output lost 0.15  ratio 6.25
[0.01, 0.01, 0.01, 0.01, 0.01, 0.01]  inflation 0.036  output lost 0.15  ratio 6.25

Each point of excess unemployment removes \kappa points of inflation for good and costs okun points of output, so the ratio is \text{okun}/\kappa = 6.25 whatever the path. When people partly believe the target (\theta < 1 with the target as anchor), expectations fall on their own each year, and the same disinflation costs less: this is why central banks care about credibility.

Loops that stop on a float. A simulation that runs "while inflation is above the target" compares floats, and a value that is equal in real arithmetic can come out a hair above it:

for tol in (0, 1e-9):
    pi, years = 0.05, 0
    while pi > 0.02 + tol:
        pi -= 0.005
        years += 1
    print("tolerance", tol, " years", years, " inflation", pi)
tolerance 0  years 7  inflation 0.015000000000000006
tolerance 1e-09  years 6  inflation 0.020000000000000007

Six steps of 0.005 take 0.05 to 0.02 exactly, but in floats the sixth leaves 0.020000000000000007, and the strict comparison runs a seventh year. A stated tolerance, pi > target + 1e-9, makes "reached the target" mean what it says.

Common mistakes: dividing the loss by the final inflation rate instead of by the fall. Comparing floats without the stated tolerance. Forgetting that with \theta < 1 expectations move before the bank acts.

Practice: What It Costs to Bring Prices Down

Three equations, one quarter at a time

Now put demand, inflation and the bank into one model and let time run in quarters t = 0, 1, 2, \ldots, with y_t the output gap, \pi_t inflation and i_t the policy rate. A common small version has three equations:

  • demand (IS): y_t = d_t - \sigma\,(i_{t-1} - \pi_{t-1} - r^*). Spending reacts to last quarter's real rate: above neutral it cools the economy, below neutral it heats it. d_t is a demand shock.
  • Phillips curve: \pi_t = \pi_{t-1} + \kappa\,y_t, adaptive expectations in quarters: inflation rises while output is above normal.
  • policy rule: i_t = r^* + \pi_t + a\,(\pi_t - \pi^*) + b\,y_t, using this quarter's numbers.

The timing is the whole model. The bank sees this quarter's inflation and gap, but its rate reaches spending only a quarter later: a feedback loop with a delay of one step, as on the Signals & Control path. Before quarter 0 the economy is at rest: \pi_{-1} = \pi^* and i_{-1} = r^* + \pi^*, so last quarter's real rate was neutral. A shock that fades is written \text{shock} \cdot \rho^t: full size in quarter 0, then shrinking by the factor \rho. Python agrees that \rho^0 = 1 even when \rho = 0:

print(0 ** 0, 0.0 ** 0, [0.5 ** t for t in range(4)], [0.0 ** t for t in range(4)])
1 1.0 [1.0, 0.5, 0.25, 0.125] [1.0, 0.0, 0.0, 0.0]

The example below hits the economy from the other side: a cost shock s_t = \text{shock} \cdot \rho^t (dearer energy, say) that raises inflation directly, \pi_t = \pi_{t-1} + \kappa\,y_t + s_t, with no shock in demand. The loop carries two numbers from quarter to quarter, last quarter's rate and inflation, and moves this quarter's values into them only after everything this quarter needs has been computed and recorded:

sigma, kappa, r_star, pi_star = 1.0, 0.25, 0.01, 0.02
a, b = 0.5, 0.5
shock, rho = 0.01, 0.5                         # costs jump in quarter 0, the jump halves each quarter
i_prev, pi_prev = r_star + pi_star, pi_star    # at rest before quarter 0
for t in range(6):
    y = -sigma * (i_prev - pi_prev - r_star)            # demand: last quarter's real rate
    pi = pi_prev + kappa * y + shock * rho ** t         # inflation: last quarter's, the gap, the cost shock
    i = r_star + pi + a * (pi - pi_star) + b * y        # the rule: this quarter's numbers
    print(t, round(y, 6) + 0.0, round(pi, 6), round(i, 6))
    i_prev, pi_prev = i, pi                             # this quarter becomes "last quarter"
0 0.0 0.03 0.045
1 -0.005 0.03375 0.048125
2 -0.004375 0.035156 0.050547
3 -0.005391 0.035059 0.049893
4 -0.004834 0.034475 0.049296
5 -0.004821 0.033582 0.047963

Quarter 0: the real rate set before the shock was neutral, so the gap is 0, inflation jumps by the shock to 3 %, and the rule lifts the rate to 4.5 %. Quarter 1: that rate is half a point above neutral in real terms, so output falls half a point below normal. Inflation still rises while the cost shock lasts, then the negative gap pulls it back, slowly, since \kappa is small. (+ 0.0 turns a printed -0.0 into 0.0.) The path after one push is the model's impulse response. Press Run to see it:

sigma, kappa, r_star, pi_star, a, b, shock, rho = 1.0, 0.25, 0.01, 0.02, 0.5, 0.5, 0.01, 0.5
i_prev, pi_prev = r_star + pi_star, pi_star
gaps, inflation, rates = [], [], []
for t in range(24):
    y = -sigma * (i_prev - pi_prev - r_star)
    pi = pi_prev + kappa * y + shock * rho ** t
    i = r_star + pi + a * (pi - pi_star) + b * y
    gaps.append(y)
    inflation.append(pi)
    rates.append(i)
    i_prev, pi_prev = i, pi
plot(inflation, label="inflation", xlabel="quarter")
plot(rates, label="policy rate")
plot(gaps, label="output gap")

A demand shock enters the other way: d_t = \text{shock} \cdot \rho^t is added to the demand equation, and inflation follows the gap.

Common mistakes: letting demand react to this quarter's rate. Moving this quarter's values into i_prev and pi_prev before the quarter is finished. Starting the rate at r^* instead of r^* + \pi^*. Applying the shock from quarter 1 instead of quarter 0.

Practice: Ripples After the Spending Spree

Scoring and tuning a rule

Which coefficients a and b make a good rule? Give each rule a loss: simulate the same shock, and add up the squared misses of the inflation target and, with a weight \lambda, the squared output gaps. Measure the misses in percentage points, 100 \cdot (\pi_t - \pi^*), so that a miss of 0.004 counts as 0.4: squares of small fractions are tiny (0.004^2 = 0.000016), and percentage points keep the numbers readable.

L = \sum_{t=0}^{T-1} \Big[ \big(100\,(\pi_t - \pi^*)\big)^2 + \lambda\,\big(100\,y_t\big)^2 \Big]

Then a grid search tries every pair. Two nested loops list the pairs in a fixed order, and replacing the best only on a strictly smaller loss keeps the first of equal losses. This is how a control engineer tunes a controller, and how the Machine Learning path picks hyperparameters. For the cost-shock economy of the last section:

def loss(a, b, lam, T=24):
    sigma, kappa, r_star, pi_star, shock, rho = 1.0, 0.25, 0.01, 0.02, 0.01, 0.5
    i_prev, pi_prev = r_star + pi_star, pi_star
    total = 0.0
    for t in range(T):
        y = -sigma * (i_prev - pi_prev - r_star)
        pi = pi_prev + kappa * y + shock * rho ** t
        i = r_star + pi + a * (pi - pi_star) + b * y
        total += (100 * (pi - pi_star)) ** 2 + lam * (100 * y) ** 2
        i_prev, pi_prev = i, pi
    return total

for lam in (0, 2):
    best = None
    for a in (0, 0.5, 1, 2, 4):
        for b in (0, 0.5, 1):
            L = loss(a, b, lam)
            if best is None or L < best[2]:
                best = (a, b, L)
    print("lam", lam, " best", best[:2], round(best[2], 4), "  passive rule", round(loss(0, 0, lam), 4), "  a = 4, b = 0:", round(loss(4, 0, lam), 4))
lam 0  best (4, 0) 1.3333   passive rule 89.3333   a = 4, b = 0: 1.3333
lam 2  best (1, 0.5) 19.5449   passive rule 89.3333   a = 4, b = 0: 44.0

When only inflation counts (lam = 0), the strongest reaction on the grid wins: it pulls inflation back at once, and the output gaps it causes cost nothing in this score. When the board also minds output (lam = 2), that same rule scores 44 and a milder one wins. The passive rule a = b = 0 scores 89.33 either way: the real rate never moves, so the gap stays 0, but inflation keeps every push the shock gave it and ends 2 points above target for good. Which rule is "best" depends on the weight, and the weight is a choice the model cannot make for you.

Common mistakes: forgetting the factor 100, or putting it outside the square. Continuing a simulation from the previous rule's final state instead of from rest. Using <=, which keeps the last of equal losses.

Practice: Choosing the Bank's Reflexes

The zero lower bound: a rate that saturates

A central bank cannot push its rate far below zero: savers would hold cash, which pays zero. Many models therefore floor the rate at 0: the rate actually set is \max(0, \text{rule}). In control terms the actuator saturates, like a valve that cannot open past fully open, or the clamped output of the Signals & Control path's PI controller. What matters is that demand reacts to the rate actually set, so the floor goes on the rate before it is carried to the next quarter.

At a zero rate the real rate is 0 - \pi = -\pi. As inflation falls, that real rate rises: the opposite of what the rule asks for.

r_star, pi_star, a, b = 0.01, 0.02, 0.5, 0.5
for pi, y in [(0.02, 0.0), (0.005, -0.02), (-0.004, -0.03), (-0.01, 0.0)]:
    rule = r_star + pi + a * (pi - pi_star) + b * y
    rate = max(0.0, rule)
    print(pi, y, " rule", round(rule, 6), " rate set", round(rate, 6), " real rate", round(rate - pi, 6), " neutral", r_star)
0.02 0.0  rule 0.03  rate set 0.03  real rate 0.01  neutral 0.01
0.005 -0.02  rule -0.0025  rate set 0.0  real rate -0.005  neutral 0.01
-0.004 -0.03  rule -0.021  rate set 0.0  real rate 0.004  neutral 0.01
-0.01 0.0  rule -0.015  rate set 0.0  real rate 0.01  neutral 0.01

In the second and third rows the rule wants a negative rate and gets 0, so the real rate stays higher than the rule intended, and demand is weaker than the rule planned. The last row is a deflation trap: inflation is -r^*, so a zero rate gives a real rate of exactly r^*, neutral. Once the shock is gone, the gap is 0, inflation stays where it is, and the rule keeps asking for a negative rate every quarter. If inflation falls below -r^*, the real rate at zero is above neutral, demand falls, and inflation falls further. In a simulation, count a quarter as binding when the rule's value is below 0 and the rate was raised to the floor.

Common mistakes: flooring only the recorded rate while demand still reacts to the negative one. Counting binding quarters in the run without the floor. Starting a running minimum at 0, which hides a minimum above zero. Sharing state between a run with the floor and one without.

Practice: When the Rate Cannot Go Lower

Promises and time inconsistency

A central bank promises low inflation. Its board also wishes output were above normal, and in one classic model (Robert Barro and David Gordon's) the only way to get that is to surprise people: output rises above normal by c\,(\pi - \pi^e) when inflation beats expectations. People set wages and prices first, then the bank chooses inflation. Can the promise be believed? Answering takes two techniques.

Minimising a quadratic. A loss made of squares, such as L(x) = (x - 2)^2 + 3(x - 6)^2, is a U-shaped parabola, and its lowest point is where its slope is zero. By Level 3's slope rule, (x - m)^2 has slope 2(x - m), so

L'(x) = 2(x - 2) + 6(x - 6) = 0 \quad\Longrightarrow\quad 8x = 40, \quad x = 5

A grid search agrees:

def L(x):
    return (x - 2) ** 2 + 3 * (x - 6) ** 2

print(L(5), min((round(L(k / 1000), 9), k / 1000) for k in range(0, 10001)))
12 (12.0, 5.0)

When the loss also depends on a number z that someone else chose, the same steps give the best x as a function of z, a best response. For L(x) = (x - 2)^2 + 3(x - z - 1)^2 the slope condition 2(x - 2) + 6(x - z - 1) = 0 gives x = (5 + 3z)/4.

Fixed points. Suppose people expect z, and the expectation is right only if the best response to it is z itself: z = (5 + 3z)/4, so z = 5, a fixed point. A public that learns reaches it by repeating "expect what was chosen last time"; each round closes a quarter of the gap here, since the best response moves only 3/4 as far as the expectation. A while loop with an explicit tolerance counts the rounds, starting at round 0 and checking before it updates:

def best(z):
    return (5 + 3 * z) / 4

z, rounds, tol = 2.0, 0, 0.01
while abs(best(z) - z) > tol:
    z = best(z)
    rounds += 1
print(rounds, round(z, 6), round(best(z) - z, 6))
16 4.969932 0.007517

Now the model's logic, in words. If the bank can commit to its target, people expect it, there is no surprise, and output stays normal. If people believe the promise but the bank is free to choose after they have set wages, its best response to \pi^e = \pi^* is more inflation than promised, since a little surprise buys output: the promise is time-inconsistent, worth breaking once others have acted on it. People who understand this expect exactly what the bank will choose: discretion is the fixed point, where the best response equals the expectation. There no surprise is left, so output is normal anyway, yet inflation sits above target, an inflation bias, and the bank is worse off than if it could commit. Rules, independent central banks and reputations are all attempts to make commitment possible.

Common mistakes: dropping a factor in the slope ((x - m)^2 has slope 2(x - m)). Computing a surprise's output with \pi^e = \pi. Counting rounds from 1, or updating before the first check. Forgetting that at the fixed point the output term of a loss can still be large.

Practice: A Promise the Bank Would Break

Practice

These problems are required on this path at this level; the path adds one problem from each of the level's topics, picked for you and kept.

Checklist

You can now:

  • Run first-price and second-price sealed-bid auctions with a reserve and a stated tie rule, and explain why bidding your value is safe under the second price and in a rising-price auction.
  • Simulate auctions with one seeded generator in a stated draw order, and state revenue equivalence as a result of the uniform-value model.
  • Turn preference lists into rank tables, test for blocking pairs, and run deferred acceptance with several seats per receiver.
  • Find the market and efficient quantities with an externality, measure the deadweight loss as an area (a trapezoid at a corner), and choose the Pigouvian tax, corners included.
  • Find voluntary and efficient levels of a public good, and solve a piecewise-linear equation by walking its kinks or by bisection.
  • Find a permit price from capped abatement choices, and explain why equal marginal costs make a cut cheapest.
  • Search bundle bids with bitmasks and backtracking, and charge VCG payments, W_{-t} - (W - v_t).
  • Use exact growth factors for real rates and the quantity equation, evaluate a Taylor rule and test the Taylor principle, and read the Phillips curve forwards and backwards.
  • Simulate adaptive and anchored expectations, a disinflation's sacrifice ratio, and the three-equation model with its timing, then score rules in percentage points and search a grid with a strict tie rule.
  • Apply a zero lower bound as saturation, recognise the deflation trap, minimise a quadratic loss by setting its slope to zero, and find a fixed point by formula or by iteration.