Andrew Fribush

← All posts

Why the grain price drifted

· October 22, 2026 · 12 min read

The grain price in my economic simulation is never solved. Each province holds a stock of grain. Every year its farms add a harvest, its people eat a year's need, and 12% of whatever is left spoils. Price is read off cover, the stock divided by a year's need, through a clamped power, so a full granary reads cheap, an empty one reads dear, and nothing in the arithmetic can reach infinity. I built it that way because a world with thousands of markets can't be solved for clearing every tick. Then I measured a twenty-year run: capacity over need drifted from 1.13 to 1.02 and cover fell from 0.90 to 0.41, and both ends priced inside the clamp, which runs from 3 to 140 against a reference price of 10.

The clamp only limits how far the price can go. The rule has no term that pulls it back toward 10. I worked out where the rule settles. Equilibrium cover is carry * (P/C - 1) / (1 - carry), and at grain's carry of 0.88 that multiplies any drift in production over consumption by 7.3. Price then moves as cover to the minus 1.9.

The fix is a supply response, output = capacity * climate * clamp((price / ref) ^ elasticity, 0.55, 1.6), with the elasticity at 0.35 for grain. With it in, over eight seeds, median cover is 1.01 of target and the median mean grain price is 10.7 against the reference of 10. Production pulls the worst year of an average run back from cover 0.62 and a mean grain price of 25.9.

The system is the economy of a strategy game I'm building. It has provinces, a stock of each good in each province, prices read off those stocks, and capitalists who carry goods wherever the spread beats the freight. I'm making no claim about any real economy. The numbers are all grain's. They come from twenty-year runs of the full game, the held equilibrium over eight seeds, and deterministic runs of the rule that I drew for the figures.

I found the drift by instrumenting the arbitrage, while working out why a railway wasn't converging prices. Ten of the 15 province pairs already had a zero gap, so distribution wasn't the problem, and I measured the aggregate instead. Capacity over need is what the country's land could grow divided by what the country eats. It went from 1.13 to 1.02 over the twenty years.

Figure 2.8.1 runs the rule itself over that drift, with the harvest noise and the trade left out. By year twenty the deterministic run reaches cover 0.46 and a price of 43.7. The measured 0.41 came from the full game.

fig. 2.8.1 · twenty years of the grain rule, capacity over need drifting 1.13 to 1.02, with and without supply response.
SUPPLY RESPONSE HOLDS GRAIN AT 12.8, NOT 43.7WITH THE SUPPLY RESPONSE, ELASTICITY 0.35BOUND ALONE, NO RESPONSEONE LINE IS ONE 20-YEAR RUN OF THE GRAIN RULE WITH CAPACITY OVER NEED WALKING1.13 TO 1.02; THE LIT LINE IS THE RUN NAMED ABOVE.COVER, IN YEARS OF NEED · TARGET 1.000.51.005101520YEAR OF THE RUNRESPONSE 0.88BOUND ALONE 0.46GRAIN PRICE · REFERENCE 10 · THE PANEL IS THE CLAMP, 3 TO 14031030100140CEILING, 14 TIMES REFERENCEFLOOR, 0.3 TIMES REFERENCE05101520YEAR OF THE RUNRESPONSE 12.8BOUND ALONE 43.7

source: vicky @ 93c9f5e · game/sim.py lines 41-71 (constants and the derived surplus), 259-270 (price rule), 1157-1158 (cold years), 1200-1216 (production, supply response, noise, consumption, then carry); docs/economy.md lines 97-102 (the 7.3 and the measured drift) · deterministic run of the source rule with fixed need · granary.data.ts

Where the 7.3 Comes From

The code does the yearly step for one province in three moves. It adds the harvest P, subtracts the year's consumption C, and multiplies what's left by carry, which is 0.88 for grain, so next year's stock is carry * (stock + P - C). At equilibrium the stock comes back to itself, so stock = carry * (stock + P - C) (it never quite does while P/C is drifting, because the stock trails that line by years, which comes up below). Collect the stock terms: stock * (1 - carry) = carry * (P - C). The rest is dividing, by 1 - carry to get stock = carry * (P - C) / (1 - carry) and then by C, and that's cover, cover = carry * (P/C - 1) / (1 - carry). At carry 0.88 the factor in front is 0.88 / 0.12, 7.3.

The order of the moves changes the number. If the carry is applied before the flows, the step is carry * stock + P - C and the equilibrium is (P - C) / (1 - carry). The factor is then 1 / 0.12, or 8.3. The source applies the carry after the harvest lands, so the harvest spoils along with the stock, and that gives 7.3. (The surplus property on the good states this equilibrium, stock = carry * (P - C) / (1 - carry), in its docstring, and that is where I checked it.)

The measured drift was 0.11 of P/C, and 0.11 times 7.3 moves the equilibrium cover by 0.81 (the rule settles at cover 0.95 at a ratio of 1.13 and at 0.15 at 1.02).

Price is read off cover as 10 * (1 / cover) ^ 1.9, between a floor of 3 and a ceiling of 140. At the measured cover of 0.90 that reads 12.2, and once cover has fallen to 0.41 the same formula gives 54.4, which is 4.5 times as much. Figure 2.8.2 draws both halves of the chain, the slope of 7.3 on the left and the curve of the minus 1.9 on the right.

fig. 2.8.2 · where cover settles for each production-over-need ratio, and the price the grain rule reads off each cover.
COVER MOVES 7.3 TIMES THE DRIFT; PRICE, COVER TO THE -1.9LEFT: WHERE COVER SETTLES AT CARRY 0.88. RIGHT: THE PRICE READ OFF EACH COVER.DOTS: THE MEASURED DRIFT'S TWO ENDS, AT EQUILIBRIUM ON THE LEFT, PRICED BY THERULE ON THE RIGHT.WHERE COVER SETTLES, YEARS OF NEED · TARGET 1.000.51.01.51.001.051.101.151.20PRODUCTION OVER NEEDSLOPE 7.3 AT CARRY 0.880.81 OF COVER, 7.3 TIMESTHE DRIFT1.13 SETTLES AT 0.951.02 SETTLES AT 0.15DRIFT 0.11GRAIN PRICE = 10 × COVER^-1.9 · CLAMPED 3 TO 140310301001400.20.40.60.81.01.2COVER, IN YEARS OF NEEDCEILING, 14 TIMES REFERENCEFLOOR, 0.3 TIMES REFERENCE0.90 · READS 12.20.41 · READS 54.44.5 TIMES THE PRICE

source: vicky @ 93c9f5e · game/sim.py lines 63-67 (equilibrium), 71 (grain constants), 259-270 (price rule); docs/economy.md lines 97-102 (7.3, drift 1.13 to 1.02, cover 0.90 to 0.41); docs/BUILD-LOG.md lines 965, 968 and 970 (standing decisions) · coverMultiplier.data.ts

The twenty-year run never reaches the 0.15 that a ratio of 1.02 settles at, because the stock takes years to drain. Hold P and C fixed, subtract the equilibrium from both sides of the step, and the gap between the stock and its equilibrium shrinks by 12% a year. That rate is 1 - carry, the spoilage itself. While the ratio walks down the equilibrium moves too, and each year's drop in it is added to the gap, so cover trails the equilibrium line. After twenty years the deterministic run sits at 0.46 and the equilibrium is already at 0.15. The full game's 0.41 comes with a harvest noise of plus or minus 8% and the inter-province trade on top.

The Bound Has No Restoring Force

The inventory rule has a bound. Price is clamped at 14 times the reference, and an empty granary reads that ceiling instead of dividing by zero. The game's design was written against exactly that failure, one good's price going to infinity.

Nothing in the rule pushes the state back once it has moved. Grain output was capacity * climate times the harvest noise, with no price in it. The only feedback grain had ran through the population. A dear price lowered welfare, welfare set the rate at which the population grew, and grain need is population times 1.0, so hunger reached need only through the growth rate. Land came into cultivation at that same rate, so in each province that feedback scaled capacity and need together, and nothing in it aimed their ratio at the surplus that holds target cover.

I audited every hardcoded threshold in the game. Some market was on its price floor in 100% of years, coal was on its floor in 26% of province-years, and iron and cloth were on their ceilings in 15%. In a dearth the grain ceiling flattened every province to the same number, and a dearth is when the player most needs to read the spread between provinces, so a clamp hit that often was erasing the signal rather than catching nonsense. Every clamp now has a check on how often it's hit, and the worst rate after retuning was 2.6%.

A Supply Response Holds Cover at 1.01 of Target

The missing piece is production responding to price. In the source, each province's grain output for the year is capacity * climate, multiplied by clamp((price / ref) ^ 0.35, 0.55, 1.6). At a price of 20, twice reference, the factor is 2 to the 0.35, which is 1.27. A price of 5 gives 0.78.

Land is slow to bring under the plough, so no price raises output by more than 1.6x. At the low end farmers still plant something, and the price factor never takes output below 0.55 of what capacity and climate allow. Manufactures get 0.55 for the exponent, because a mill can add a shift faster than a field can be cleared.

This is the rule reduced to one province with a fixed need of 100, which is the engine figures 2.8.1 and 2.8.3 run in the browser. It is the source's yearly step in the source's order, with the harvest noise, the trade and the climate set aside. The ratio it walks is capacity over need; with the response on, the harvest is that times the price factor.

Python
CARRY, REF, K, TARGET = 0.88, 10.0, 1.9, 1.0   # the grain row of GOODSdef price_of(cover):                   # sim.py price_of, reduced    ratio = 14.0 if cover <= 1e-6 else (TARGET / cover) ** K    return REF * max(0.30, min(14.0, ratio))def run(ratio_from, ratio_to, elasticity, years=20, need=100):    stock = TARGET * need    for y in range(years + 1):        ratio = ratio_from + (ratio_to - ratio_from) * y / years        cover = stock / need        price = price_of(cover)        if y % 5 == 0:            print(f"year {y:2d}  cap/need {ratio:.3f}  "                  f"cover {cover:.2f}  price {price:5.1f}")        out = ratio * need                     # capacity, climate 1.0        out *= min(1.6, max(0.55, (price / REF) ** elasticity))  # supply        stock += out                           # harvest in        stock = max(0.0, stock - need)         # a year eaten        stock *= CARRY                         # then spoilagerun(1.13, 1.02, elasticity=0.0)     # the bound alonerun(1.13, 1.02, elasticity=0.35)    # with the supply response

Run over the measured drift, the bound alone first and then the response:

text
year  0  cap/need 1.130  cover 1.00  price  10.0year  5  cap/need 1.103  cover 0.94  price  11.4year 10  cap/need 1.075  cover 0.81  price  15.1year 15  cap/need 1.047  cover 0.64  price  23.2year 20  cap/need 1.020  cover 0.46  price  43.7year  0  cap/need 1.130  cover 1.00  price  10.0year  5  cap/need 1.103  cover 0.97  price  10.6year 10  cap/need 1.075  cover 0.94  price  11.3year 15  cap/need 1.047  cover 0.91  price  12.0year 20  cap/need 1.020  cover 0.88  price  12.8

Against a ratio of 1.02 the response holds cover at 0.88 in year twenty, short of 1.0. Holding target cover needs output at 1.136 times need. A price of 12.8 calls forth 1.28 to the 0.35, which is 1.09, and 1.02 times 1.09 is 1.11, so year twenty's output falls short of the 1.136.

I re-measured the full game on 2026-09-09 over seeds 1 to 8 with a fixed budget mix. For each year I averaged cover and grain price across the live provinces, then took the median of each over every seed-year: cover 1.01 of target, price 10.7 against a reference of 10. In the worst year of an average run cover drops to 0.62 and the mean grain price reaches 25.9, two and a half times reference. Production responds and pulls it back. (The earlier measurement, which this one superseded, was taken before the elasticities were derived and read cover 0.94 to 0.98, price 13 to 15, and a trough of cover 0.61 and price 34.8 that was back to 0.84 four years on.)

The game's calendar lists seven cold years, and three of them, 1657, 1658 and 1665, fall inside a twenty-year run from 1650. The harvest in a cold year is drawn from 0.62 to 0.80 of normal. Figure 2.8.3 runs the sheet through them with the harvest at the midpoint, 0.71, and capacity over need held at the derived surplus, 1.136. Two short harvests in a row take cover to 0.71 and then 0.64 and the price to 19.2 and then 23.6. At 23.6 the response multiplies the next harvest by 1.35 (2.36 to the 0.35), the granary refills to cover 1.03 the following year, and that reads 9.4.

The bound alone takes the same two harvests to a price of 44.7. Cover has only recovered to 0.75 when the third cold year comes, and the price goes back up to 39.2.

fig. 2.8.3 · the grain rule through the source’s cold years, with and without the supply response.
SUPPLY RESPONSE: 23.6 BACK TO 9.4 IN A YEARWITH THE SUPPLY RESPONSE, ELASTICITY 0.35BOUND ALONE, NO RESPONSEONE LINE IS ONE 20-YEAR RUN OF THE GRAIN RULE, THE HARVEST AT 0.71 IN THESHADED YEARS; THE LIT LINE IS THE RUN NAMED ABOVE.COVER, IN YEARS OF NEED · TARGET 1.0COLD YEAR, HARVEST 0.7100.51.005101520YEAR OF THE RUNRESPONSE 1.00BOUND ALONE 0.69GRAIN PRICE · REFERENCE 10 · THE PANEL IS THE CLAMP, 3 TO 14031030100140CEILING, 14 TIMES REFERENCEFLOOR, 0.3 TIMES REFERENCE05101520YEAR OF THE RUNRESPONSE 10.0BOUND ALONE 20.1

source: vicky @ 93c9f5e · game/sim.py lines 41-71 (constants and the derived surplus), 259-270 (price rule), 1157-1158 (cold years), 1200-1216 (production, supply response, noise, consumption, then carry); docs/economy.md lines 97-102 (the 7.3 and the measured drift) · deterministic run of the source rule with fixed need · granary.data.ts

Carry, target cover and the surplus are related by the equilibrium equation, so the dependent one gets solved for. The surplus property on each good computes the production-over-need ratio that holds exactly target cover, 1 + target_cover * (1 - carry) / carry, which at carry 0.88 and a target of one year is 1.136. The build log carries this as two standing decisions: derive equilibria rather than tune them, and solve coupled constants. The entry for the first names the price-floor bug, which appeared twice because its first fix went into one good's constants instead of into the rule.

Any Model That Prices Off a Depreciating Stock Has This Multiplier

The objection I'd take most seriously is that real economies don't clear either, and that this is a game. Both are true. The claim is about any model that reads a price off a stock losing a fixed share a year, and agent-based economic models that price off depreciating inventories are in that set. The equilibrium sits at carry / (1 - carry) times the net flow when the spoilage comes after the flows, and at 1 / (1 - carry) times it when the spoilage comes first. A persistent drift in the flow walks the stock toward that equilibrium, closing 1 - carry of the gap a year once the flow stops moving, whether or not the price is clamped. The build log has a standing decision to check that multiplier whenever a new stock is added.

Limits

Everything here is grain's: carry 0.88, price exponent 1.9, supply exponent 0.35. The other goods have their own carries and elasticities.

The 25.9 is a mean over the live provinces in the worst year of an average run, measured on the full game with trade, noise and random climate, and the economy doc records its recovery. The 23.6 in figure 2.8.3 is the peak of the deterministic sheet, which is a different quantity from a different run.

The 1.01 and the 10.7 are asserted as medians in the repo's check file, with tolerances of 0.06 and 0.8, and the economy doc's 2026-09-09 paragraph records them. The 0.62 and the 25.9 are in that paragraph and in the commit that measured them, nothing in the repo asserts them, and the number of runs averaged for that worst year is not recorded.

If a model of yours reads a price off a stock, compute carry / (1 - carry) for that stock before tuning anything around it, and then look for a term that makes the flow answer the price. At a carry of 0.88 the multiplier is 7.3, and at 0.95 it would be 19, so a drift of 0.05 in the flow ratio would move equilibrium cover by 0.95 of a year's need, with the stock closing 5% of the gap to it each year once the flow settles.

Andrew Fribush, software and computer engineer in Austin. I build consequential systems where software meets institutions: finance, healthcare, commerce, government data, and the mechanisms that govern AI agents. Taking new engagements: hello@andrewfribush.com.

I write when a system teaches me something. Get new posts by email: no tracking, one-click unsubscribe.