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:
+49
-27
@@ -639,26 +639,21 @@ func validateInvestment(f Facts, a Account, instruments map[string]Instrument) e
|
||||
return fmt.Errorf("%s requires a nonzero quantity", inv.Event)
|
||||
}
|
||||
// A position-only valuation carries the sign of the position change; a
|
||||
// settled trade carries the sign of the cash, which is the opposite.
|
||||
expected, ok := RoundedProduct(quantity, price)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s quantity times price is out of range", inv.Event)
|
||||
}
|
||||
// settled trade carries the sign of the cash, which is the opposite. The
|
||||
// product is kept exact at 1e-16 so the comparison never rounds first.
|
||||
product := new(big.Int).Mul(big.NewInt(quantity), big.NewInt(price))
|
||||
if inv.Settling() {
|
||||
expected = -expected
|
||||
product.Neg(product)
|
||||
}
|
||||
// The gross is checked to the precision the broker stated it at, and no
|
||||
// further. One broker prints the exact product to nine places, and the
|
||||
// check is then exact. Another prints the notional rounded to cents, where
|
||||
// demanding exactness rejects every trade whose product does not land on a
|
||||
// whole cent - measured on a real export, 29 of 59 of them. One unit of
|
||||
// the stated precision is still four orders of magnitude tighter than the
|
||||
// misplaced decimal separator this check exists to catch.
|
||||
difference := expected - gross
|
||||
if difference < 0 {
|
||||
difference = -difference
|
||||
}
|
||||
if difference >= statedUnit(inv.Gross) {
|
||||
difference := new(big.Int).Sub(product, new(big.Int).Mul(big.NewInt(gross), productPerMoney))
|
||||
if difference.Abs(difference).Cmp(grossSlack(gross, inv.Gross)) > 0 {
|
||||
expected, ok := RoundedProduct(quantity, price)
|
||||
if !ok {
|
||||
return fmt.Errorf("%s quantity times price is out of range", inv.Event)
|
||||
}
|
||||
if inv.Settling() {
|
||||
expected = -expected
|
||||
}
|
||||
return fmt.Errorf("%s gross %s does not equal quantity %s times price %s, which is %s", inv.Event, inv.Gross.String(), inv.Quantity.String(), inv.Price.String(), Money(formatScaled(expected, moneyScale, 2)))
|
||||
}
|
||||
if inv.PositionOnly() {
|
||||
@@ -689,14 +684,41 @@ func settles(inv *Investment, gross, fee, tax, amount int64) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// statedUnit is one unit of the last decimal place a money figure was written
|
||||
// with, in exact ten-thousandths. Money always renders at least two places, so
|
||||
// a whole-euro figure counts as stated to the cent.
|
||||
func statedUnit(m Money) int64 {
|
||||
_, fraction, _ := strings.Cut(string(m), ".")
|
||||
unit := int64(1)
|
||||
for range moneyScale - len(strings.TrimRight(fraction, "0")) {
|
||||
unit *= 10
|
||||
// productPerMoney converts money's ten-thousandths to the 1e-16 units a
|
||||
// quantity times a price lands in.
|
||||
var productPerMoney = new(big.Int).Exp(big.NewInt(10), big.NewInt(productScale-moneyScale), nil)
|
||||
|
||||
const productScale = quantityScale * 2
|
||||
|
||||
// grossSlack is how far a printed gross may sit from the product of the printed
|
||||
// quantity and price before the row is refused. Both ends are rounded, and
|
||||
// neither states by how much.
|
||||
//
|
||||
// The gross is rounded to its own last decimal place: one broker prints the
|
||||
// notional to the cent, so 0.426581 shares at 63.06 settle as 26.90 where the
|
||||
// product is 26.90019786, and demanding exactness there rejects half a
|
||||
// portfolio. The price is rounded to a precision the file does not state: the
|
||||
// same export settles six NVIDIA shares at 808.5599 while printing the price
|
||||
// as 134.76, whose product is 808.56, because the real fill was 134.759983.
|
||||
// So the slack is half a unit of the gross's stated precision, plus one part
|
||||
// in a hundred thousand of the gross itself.
|
||||
//
|
||||
// Measured over a complete real export of 88 security rows, exactly one
|
||||
// deviates at all, by one part in eight million - eighty times inside this
|
||||
// bound. What it refuses: any deviation above one part in a hundred thousand,
|
||||
// which covers a price taken from the wrong share class and the lost decimal
|
||||
// separator this check exists for, four orders of magnitude out. What it
|
||||
// accepts: the broker's own rounding. On a gross stated to the cent the slack
|
||||
// reaches a whole cent at around five hundred euro, above which a genuine
|
||||
// one-cent error is indistinguishable from that rounding and is allowed.
|
||||
func grossSlack(gross int64, printed Money) *big.Int {
|
||||
_, fraction, _ := strings.Cut(string(printed), ".")
|
||||
places := len(fraction)
|
||||
if places > moneyScale {
|
||||
places = moneyScale
|
||||
}
|
||||
return unit
|
||||
half := new(big.Int).Exp(big.NewInt(10), big.NewInt(int64(productScale-places)), nil)
|
||||
half.Quo(half, big.NewInt(2))
|
||||
relative := new(big.Int).Abs(new(big.Int).Mul(big.NewInt(gross), productPerMoney))
|
||||
return half.Add(half, relative.Quo(relative, big.NewInt(100_000)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user