Skip to content

Commit 6805b85

Browse files
committed
Stabilize trade v4 budget migration
1 parent 9a8ca5d commit 6805b85

6 files changed

Lines changed: 321 additions & 61 deletions

File tree

site/admin/index.html

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@
1818
})();
1919
</script>
2020
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%2314171a'/%3E%3Cpath d='M8 8h32L8 40z' fill='%23f0c33b'/%3E%3Cpath d='M56 56H24l32-32z' fill='%230d65d9'/%3E%3C/svg%3E">
21-
<link rel="stylesheet" href="../css/base.css?v=20260604-min-data-seed">
22-
<link rel="stylesheet" href="../css/trade.css?v=20260604-min-data-seed">
23-
<link rel="stylesheet" href="../css/ledger.css?v=20260604-min-data-seed">
24-
<link rel="stylesheet" href="../css/simulation.css?v=20260604-min-data-seed">
25-
<link rel="stylesheet" href="../css/records.css?v=20260604-min-data-seed">
26-
<link rel="stylesheet" href="../css/responsive.css?v=20260604-min-data-seed">
21+
<link rel="stylesheet" href="../css/base.css?v=20260604-trade-v4-soften">
22+
<link rel="stylesheet" href="../css/trade.css?v=20260604-trade-v4-soften">
23+
<link rel="stylesheet" href="../css/ledger.css?v=20260604-trade-v4-soften">
24+
<link rel="stylesheet" href="../css/simulation.css?v=20260604-trade-v4-soften">
25+
<link rel="stylesheet" href="../css/records.css?v=20260604-trade-v4-soften">
26+
<link rel="stylesheet" href="../css/responsive.css?v=20260604-trade-v4-soften">
2727
</head>
2828
<body data-app-mode="admin">
2929
<header class="topbar">
@@ -63,24 +63,24 @@ <h1>Global Ledger</h1>
6363
<span id="sourceNote"></span>
6464
</footer>
6565

66-
<script src="../data.js?v=20260604-min-data-seed"></script>
67-
<script src="../js/engine/fiscal.js?v=20260604-min-data-seed"></script>
68-
<script src="../js/engine/tradePolicy.js?v=20260604-min-data-seed"></script>
69-
<script src="../js/engine/trade.js?v=20260604-min-data-seed"></script>
70-
<script src="../engine.js?v=20260604-min-data-seed"></script>
71-
<script src="../js/app/config.js?v=20260604-min-data-seed"></script>
72-
<script src="../js/app/format.js?v=20260604-min-data-seed"></script>
73-
<script src="../js/app/sync.js?v=20260604-min-data-seed"></script>
74-
<script src="../js/app/statusTables.js?v=20260604-min-data-seed"></script>
75-
<script src="../js/app/recordsParser.js?v=20260604-min-data-seed"></script>
76-
<script src="../js/app/records.js?v=20260604-min-data-seed"></script>
77-
<script src="../js/app/tradeMapShapes.js?v=20260604-min-data-seed"></script>
78-
<script src="../js/app/tradeZones.js?v=20260604-min-data-seed"></script>
79-
<script src="../js/app/tradeRouteMesh.js?v=20260604-min-data-seed"></script>
80-
<script src="../js/app/tradeLaneSkeleton.js?v=20260604-min-data-seed"></script>
81-
<script src="../js/app/tradeMap.js?v=20260604-min-data-seed"></script>
82-
<script src="../js/app/tradeView.js?v=20260604-min-data-seed"></script>
83-
<script src="../js/app/editorView.js?v=20260604-min-data-seed"></script>
84-
<script src="../app.js?v=20260604-min-data-seed"></script>
66+
<script src="../data.js?v=20260604-trade-v4-soften"></script>
67+
<script src="../js/engine/fiscal.js?v=20260604-trade-v4-soften"></script>
68+
<script src="../js/engine/tradePolicy.js?v=20260604-trade-v4-soften"></script>
69+
<script src="../js/engine/trade.js?v=20260604-trade-v4-soften"></script>
70+
<script src="../engine.js?v=20260604-trade-v4-soften"></script>
71+
<script src="../js/app/config.js?v=20260604-trade-v4-soften"></script>
72+
<script src="../js/app/format.js?v=20260604-trade-v4-soften"></script>
73+
<script src="../js/app/sync.js?v=20260604-trade-v4-soften"></script>
74+
<script src="../js/app/statusTables.js?v=20260604-trade-v4-soften"></script>
75+
<script src="../js/app/recordsParser.js?v=20260604-trade-v4-soften"></script>
76+
<script src="../js/app/records.js?v=20260604-trade-v4-soften"></script>
77+
<script src="../js/app/tradeMapShapes.js?v=20260604-trade-v4-soften"></script>
78+
<script src="../js/app/tradeZones.js?v=20260604-trade-v4-soften"></script>
79+
<script src="../js/app/tradeRouteMesh.js?v=20260604-trade-v4-soften"></script>
80+
<script src="../js/app/tradeLaneSkeleton.js?v=20260604-trade-v4-soften"></script>
81+
<script src="../js/app/tradeMap.js?v=20260604-trade-v4-soften"></script>
82+
<script src="../js/app/tradeView.js?v=20260604-trade-v4-soften"></script>
83+
<script src="../js/app/editorView.js?v=20260604-trade-v4-soften"></script>
84+
<script src="../app.js?v=20260604-trade-v4-soften"></script>
8585
</body>
8686
</html>

site/engine.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
(function () {
22
const STORAGE_KEY = "aggs-operations-state-v4";
3+
const TRADE_V4_FORMULA_VERSION = "trade2028";
34

45
const HEALTH_GROWTH = { Depression: -3, Recession: -2, Slowdown: -1, Recovery: 1, Expansion: 2, Prosperity: 3 };
56
const HEALTH_DEMOGRAPHICS = {
@@ -149,14 +150,33 @@
149150
return Number(number(value, 0).toFixed(2));
150151
}
151152

153+
function budgetBalanceMigrationTarget(national = {}) {
154+
const storedBalance = number(national.budgetBalance, null);
155+
if (Number.isFinite(storedBalance)) return roundCurrency(storedBalance);
156+
return roundCurrency(number(national.budgetCapacity, 0) - number(national.budgetExpenditure, 0));
157+
}
158+
159+
function captureTradeV4BudgetBalanceTargets(data, previousTradeFormulaVersion) {
160+
if (previousTradeFormulaVersion === TRADE_V4_FORMULA_VERSION || data.meta.tradeV4BudgetBalanceTargets) return;
161+
const nationalRows = data.national && typeof data.national === "object" && !Array.isArray(data.national)
162+
? data.national
163+
: {};
164+
const targets = Object.fromEntries(
165+
Object.entries(nationalRows)
166+
.map(([id, national]) => [id, budgetBalanceMigrationTarget(national)])
167+
.filter(([, target]) => Number.isFinite(target))
168+
);
169+
if (Object.keys(targets).length) data.meta.tradeV4BudgetBalanceTargets = targets;
170+
}
171+
152172
function ensureState(data = {}) {
153173
data.meta = data.meta || {};
174+
const previousTradeFormulaVersion = data.meta.tradeFormulaVersion;
154175
data.meta.title = data.meta.title || "AG-GS Global Ledger";
155176
data.meta.currentYear = number(data.meta.currentYear, 2021);
156177
data.meta.worldEconomicHealth = data.meta.worldEconomicHealth || "Expansion";
157178
data.meta.budgetFormulaVersion = data.meta.budgetFormulaVersion || "legacy";
158179
data.meta.tariffFormulaVersion = TARIFF_FORMULAS[data.meta.tariffFormulaVersion] ? data.meta.tariffFormulaVersion : "legacy";
159-
data.meta.tradeFormulaVersion = "trade2028";
160180
data.meta.populationFormulaVersion = POPULATION_FORMULAS[data.meta.populationFormulaVersion] ? data.meta.populationFormulaVersion : "population2026";
161181
delete data.meta.tradeV3Enabled;
162182
data.meta.archivedNationIds = Array.isArray(data.meta.archivedNationIds) ? data.meta.archivedNationIds : [];
@@ -170,6 +190,8 @@
170190
["populationColumns", "equipmentCosts", "eraMultipliers", "costAdditionModifiers", "costReductionModifiers"].forEach((key) => {
171191
data[key] = Array.isArray(data[key]) ? data[key] : [];
172192
});
193+
captureTradeV4BudgetBalanceTargets(data, previousTradeFormulaVersion);
194+
data.meta.tradeFormulaVersion = TRADE_V4_FORMULA_VERSION;
173195
data.tradeNetwork = data.tradeNetwork && typeof data.tradeNetwork === "object" && !Array.isArray(data.tradeNetwork) ? data.tradeNetwork : {};
174196
data.tradeNetwork.targetedTariffs = data.tradeNetwork.targetedTariffs && typeof data.tradeNetwork.targetedTariffs === "object" && !Array.isArray(data.tradeNetwork.targetedTariffs)
175197
? data.tradeNetwork.targetedTariffs
@@ -230,6 +252,26 @@
230252
return true;
231253
}
232254

255+
function hasTradeV4BudgetBalanceTargets(data) {
256+
const targets = data.meta?.tradeV4BudgetBalanceTargets;
257+
return Boolean(targets && typeof targets === "object" && !Array.isArray(targets) && Object.keys(targets).length);
258+
}
259+
260+
function budgetMigrationSignature(data) {
261+
return Object.keys(data.national || {})
262+
.sort()
263+
.map((id) => {
264+
const national = data.national[id] || {};
265+
return [
266+
id,
267+
roundCurrency(national.budgetCapacity),
268+
roundCurrency(national.budgetExpenditure),
269+
roundCurrency(national.budgetBalance)
270+
].join(":");
271+
})
272+
.join("|");
273+
}
274+
233275
function load(baseData) {
234276
const saved = localStorage.getItem(STORAGE_KEY);
235277
if (saved) {
@@ -729,6 +771,19 @@
729771

730772
function recalculateAll(data, options = {}) {
731773
ensureTradeV4State(data);
774+
if (hasTradeV4BudgetBalanceTargets(data)) {
775+
const migrationOptions = { ...options, keepTradeV4BudgetBalanceTargets: true };
776+
let previousSignature = "";
777+
for (let attempt = 0; attempt < 6; attempt++) {
778+
recalculateTrade(data, options);
779+
recalculateBudgets(data, migrationOptions);
780+
const nextSignature = budgetMigrationSignature(data);
781+
if (attempt > 0 && nextSignature === previousSignature) break;
782+
previousSignature = nextSignature;
783+
}
784+
delete data.meta.tradeV4BudgetBalanceTargets;
785+
return data;
786+
}
732787
recalculateTrade(data, options);
733788
recalculateBudgets(data, options);
734789
recalculateTrade(data, options);
@@ -767,7 +822,7 @@
767822
function ensureTradeV4State(data) {
768823
ensureState(data);
769824
ensureTradeNetworkState(data);
770-
data.meta.tradeFormulaVersion = "trade2028";
825+
data.meta.tradeFormulaVersion = TRADE_V4_FORMULA_VERSION;
771826
delete data.meta.tradeV3Enabled;
772827
return data;
773828
}

site/index.html

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
})();
2929
</script>
3030
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='10' fill='%2314171a'/%3E%3Cpath d='M8 8h32L8 40z' fill='%23f0c33b'/%3E%3Cpath d='M56 56H24l32-32z' fill='%230d65d9'/%3E%3C/svg%3E">
31-
<link rel="stylesheet" href="css/base.css?v=20260604-min-data-seed">
32-
<link rel="stylesheet" href="css/trade.css?v=20260604-min-data-seed">
33-
<link rel="stylesheet" href="css/ledger.css?v=20260604-min-data-seed">
34-
<link rel="stylesheet" href="css/simulation.css?v=20260604-min-data-seed">
35-
<link rel="stylesheet" href="css/records.css?v=20260604-min-data-seed">
36-
<link rel="stylesheet" href="css/responsive.css?v=20260604-min-data-seed">
31+
<link rel="stylesheet" href="css/base.css?v=20260604-trade-v4-soften">
32+
<link rel="stylesheet" href="css/trade.css?v=20260604-trade-v4-soften">
33+
<link rel="stylesheet" href="css/ledger.css?v=20260604-trade-v4-soften">
34+
<link rel="stylesheet" href="css/simulation.css?v=20260604-trade-v4-soften">
35+
<link rel="stylesheet" href="css/records.css?v=20260604-trade-v4-soften">
36+
<link rel="stylesheet" href="css/responsive.css?v=20260604-trade-v4-soften">
3737
</head>
3838
<body data-app-mode="public">
3939
<header class="topbar">
@@ -75,24 +75,24 @@ <h1>Global Ledger</h1>
7575
</span>
7676
</footer>
7777

78-
<script src="data.js?v=20260604-min-data-seed"></script>
79-
<script src="js/engine/fiscal.js?v=20260604-min-data-seed"></script>
80-
<script src="js/engine/tradePolicy.js?v=20260604-min-data-seed"></script>
81-
<script src="js/engine/trade.js?v=20260604-min-data-seed"></script>
82-
<script src="engine.js?v=20260604-min-data-seed"></script>
83-
<script src="js/app/config.js?v=20260604-min-data-seed"></script>
84-
<script src="js/app/format.js?v=20260604-min-data-seed"></script>
85-
<script src="js/app/sync.js?v=20260604-min-data-seed"></script>
86-
<script src="js/app/statusTables.js?v=20260604-min-data-seed"></script>
87-
<script src="js/app/recordsParser.js?v=20260604-min-data-seed"></script>
88-
<script src="js/app/records.js?v=20260604-min-data-seed"></script>
89-
<script src="js/app/tradeMapShapes.js?v=20260604-min-data-seed"></script>
90-
<script src="js/app/tradeZones.js?v=20260604-min-data-seed"></script>
91-
<script src="js/app/tradeRouteMesh.js?v=20260604-min-data-seed"></script>
92-
<script src="js/app/tradeLaneSkeleton.js?v=20260604-min-data-seed"></script>
93-
<script src="js/app/tradeMap.js?v=20260604-min-data-seed"></script>
94-
<script src="js/app/tradeView.js?v=20260604-min-data-seed"></script>
95-
<script src="js/app/editorView.js?v=20260604-min-data-seed"></script>
96-
<script src="app.js?v=20260604-min-data-seed"></script>
78+
<script src="data.js?v=20260604-trade-v4-soften"></script>
79+
<script src="js/engine/fiscal.js?v=20260604-trade-v4-soften"></script>
80+
<script src="js/engine/tradePolicy.js?v=20260604-trade-v4-soften"></script>
81+
<script src="js/engine/trade.js?v=20260604-trade-v4-soften"></script>
82+
<script src="engine.js?v=20260604-trade-v4-soften"></script>
83+
<script src="js/app/config.js?v=20260604-trade-v4-soften"></script>
84+
<script src="js/app/format.js?v=20260604-trade-v4-soften"></script>
85+
<script src="js/app/sync.js?v=20260604-trade-v4-soften"></script>
86+
<script src="js/app/statusTables.js?v=20260604-trade-v4-soften"></script>
87+
<script src="js/app/recordsParser.js?v=20260604-trade-v4-soften"></script>
88+
<script src="js/app/records.js?v=20260604-trade-v4-soften"></script>
89+
<script src="js/app/tradeMapShapes.js?v=20260604-trade-v4-soften"></script>
90+
<script src="js/app/tradeZones.js?v=20260604-trade-v4-soften"></script>
91+
<script src="js/app/tradeRouteMesh.js?v=20260604-trade-v4-soften"></script>
92+
<script src="js/app/tradeLaneSkeleton.js?v=20260604-trade-v4-soften"></script>
93+
<script src="js/app/tradeMap.js?v=20260604-trade-v4-soften"></script>
94+
<script src="js/app/tradeView.js?v=20260604-trade-v4-soften"></script>
95+
<script src="js/app/editorView.js?v=20260604-trade-v4-soften"></script>
96+
<script src="app.js?v=20260604-trade-v4-soften"></script>
9797
</body>
9898
</html>

site/js/engine/fiscal.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,43 @@
227227
};
228228
}
229229

230+
function applyBudgetBalanceTarget(data, id, national, budgetCapacity, targetBalance) {
231+
let budgetExpenditure = Math.max(0, roundCurrency(budgetCapacity - targetBalance));
232+
let fiscal = null;
233+
let best = null;
234+
235+
function consider(candidateExpenditure) {
236+
const candidate = Math.max(0, roundCurrency(candidateExpenditure));
237+
const candidateFiscal = calculateFiscalForNation(data, id, { budgetCapacity, budgetExpenditure: candidate });
238+
if (!candidateFiscal) return;
239+
const diff = Math.abs(roundCurrency(candidateFiscal.effectiveBalance - targetBalance));
240+
if (!best || diff < best.diff) {
241+
best = { budgetExpenditure: candidate, fiscal: candidateFiscal, diff };
242+
}
243+
}
244+
245+
for (let attempt = 0; attempt < 6; attempt++) {
246+
fiscal = calculateFiscalForNation(data, id, { budgetCapacity, budgetExpenditure });
247+
if (!fiscal) return null;
248+
consider(budgetExpenditure);
249+
const error = roundCurrency(fiscal.effectiveBalance - targetBalance);
250+
if (Math.abs(error) <= 1) break;
251+
budgetExpenditure = Math.max(0, roundCurrency(budgetExpenditure + error));
252+
}
253+
for (let offset = 1; offset <= 512 && (!best || best.diff > 1); offset++) {
254+
consider(budgetExpenditure - offset);
255+
consider(budgetExpenditure + offset);
256+
}
257+
if (!best) return null;
258+
national.budgetExpenditure = best.budgetExpenditure;
259+
return best.fiscal;
260+
}
261+
230262
function recalculateBudgets(data, options = {}) {
231263
const shouldUpdateDebt = options.updateDebt === true;
264+
const balanceTargets = data.meta?.tradeV4BudgetBalanceTargets && typeof data.meta.tradeV4BudgetBalanceTargets === "object" && !Array.isArray(data.meta.tradeV4BudgetBalanceTargets)
265+
? data.meta.tradeV4BudgetBalanceTargets
266+
: null;
232267
for (const id of Object.keys(data.national || {})) {
233268
const national = data.national[id];
234269
const budgetCapacity = calculateBudgetForNation(data, id, {
@@ -237,7 +272,10 @@
237272
});
238273
if (budgetCapacity === null) continue;
239274
national.budgetCapacity = budgetCapacity;
240-
let fiscal = calculateFiscalForNation(data, id, { budgetCapacity });
275+
const balanceTarget = balanceTargets ? number(balanceTargets[id], null) : null;
276+
let fiscal = Number.isFinite(balanceTarget)
277+
? applyBudgetBalanceTarget(data, id, national, budgetCapacity, balanceTarget)
278+
: calculateFiscalForNation(data, id, { budgetCapacity });
241279
if (!fiscal) continue;
242280
applyFiscalFields(national, fiscal);
243281
if (shouldUpdateDebt) {
@@ -247,6 +285,7 @@
247285
if (fiscal) applyFiscalFields(national, fiscal);
248286
}
249287
}
288+
if (balanceTargets && data.meta && options.keepTradeV4BudgetBalanceTargets !== true) delete data.meta.tradeV4BudgetBalanceTargets;
250289
return data;
251290
}
252291

site/js/engine/trade.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,10 +208,10 @@
208208

209209
function tradePolicyProfile(policy) {
210210
return {
211-
Protectionist: { access: 0.52, importDemand: 0.5, exportSupply: 0.68, capacity: 0.66, balanceRisk: 0.72 },
211+
Protectionist: { access: 0.52, importDemand: 0.5, exportSupply: 0.68, capacity: 0.66, balanceRisk: 0.78 },
212212
Balanced: { access: 1, importDemand: 1, exportSupply: 1, capacity: 1, balanceRisk: 1 },
213-
"Open Market": { access: 1.22, importDemand: 1.18, exportSupply: 1.14, capacity: 1.12, balanceRisk: 1.18 },
214-
"Free Trade": { access: 1.44, importDemand: 1.32, exportSupply: 1.26, capacity: 1.22, balanceRisk: 1.35 }
213+
"Open Market": { access: 1.22, importDemand: 1.18, exportSupply: 1.14, capacity: 1.12, balanceRisk: 1.08 },
214+
"Free Trade": { access: 1.44, importDemand: 1.32, exportSupply: 1.26, capacity: 1.22, balanceRisk: 1.14 }
215215
}[policy] || { access: 1, importDemand: 1, exportSupply: 1, capacity: 1, balanceRisk: 1 };
216216
}
217217

@@ -2124,13 +2124,14 @@
21242124
const policy = tradePolicyProfile(current.tradePolicy);
21252125
const logistics = tradeLogisticsProfile(current);
21262126
const relianceGap = number(current.exportReliance, 0) - number(current.importReliance, 0);
2127+
const relianceGapSignal = Math.sign(relianceGap) * Math.pow(Math.abs(relianceGap), 0.88);
21272128
const relianceAverage = Math.max(1, (number(current.exportReliance, 0) + number(current.importReliance, 0)) / 2);
21282129
const normalizedGap = relianceGap / relianceAverage;
2129-
const balanceScale = 160 + Math.sqrt(Math.max(totalFlow, 0)) * 0.72 + Math.sqrt(Math.max(current.budgetCapacity, 0)) * 8.5;
2130-
const structuralBalance = relianceGap * balanceScale * policy.balanceRisk;
2131-
const flowBalance = exportFlow * 0.034 - importFlow * 0.04;
2132-
const tariffBalance = number(impact.tariffRevenueDelta, 0) * 0.72 - number(impact.importCostDelta, 0) * 0.5;
2133-
const imbalancePenalty = -Math.pow(Math.abs(normalizedGap), 1.45) * balanceScale * 3.4 * (relianceGap < 0 ? 1.24 : 0.38);
2130+
const balanceScale = 70 + Math.sqrt(Math.max(totalFlow, 0)) * 0.24 + Math.sqrt(Math.max(current.budgetCapacity, 0)) * 3.2;
2131+
const structuralBalance = relianceGapSignal * balanceScale * policy.balanceRisk;
2132+
const flowBalance = exportFlow * 0.014 - importFlow * 0.015;
2133+
const tariffBalance = number(impact.tariffRevenueDelta, 0) * 0.55 - number(impact.importCostDelta, 0) * 0.36;
2134+
const imbalancePenalty = -Math.pow(Math.abs(normalizedGap), 1.35) * balanceScale * 0.72 * (relianceGap < 0 ? 1.14 : 0.34);
21342135
const tradeBalance = roundCurrency(flowBalance + structuralBalance + tariffBalance + imbalancePenalty + number(impact.tradeBalanceDelta, 0));
21352136
const tradeCapacity = roundCurrency(
21362137
worldPoolCapacityScore(current) / 600

0 commit comments

Comments
 (0)