Skip to content
This repository was archived by the owner on Mar 12, 2026. It is now read-only.

Commit 9d8b44b

Browse files
committed
Fix open economy: real intermediate imports, balanced trade, importPush cap
Three bugs in OpenEconomy: 1. Imported intermediates scaled with priceLevel² (sectorOutputs already includes PL, then multiplied by PL again) — removed double-counting 2. Imported intermediates used nominal sector output — now divides by PL to get real output, since foreign goods are priced in EUR not PLN 3. ER import effect was inverted: (baseER/ER)^ε reduced PLN bill when PLN weakened — corrected to (ER/baseER)^(1-ε) net effect Two calibration fixes: - OeExportBase = 475M (balances initial trade with intermediate imports; legacy ExportBase=190M was calibrated without I-O imports) - OeImportPushCap = 3%/month (prevents runaway ER→inflation→ER spiral) Export competitiveness now uses real exchange rate (PL/ER) instead of separate PL and ER terms that double-counted. Result: ER stable at 4.33→2.66 (appreciation from deflation-driven export competitiveness). PLN advantage over EUR preserved (61% vs 78% unemployment at BDP=2000).
1 parent d009150 commit 9d8b44b

3 files changed

Lines changed: 34 additions & 11 deletions

File tree

src/main/scala/sfc/config/SimConfig.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,16 @@ object Config:
217217
val OeErFloor: Double = sys.env.get("OE_ER_FLOOR").map(_.trim.toDouble).getOrElse(2.5)
218218
val OeErCeiling: Double = sys.env.get("OE_ER_CEILING").map(_.trim.toDouble).getOrElse(10.0)
219219

220+
// Export base for open economy. Default 475M balances initial trade when
221+
// intermediate imports (~200M) are added to the existing consumption imports (~275M).
222+
// The legacy ExportBase (190M) was calibrated without intermediate imports.
223+
val OeExportBase: Double = sys.env.get("OE_EXPORT_BASE").map(_.trim.toDouble)
224+
.getOrElse(475000000.0) * ScaleFactor
225+
226+
// Import-push inflation cap (monthly). Prevents runaway ER→inflation→ER spiral.
227+
// At 3% cap, even a 100% ER deviation contributes at most 3% monthly inflation.
228+
val OeImportPushCap: Double = 0.03
229+
220230
// Hardcoded calibration (NBP/GUS/WIOD)
221231
val OeForeignGdpGrowth = 0.015 // EZ annual real GDP growth
222232
val OeExportPriceElasticity = 0.8 // Marshall-Lerner

src/main/scala/sfc/engine/OpenEconomy.scala

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,32 @@ object OpenEconomy:
2121
val nSectors = 6
2222

2323
// A. Export demand (structural)
24+
// Foreign buyers care about the real exchange rate: PL / (ER/baseER).
25+
// When PL rises but ER depreciates proportionally, real price unchanged.
26+
// This replaces the separate priceCompetitiveness × erCompetitiveness terms
27+
// which double-counted and let PL crush exports even when ER compensated.
2428
val foreignGdpFactor = Math.pow(1.0 + Config.OeForeignGdpGrowth / 12.0, month.toDouble)
25-
val erCompetitiveness = if rc.isEurozone then 1.0
26-
else Math.pow(prevForex.exchangeRate / Config.BaseExRate, Config.OeExportPriceElasticity)
2729
val ulcEffect = 1.0 + autoRatio * Config.OeUlcExportBoost
28-
val priceCompetitiveness = if priceLevel > 0 then
29-
Math.pow(1.0 / priceLevel, Config.OeExportPriceElasticity)
30-
else 1.0
31-
val exports = Config.ExportBase * foreignGdpFactor * priceCompetitiveness *
32-
erCompetitiveness * ulcEffect
30+
val realExRate = if rc.isEurozone then 1.0
31+
else
32+
val nominalER = prevForex.exchangeRate / Config.BaseExRate // >1 when PLN weaker
33+
val realPrice = if priceLevel > 0 && nominalER > 0 then priceLevel / nominalER
34+
else 1.0
35+
Math.pow(1.0 / Math.max(0.1, realPrice), Config.OeExportPriceElasticity)
36+
val exports = Config.OeExportBase * foreignGdpFactor * realExRate * ulcEffect
3337

3438
// B. Imported intermediates (cross-border I-O)
35-
val erImportEffect = if rc.isEurozone then 1.0
36-
else Math.pow(Config.BaseExRate / prevForex.exchangeRate, Config.OeErElasticity)
39+
// Intermediate imports = real domestic output × import share × ER net effect.
40+
// sectorOutputs is nominal (includes priceLevel), so divide by PL to get real output.
41+
// PLN import bill: quantity × foreign price × ER. With demand elasticity ε:
42+
// bill ∝ realOutput × (ER/baseER)^(1-ε). For ε<1 bill rises with depreciation.
43+
val erNetEffect = if rc.isEurozone then 1.0
44+
else Math.pow(prevForex.exchangeRate / Config.BaseExRate,
45+
1.0 - Config.OeErElasticity)
3746
val importedInterm = (0 until nSectors).map { s =>
38-
sectorOutputs(s) * Config.OeImportContent(s) * erImportEffect * priceLevel
47+
val realOutput = if priceLevel > 0 then sectorOutputs(s) / priceLevel
48+
else sectorOutputs(s)
49+
realOutput * Config.OeImportContent(s) * erNetEffect
3950
}.toVector
4051
val totalImportedInterm = importedInterm.sum
4152

src/main/scala/sfc/engine/Simulation.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,10 @@ object Sectors:
2828
val demandPull = (demandMult - 1.0) * 0.15
2929
val costPush = wageGrowth * 0.25
3030
// EUR: no exchange rate pass-through (single currency area)
31-
val importPush = if rc.isEurozone then 0.0
31+
val rawImportPush = if rc.isEurozone then 0.0
3232
else Math.max(0.0, exRateDeviation) * Config.ImportPropensity * 0.25
33+
val importPush = if Config.OeEnabled then Math.min(rawImportPush, Config.OeImportPushCap)
34+
else rawImportPush
3335
val techDeflation = autoRatio * 0.060 + hybridRatio * 0.018
3436
// Soft floor: beyond -1.5%/month, deflation passes through at 30% rate
3537
// (models downward price stickiness -- Bewley 1999, Schmitt-Grohe & Uribe 2016)

0 commit comments

Comments
 (0)