Allow the rounding a broker's own printed figures propagate

A real Scalable export refused to import at record 148: "buy gross -808.5599
does not equal quantity 6 times price 134.76, which is -808.56". Six NVIDIA
shares settled at 808.5599 against a printed price of 134.76, because the fill
was 134.759983 and the export printed the price to two places. One
ten-thousandth out, and the whole file was rejected.

The check held a gross to its own stated precision, which is only half the
story: the price is rounded too, and the file never says by how much. So the
allowance is now half a unit of the gross's stated precision plus one part in a
hundred thousand of the gross, compared against a product kept exact at 1e-16
rather than rounded first.

Measured over the complete export - 88 security rows - exactly one deviates at
all, by one part in eight million, eighty times inside the new bound. What the
bound still refuses is unchanged in kind: a price taken from the wrong share
class, and the misplaced decimal separator the check exists for, which misses
by four orders of magnitude. What it now accepts is the broker's own rounding,
including a whole cent once a gross stated to the cent passes about five hundred
euro, where a genuine one-cent error cannot be told from that rounding anyway.

The row is kept as a regression test alongside four grosses that must still be
refused: a cent, a euro, a wrong instrument's price, and a factor of ten.
This commit is contained in:
Lars Nolden
2026-09-12 12:20:19 +02:00
parent da817078f4
commit 635c11be56
4 changed files with 99 additions and 36 deletions
+32
View File
@@ -228,6 +228,38 @@ func TestSingleShareRowCatchesOnlyInconsistentArithmetic(t *testing.T) {
}
}
// A broker's own gross can disagree with its own printed shares times price,
// because the price is printed to fewer places than the fill actually had.
// Six NVIDIA shares settled at 808.5599 against a printed 134.76, whose
// product is 808.56: one ten-thousandth out, and the whole file was refused.
// The rounding the printed figures propagate is allowed; anything above one
// part in a hundred thousand still is not.
func TestRoundedPriceDoesNotRejectTheBrokersOwnGross(t *testing.T) {
const row = `2025-01-09;10:37:32;Executed;SCALixkS3TomjQv;NVIDIA;Security;Buy;US67066G1040;6;134,76;-808,5599;0,00;0,00;EUR`
result := readBroker(t, row)
inv := result.Facts[0].Investment
if inv.Gross != "-808.5599" || inv.Price != "134.76" || inv.Quantity != "6" {
t.Fatalf("trade read as %+v", inv)
}
if got := result.Facts[0].Amount; got != "-808.5599" {
t.Errorf("settled %s, want -808.5599", got)
}
for name, gross := range map[string]string{
"one cent out": "-808,5699",
"factor of ten": "-8.085,599",
"a euro out": "-809,5599",
"wrong instrument": "-908,5599",
} {
file, err := ReadCSV(strings.NewReader(scalableHeader + strings.Replace(row, ";-808,5599;", ";"+gross+";", 1) + "\n"))
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if _, err := ParseScalableCSV(file, brokerAccount(), nil); err == nil {
t.Errorf("%s: accepted a gross its own shares times price does not support", name)
}
}
}
// A reinvested distribution settles shares times price, so it carries as many
// decimal places as the two together need. A real export reinvests to nine,
// which is past what money holds and past what a share count holds, so reading