[engine] ACCT DIVERGENCE when an acquirer inherits standing bond offers #45

Closed
opened 2026-09-29 18:17:02 +00:00 by Winnie · 8 comments
Owner

Found while smoke-testing deposit-bond-spec (PR #44): in one spectate game, Bank 1 acquired Bank 5 for 0 (second price) and the turn logged ACCT DIVERGENCE Bank 1 turn 15: dEq -2042 netInc -2573 diff 531 (cash 193434, deps 228410, loans 0, binv 150000, bcomm 0, bdebt 110000). Mains-only smoke of the same seed is clean (0 divergences), so the trigger is the bond-heavy play pattern: a bank with standing bond offers and heavy bond debt/asset books gets acquired. The failed bank's standing offers are transferred to the acquirer (issuer reassigned) while committed cash was already deducted at placement - the bargain-gain math or the offer handover is off by ~531 on this path. Needs a unit test that acquires a bank holding standing offers + bond debts/assets and asserts equity change == net income. Not a brain bug; the probe brain just exposed an engine path.

Found while smoke-testing deposit-bond-spec (PR #44): in one spectate game, Bank 1 acquired Bank 5 for 0 (second price) and the turn logged `ACCT DIVERGENCE Bank 1 turn 15: dEq -2042 netInc -2573 diff 531` (cash 193434, deps 228410, loans 0, binv 150000, bcomm 0, bdebt 110000). Mains-only smoke of the same seed is clean (0 divergences), so the trigger is the bond-heavy play pattern: a bank with standing bond offers and heavy bond debt/asset books gets acquired. The failed bank's standing offers are transferred to the acquirer (issuer reassigned) while committed cash was already deducted at placement - the bargain-gain math or the offer handover is off by ~531 on this path. Needs a unit test that acquires a bank holding standing offers + bond debts/assets and asserts equity change == net income. Not a brain bug; the probe brain just exposed an engine path.
Winnie self-assigned this 2026-09-29 18:17:02 +00:00
Author
Owner

Starting work on this now. Plan: (1) reproduce with a Go unit test that acquires a bank holding standing bond offers + bond debt/assets and asserts equity change == net income (expect RED), (2) fix the engine root cause on the offer-handover/bargain path, (3) build + vet + full test pass, (4) re-run the deposit-bond-spec x2 + 4 mains spectate smoke and confirm zero ACCT DIVERGENCE lines, (5) push branch and open a PR for review. Will post progress here at every step.

Starting work on this now. Plan: (1) reproduce with a Go unit test that acquires a bank holding standing bond offers + bond debt/assets and asserts equity change == net income (expect RED), (2) fix the engine root cause on the offer-handover/bargain path, (3) build + vet + full test pass, (4) re-run the deposit-bond-spec x2 + 4 mains spectate smoke and confirm zero ACCT DIVERGENCE lines, (5) push branch and open a PR for review. Will post progress here at every step.
Author
Owner

Step 2 done: worktree /root/bw-45-acct on branch 45-acct created in bankdev off origin/master @ e939cb1 (PR #44 merge). Now reading game.go (bankrupt/financePhase/bondTake/placeOffers/revokeOffers) and finance.go (report) to trace the offer-handover path before writing the reproduction test.

Step 2 done: worktree /root/bw-45-acct on branch 45-acct created in bankdev off origin/master @ e939cb1 (PR #44 merge). Now reading game.go (bankrupt/financePhase/bondTake/placeOffers/revokeOffers) and finance.go (report) to trace the offer-handover path before writing the reproduction test.
Author
Owner

Red test in hand. Reproduction: TestAcquisitionAccounting builds the issue scenario exactly - bank A holds a standing bond offer (66,500 committed cash) plus 60,000 bond assets and 30,000 bond debt, spikes its deposit rate so interest+op expense can't be covered, goes insolvent, and bank B acquires the position at the Vickrey second price (0). FAILS: dEq 22100, netInc 21850, diff 250.

Root cause found (not the offer handover): the 250 is exactly failed.Deposits (50,000) x acquirer.DepRate (0.005) - matching the smoke's 531 signature (failed deposits x acquirer rate there too). report() recomputes DepInterest = Deposits * DepRate at report time using the POST-acquisition deposit balance. The failed bank already paid its own turn-T deposit interest from its cash before the auction, and that payment is embedded in the bargain via failedNet, so the acquirer's income statement double-charges interest on the acquired deposits. I verified the standing-offer handover itself is accounting-neutral (committed cash vs BondComm, fills vs PendingBondIncome/BondInv, expiries vs BondsReturned all balance). Fix: income statement must use the interest actually paid this turn (b.CF.DepInterest, computed by financePhase before the acquisition), not a recompute from the current deposit balance. Applying it now.

Red test in hand. Reproduction: TestAcquisitionAccounting builds the issue scenario exactly - bank A holds a standing bond offer (66,500 committed cash) plus 60,000 bond assets and 30,000 bond debt, spikes its deposit rate so interest+op expense can't be covered, goes insolvent, and bank B acquires the position at the Vickrey second price (0). FAILS: dEq 22100, netInc 21850, diff 250. Root cause found (not the offer handover): the 250 is exactly failed.Deposits (50,000) x acquirer.DepRate (0.005) - matching the smoke's 531 signature (failed deposits x acquirer rate there too). report() recomputes DepInterest = Deposits * DepRate at report time using the POST-acquisition deposit balance. The failed bank already paid its own turn-T deposit interest from its cash before the auction, and that payment is embedded in the bargain via failedNet, so the acquirer's income statement double-charges interest on the acquired deposits. I verified the standing-offer handover itself is accounting-neutral (committed cash vs BondComm, fills vs PendingBondIncome/BondInv, expiries vs BondsReturned all balance). Fix: income statement must use the interest actually paid this turn (b.CF.DepInterest, computed by financePhase before the acquisition), not a recompute from the current deposit balance. Applying it now.
Author
Owner

Fix applied and verified green: one line in finance.go - report() now books DepInterest from b.CF.DepInterest (the interest actually paid this turn, recorded by financePhase before the acquisition) instead of recomputing Deposits*DepRate against the post-acquisition deposit balance. The regression test (acquisition_accounting_test.go) runs the inherited books through their full lifecycle (offer aging + expiry, bond asset maturity, bond debt repayment) and asserts equity change == net income exactly for every alive bank on every turn; it fails with diff 250 pre-fix and passes with diff 0 post-fix. Now running go build, go vet, and the full test suite.

Fix applied and verified green: one line in finance.go - report() now books DepInterest from b.CF.DepInterest (the interest actually paid this turn, recorded by financePhase before the acquisition) instead of recomputing Deposits*DepRate against the post-acquisition deposit balance. The regression test (acquisition_accounting_test.go) runs the inherited books through their full lifecycle (offer aging + expiry, bond asset maturity, bond debt repayment) and asserts equity change == net income exactly for every alive bank on every turn; it fails with diff 250 pre-fix and passes with diff 0 post-fix. Now running go build, go vet, and the full test suite.
Author
Owner

go build, go vet, and full go test suite all pass. Now re-running the exact spectate smoke that produced this bug: seed 33, 6 players, deposit-bond-spec x2 + 4 mains (empire/mill/vault/adapt), /tmp/cfg.json. Will grep the full output for DIVERGENCE lines and for [ai hook errors.

go build, go vet, and full go test suite all pass. Now re-running the exact spectate smoke that produced this bug: seed 33, 6 players, deposit-bond-spec x2 + 4 mains (empire/mill/vault/adapt), /tmp/cfg.json. Will grep the full output for DIVERGENCE lines and for [ai hook errors.
Author
Owner

Spectate smoke verified both ways. UNFIXED engine, same seed/config: 3 ACCT DIVERGENCE lines including the exact original signature (Bank 1 turn 15, diff 531). FIXED engine: 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 fix only changes report numbers, not game state, so both runs evolve identically - the divergence lines are simply gone. Committing to branch 45-acct and opening the PR next.

Spectate smoke verified both ways. UNFIXED engine, same seed/config: 3 ACCT DIVERGENCE lines including the exact original signature (Bank 1 turn 15, diff 531). FIXED engine: 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 fix only changes report numbers, not game state, so both runs evolve identically - the divergence lines are simply gone. Committing to branch 45-acct and opening the PR next.
Author
Owner

Done — PR #50 ready for review: #50

Root cause: report() recomputed DepInterest = Deposits * DepRate against the POST-acquisition deposit balance. The failed bank had already paid its own turn-T deposit interest from its cash before the auction (embedded in the bargain gain via failedNet), so the acquirer was charged that interest a second time. The overcharge is exactly failed.Deposits x acquirer.DepRate — the 531 in the original smoke log, 250 in the unit test. The standing-offer handover itself was accounting-neutral (committed cash vs BondComm, fills vs PendingBondIncome/BondInv, expiries vs BondsReturned — all verified consistent), so no changes there.

Fix: one line in finance.go — income statement books the interest actually paid this turn (b.CF.DepInterest, recorded by financePhase before the acquisition), matching the cash flow statement.

Validation:

  • New regression test TestAcquisitionAccounting: bank with standing offer + bond assets + bond debt goes insolvent, acquirer inherits; identity asserted exactly every turn through the inherited books' full lifecycle (aging, expiry, both maturities). RED pre-fix (diff 250), GREEN post-fix (diff 0).
  • go build, go vet, go test all pass.
  • Exact spectate smoke (seed 33, deposit-bond-spec x2 + 4 mains): 0 DIVERGENCE lines, 3 acquisitions exercised. Unfixed engine on the same smoke logs 3 divergences incl. the original Bank 1 turn 15 diff 531.

Branch 45-acct, master untouched. Closing after review/merge.

Done — PR #50 ready for review: https://git.machinetele.com/Winnie/bankwars/pulls/50 Root cause: report() recomputed DepInterest = Deposits * DepRate against the POST-acquisition deposit balance. The failed bank had already paid its own turn-T deposit interest from its cash before the auction (embedded in the bargain gain via failedNet), so the acquirer was charged that interest a second time. The overcharge is exactly failed.Deposits x acquirer.DepRate — the 531 in the original smoke log, 250 in the unit test. The standing-offer handover itself was accounting-neutral (committed cash vs BondComm, fills vs PendingBondIncome/BondInv, expiries vs BondsReturned — all verified consistent), so no changes there. Fix: one line in finance.go — income statement books the interest actually paid this turn (b.CF.DepInterest, recorded by financePhase before the acquisition), matching the cash flow statement. Validation: - New regression test TestAcquisitionAccounting: bank with standing offer + bond assets + bond debt goes insolvent, acquirer inherits; identity asserted exactly every turn through the inherited books' full lifecycle (aging, expiry, both maturities). RED pre-fix (diff 250), GREEN post-fix (diff 0). - go build, go vet, go test all pass. - Exact spectate smoke (seed 33, deposit-bond-spec x2 + 4 mains): 0 DIVERGENCE lines, 3 acquisitions exercised. Unfixed engine on the same smoke logs 3 divergences incl. the original Bank 1 turn 15 diff 531. Branch 45-acct, master untouched. Closing after review/merge.
Author
Owner

Merged: PR #50 is on master. Deposit interest is now booked from what was actually paid, not recomputed post-acquisition, with a regression test asserting the accounting identity every turn through an inherited-book lifecycle. Closing.

Merged: PR #50 is on master. Deposit interest is now booked from what was actually paid, not recomputed post-acquisition, with a regression test asserting the accounting identity every turn through an inherited-book lifecycle. Closing.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
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#45
No description provided.