[engine] ACCT DIVERGENCE when an acquirer inherits standing bond offers #45
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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.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.
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.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.
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.
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.
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.
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:
Branch 45-acct, master untouched. Closing after review/merge.
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.