[#45] Fix ACCT DIVERGENCE when acquirer inherits standing bond offers #50

Merged
robertismo merged 1 commit from 45-acct into master 2026-09-29 18:53:31 +00:00
Owner

"Fixes #45.\n\n## Root cause\n\nreport() computed the income-statement line as DepInterest = b.Deposits * b.DepRate \u2014 a fresh recomputation against the post-acquisition deposit balance. On the turn a bank is acquired:\n\n1. financePhase pays the failed bank's own deposit interest out of the failed bank's cash before the auction runs.\n2. bankrupt() transfers the failed bank's deposits to the acquirer.\n3. report() then charges the acquirer interest on the acquired deposits too.\n\nThe interest was therefore double-counted: once inside the bargain purchase gain (the failed bank's cash, which already had the interest deducted, feeds failedNet), and once again as the acquirer's DepInterest. The overcharge is exactly failed.Deposits \u00d7 acquirer.DepRate \u2014 the 531 in the smoke log (dEq -2042 netInc -2573 diff 531), and 250 in the unit test. The standing-bond-offer handover itself is accounting-neutral (committed cash \u2194 BondComm, fills \u2194 PendingBondIncome/BondInv at face, expiries \u2194 BondsReturned \u2014 all verified); the bug fires on every acquisition where the failed bank has deposits and the acquirer posts a nonzero rate, which is why only the bond-heavy smoke (which drives banks to insolvency) exposed it.\n\n## Fix\n\nOne line in finance.go: the income statement books the interest actually paid this turn (b.CF.DepInterest, recorded by financePhase before any acquisition), matching the statement of cash flows and restoring the identity equity change == net income exactly.\n\n## Regression test\n\nTestAcquisitionAccounting (acquisition_accounting_test.go) constructs the issue scenario deterministically: bank A holds a standing bond offer (66,500 committed cash) plus 60,000 bond assets and 30,000 bond debt, goes insolvent (rate spike makes interest + op expense uncovered), and bank B acquires the position at the Vickrey second price (0). The test then runs the inherited books through their full lifecycle \u2014 offer aging and expiry (committed cash returns), bond asset maturity, bond debt repayment \u2014 asserting equity change == net income exactly for every alive bank on every turn, plus zero DIVERGENCE log lines.\n\n- Before fix: FAILS \u2014 turn 2 diff 250 (exactly failed.Deposits \u00d7 acquirer.DepRate).\n- After fix: PASSES \u2014 diff 0.00 through turn 10.\n\n## Verification\n\n- go build -o bankwars . \u2014 clean\n- go vet ./... \u2014 clean\n- go test . \u2014 all pass\n- Spectate smoke, seed 33, 6 players, deposit-bond-spec \u00d72 + 4 mains (/tmp/cfg.json): 0 DIVERGENCE lines (ACCT and CF), 0 AI hook errors, 3 acquisitions exercised (Bank 1 acquires Bank 3 for 0, Bank 2 acquires Bank 1 for 0, Bank 2 acquires Bank 6 for 0), game completes with 3 survivors. The same smoke on the unfixed engine logs 3 ACCT DIVERGENCE lines, including the exact original signature Bank 1 turn 15 ... diff 531.\n"

"Fixes #45.\n\n## Root cause\n\n`report()` computed the income-statement line as `DepInterest = b.Deposits * b.DepRate` \u2014 a fresh recomputation against the **post-acquisition** deposit balance. On the turn a bank is acquired:\n\n1. `financePhase` pays the failed bank's own deposit interest out of the failed bank's cash **before** the auction runs.\n2. `bankrupt()` transfers the failed bank's deposits to the acquirer.\n3. `report()` then charges the acquirer interest on the *acquired* deposits too.\n\nThe interest was therefore double-counted: once inside the bargain purchase gain (the failed bank's cash, which already had the interest deducted, feeds `failedNet`), and once again as the acquirer's `DepInterest`. The overcharge is exactly `failed.Deposits \u00d7 acquirer.DepRate` \u2014 the 531 in the smoke log (`dEq -2042 netInc -2573 diff 531`), and 250 in the unit test. The standing-bond-offer handover itself is accounting-neutral (committed cash \u2194 `BondComm`, fills \u2194 `PendingBondIncome`/`BondInv` at face, expiries \u2194 `BondsReturned` \u2014 all verified); the bug fires on every acquisition where the failed bank has deposits and the acquirer posts a nonzero rate, which is why only the bond-heavy smoke (which drives banks to insolvency) exposed it.\n\n## Fix\n\nOne line in `finance.go`: the income statement books the interest **actually paid this turn** (`b.CF.DepInterest`, recorded by `financePhase` before any acquisition), matching the statement of cash flows and restoring the identity `equity change == net income` exactly.\n\n## Regression test\n\n`TestAcquisitionAccounting` (`acquisition_accounting_test.go`) constructs the issue scenario deterministically: bank A holds a standing bond offer (66,500 committed cash) plus 60,000 bond assets and 30,000 bond debt, goes insolvent (rate spike makes interest + op expense uncovered), and bank B acquires the position at the Vickrey second price (0). The test then runs the inherited books through their full lifecycle \u2014 offer aging and expiry (committed cash returns), bond asset maturity, bond debt repayment \u2014 asserting `equity change == net income` exactly for every alive bank on every turn, plus zero `DIVERGENCE` log lines.\n\n- Before fix: **FAILS** \u2014 turn 2 diff 250 (exactly `failed.Deposits \u00d7 acquirer.DepRate`).\n- After fix: **PASSES** \u2014 diff 0.00 through turn 10.\n\n## Verification\n\n- `go build -o bankwars .` \u2014 clean\n- `go vet ./...` \u2014 clean\n- `go test .` \u2014 all pass\n- Spectate smoke, seed 33, 6 players, deposit-bond-spec \u00d72 + 4 mains (`/tmp/cfg.json`): **0 DIVERGENCE lines** (ACCT and CF), 0 AI hook errors, 3 acquisitions exercised (Bank 1 acquires Bank 3 for 0, Bank 2 acquires Bank 1 for 0, Bank 2 acquires Bank 6 for 0), game completes with 3 survivors. The same smoke on the unfixed engine logs 3 `ACCT DIVERGENCE` lines, including the exact original signature `Bank 1 turn 15 ... diff 531`.\n"
[#45] Fix ACCT DIVERGENCE when acquirer inherits standing bond offers
All checks were successful
build / build (push) Successful in 24s
980e27ca21
report() recomputed DepInterest as Deposits*DepRate against the
post-acquisition deposit balance, so a bank that acquired a failed bank
was charged deposit interest on the acquired deposits a second time
(the failed bank already paid that interest from its own cash before
the auction, and that payment is embedded in the bargain purchase
gain via failedNet). The overcharge is exactly failed.Deposits x
acquirer.DepRate - the 531 from the smoke log, 250 in the unit test.

Income statement now books the interest actually paid this turn
(b.CF.DepInterest, recorded by financePhase before the acquisition),
matching the cash flow statement and restoring equity-change ==
net-income.

Regression test TestAcquisitionAccounting: a bank holding a standing
bond offer (committed cash), bond assets, and bond debt goes insolvent
and is acquired; the inherited books then run their full lifecycle
(offer aging/expiry, asset maturity, debt repayment) with the identity
asserted exactly for every alive bank on every turn. Fails with diff
250 pre-fix, passes with diff 0 post-fix.
First-time contributor

@Winnie Can you explain this in simple terms?

@Winnie Can you explain this in simple terms?
Author
Owner

In plain terms:

Think of each bank's books as a checkbook that has to balance. Every turn, the game prints an income statement and then checks one rule: "the change in the bank's net worth must exactly equal the profit it reported." If those two numbers don't match, something got counted twice or not at all. This PR fixes a case where interest got counted twice.

The bug: when a bank fails and another bank buys its corpse at auction, the buyer takes over the dead bank's depositors, and from then on the buyer pays interest on those deposits. Fine. But on the day of the takeover, the accounting code looked at the buyer's balance sheet, saw the freshly acquired deposits, and charged the buyer interest on them for that day. The dead bank had already paid that day's interest itself before it failed. So the same interest was paid twice, and the books were off by exactly that amount.

The fix is one line: the income statement now reports the interest the bank actually paid that turn (which the game already records when the cash leaves), instead of recomputing it from the current deposit balance.

How I'm sure: I wrote a test that re-creates the exact scenario, takeover and all. Before the fix, the books were off by 250. After the fix, off by zero, every turn. And the original bug report's exact game now runs clean.

Why it mattered in practice: every time a takeover happened involving depositors, the buyer's reported profit was wrong. In games with lots of failures, those small errors could add up and make a bank look healthier or sicker than it really was.

In plain terms: Think of each bank's books as a checkbook that has to balance. Every turn, the game prints an income statement and then checks one rule: "the change in the bank's net worth must exactly equal the profit it reported." If those two numbers don't match, something got counted twice or not at all. This PR fixes a case where interest got counted twice. The bug: when a bank fails and another bank buys its corpse at auction, the buyer takes over the dead bank's depositors, and from then on the buyer pays interest on those deposits. Fine. But on the day of the takeover, the accounting code looked at the buyer's balance sheet, saw the freshly acquired deposits, and charged the buyer interest on them for that day. The dead bank had already paid that day's interest itself before it failed. So the same interest was paid twice, and the books were off by exactly that amount. The fix is one line: the income statement now reports the interest the bank actually paid that turn (which the game already records when the cash leaves), instead of recomputing it from the current deposit balance. How I'm sure: I wrote a test that re-creates the exact scenario, takeover and all. Before the fix, the books were off by 250. After the fix, off by zero, every turn. And the original bug report's exact game now runs clean. Why it mattered in practice: every time a takeover happened involving depositors, the buyer's reported profit was wrong. In games with lots of failures, those small errors could add up and make a bank look healthier or sicker than it really was.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Winnie/bankwars!50
No description provided.