The Problem of Non-integer Shares

Open a modern brokerage account and you own fractional shares almost before you mean to. Dividend reinvestment turns a 53.31 USD payout into 0.079 shares of an ETF; dollar-based recurring buys convert a flat 200 USD into whatever slice of a share that happens to buy today; fractional-share trading lets you order half a share outright. Whole-number lots are increasingly the exception, not the rule.

This is a bookkeeping problem, not just a trading one, because a fractional lot is not quite the same thing to record as a whole-share one. In Beancount — and in double-entry systems generally — the natural way to record a purchase is to attach a per-share cost to the shares: ten shares at 200 each. That notation is exact when the share count is a whole number, and every tutorial reaches for it, including one I wrote last year. But the moment the share count carries decimals, that same notation quietly records a cost basis that is wrong — off by a few cents per lot, with no error to warn you, and in a way that eventually shows up as a mis-stated capital gain.

This post is about why that happens and how to record fractional lots correctly. The fix itself is small — declare the total cost of the lot rather than its per-share price — but it’s worth walking through the reasoning, because it generalizes beyond stocks: whenever three numbers are bound by a single equation, only two of them carry independent information, and getting your books right comes down to writing the right two down.

The Beancount example in this post can be found in: https://github.com/flyaway1217/beancount_example/blob/main/single_examples/fractional_share_cost_basis.bean

The cleanest way to see the problem is a real fractional lot. Here’s a reinvested dividend from Fidelity. It reports three numbers: a share count of 0.079, a price of 678.58, and an amount of 53.31.

Fidelity reinvestment detail showing Shares +0.079, Price 678.58, and Amount -53.31

But multiply the first two:

0.079 × 678.58 = 53.6078

The Amount line says 53.31. That’s about thirty cents off.

At the time I didn’t think much about it. I added a fee posting with no amount, Expenses:Financial:Fees, and let Beancount work out the difference on its own:

2026-06-30 * "Fidelity" "REINVESTMENT VANGUARD INDEX FUNDS S&P 500 ETF (VOO)"
    Assets:Fidelity:Cash                    -53.31 USD
    Assets:Fidelity:Playground:VOO          0.079 VOO {678.58 USD}
    Expenses:Financial:Fees

The books balanced, bean-check passed, and I moved on.

But this transaction is wrong — and wrong in two separate places.

First, that fee doesn’t exist. DRIP is free. The number Beancount put in the Fees posting has no counterpart in reality; it’s just the residual the software back-solved to make the transaction balance.

Second, the cost basis of this lot is wrong. {678.58 USD} is a per-share cost, so Beancount computes total cost as 0.079 × 678.58 = 53.6078. But the real total cost is 53.31 — the number on the cash posting, the number Fidelity recorded, and the number that will eventually show up on my 1099-B.

These two errors are two faces of the same misunderstanding.

Diagnosis

Three Numbers, Two Degrees of Freedom

The problem isn’t in any single number. It’s in the relationship between them. Shares, price, and amount are bound by one equation:

shares × price = amount

One equation, three unknowns — that’s two degrees of freedom, not three. What your broker hands you is an over-determined record: only two of the three numbers carry independent information, the third is derived, and all three have been independently rounded for display.

So if you take any two and use them to reconstruct the third, you get a number the broker never recorded. I chose shares × price to reconstruct amount — when amount was already sitting right there on the line, written down in plain text.

Which Two Are Real Depends on the Transaction Type

When you place a market order for 10 shares, the order you submit is “10 shares,” the fill price is set by the market, and the amount is what settlement computes. The original quantities are shares and price.

Dividend reinvestment (DRIP) runs the causality in reverse. First a dividend of 53.31 lands — that amount is non-negotiable — and then it’s converted into stock at the day’s market price of 678.58. The share count is the quotient of that division:

53.31 ÷ 678.58 = 0.0785611...

Fidelity rounds that quotient to three decimals for display, giving 0.079. When I multiply that rounded quotient back by 678.58, I’m amplifying an error in the fourth decimal place by a factor of 678.

You can verify this diagnosis directly on the lot page:

Fidelity lot page: the 0.079-share lot acquired Jun-29-2026 shows an average cost basis of 674.81 and a cost basis total of 53.31

Shares True basis shares × price Error
2 1372.80 1372.8000 0.0000
1 684.03 684.0300 0.0000
0.185 126.99 126.9840 −0.0060
0.079 53.31 53.6078 +0.2978

The integer-share lots are perfectly clean: in those trades shares is the original quantity, never rounded, so the reconstruction path happens to be lossless. The error appears only on the rows where shares is the derived quantity — and that’s not a coincidence, it’s exactly what the mechanism dictates. The reason last year’s examples held up is that I used integer shares.

Mistake One: A Plug That Fixes Nothing

Back to that Fees posting from the beginning. It has no amount, so Beancount interpolates one to make the transaction balance:

VOO leg:   0.079 × 678.58 = +53.6078
Cash leg:                   −53.3100
                           ─────────
Difference:                 +0.2978  →  Fees = −0.2978 USD

A negative thirty-cent fee — the residue of that inequality — gets absorbed into an empty posting.

Let’s be clear about the amounts first. The errors across all the lots on that page add up to 0.29 USD total; at a quarterly DRIP cadence, that’s roughly seven dollars over ten years. And those seven dollars never become taxes: your return is filed from the 1099-B your broker mails you, and the cost basis on it is Fidelity’s 53.31, not my ledger’s 53.6078. The ledger can be wrong and the tax form is still right.

So if your reaction is “thirty cents isn’t worth chasing,” you’re right. It isn’t. But the problem with that Fees posting isn’t the thirty cents.

It doesn’t correct anything. After you add it, the cost basis is still 53.6078, still thirty cents off from what Fidelity recorded. The plug changes exactly one thing: Beancount stops reporting an error.

It also introduces a new false fact: my ledger now claims this DRIP charged a negative thirty-cent fee. That’s not an approximation, it’s a transaction that never happened. The day I want to ask “how much did I pay in investment fees this year,” that number will be wrong.

More fundamentally, look at my motive. I added that posting not because I knew there was a fee, but because the transaction wouldn’t balance. That ordering is backwards: balancing isn’t the goal of bookkeeping, it’s the check on bookkeeping. Editing the data to make the check pass is the same as deleting the check.

Delete the Fees posting and run it again, and Beancount says:

Transaction does not balance: (0.29782 USD)

It was standing by the whole time, ready to tell me these two legs weren’t saying the same thing. I just didn’t let it finish.

Every transaction must balance on its own data. If it doesn’t, let it error — don’t plug.

Mistake Two: A Unit Price Can’t Carry a Total

What about the other error — why is {678.58 USD} the wrong notation?

Start with the definition of a capital gain:

gain = proceeds − total_cost

The fields on a 1099-B are acquisition date, sale date, proceeds (a total), and cost basis (a total) — all totals, with no “per-share cost” column, because the tax code doesn’t need one. What the broker stores is the pair (53.31 USD, Jun-29-2026); what you pick in the specific-share dialog is the identity of a lot, not a price.

So there are really only two quantities that matter: shares and amount. Price has no independent standing anywhere in this — it’s the one of the three numbers that gets derived, and the one that gets rounded.

The problem is one of expression: I used a unit price to express a total-cost concept. With integer shares that’s harmless — 10 AMZN {200.00 USD} gives a total cost of exactly 2000, and you can go back and forth between unit price and total freely. With fractional shares the equivalence breaks, because one factor of that multiplication has been rounded.

Just reverse the direction: declare the total, and let Beancount do the division.

Solution

Double Braces

Beancount gives you the syntax for exactly this:

2026-06-30 * "Fidelity" "REINVESTMENT VANGUARD INDEX FUNDS S&P 500 ETF (VOO)"
    Assets:Fidelity:Cash                    -53.31 USD
    Assets:Fidelity:Playground:VOO          0.079 VOO {{53.31 USD}}

Double braces mean total cost; single braces mean per-share cost. The difference looks small and isn’t: the cost basis is now exactly 53.31, matching the 1099-B to the penny — not “negligible error,” but zero error. And since the cash leg is −53.31 and the VOO leg’s weight is also 53.31, the transaction balances on its own. The Fees posting is no longer needed, and must be deleted.

This notation has a bonus: you don’t need to know which field is the original quantity. You don’t have to decide whether this is a buy or a DRIP, or guess where in the pipeline the broker did its division. amount is the number you declare — you just read it off. One rule, no branches.

The Price of Precision: an Ugly Number

So what does Beancount actually store internally after you use {{}}? I printed it out to see:

Cost(number=Decimal('674.8101265822784810126582278'),
     currency='USD', date=..., label=None)

Twenty-eight significant digits. The reason is that Beancount’s storage model has no “total cost” field — it always stores a per-share cost, and the total is computed on the fly as shares × per_share_cost. {{}} is just sugar: the parser takes 53.31 ÷ 0.079 and stores that quotient in full.

It is genuinely ugly, and you’ll run into it in bean-format output and error messages. But don’t try to clean it up: ugly and accurate are the same thing here. 53.31 ÷ 0.079 doesn’t divide evenly, and the point of those 28 digits is that not a single bit of information is lost — multiply it back and you recover 53.31 exactly. Every digit you trim injects a little rounding error, trading away the one invariant you have — that the total is fixed — for one line of prettier output. And the whole point of this post is cleaning up the rounding someone else did for you; there’s no reason to turn around and do it to yourself.

Besides, you’ll rarely see it. What you look at day to day in Fava is the formatted number; that long decimal only surfaces if you open the raw file, and you never write it by hand — you write the total cost, and the per-share cost is Beancount’s problem.

A Side Effect: Matching Lots by Date, Not Price

Because what’s stored is that 28-digit decimal, you can no longer match a lot by its cost price: you can’t write 674.8101265822784810126582278, and writing {674.81 USD} gets you a flat “No position matches.” The kind of partial sale I wrote in last year’s post:

Assets:Fidelity:Playground:AMZN -5 AMZN {200.00 USD} @ 190 USD

doesn’t work on a lot created with {{}}. The match key has to become the date or a label instead:

Assets:Fidelity:Playground:VOO -0.0785611 VOO {2026-06-29} @ 700.00 USD

This is actually an upgrade, not a compromise. Cost price was always a fragile match key: two lots bought on the same day at the same price are immediately ambiguous. And the broker’s specific-share dialog already has you pick lots by date, not by price — so switching to dates means your match key and the broker’s UI are finally using the same concept.

Verifying It on a Sale

Suppose the account holds two lots, both recorded by the rule above: 1 share bought on 2026-05-21 with a total cost of 684.03, and the 0.079 shares from that DRIP on 2026-06-30 with a total cost of 53.31.

Selling an integer share. Sell the original share for 700:

2027-03-10 * "Sell 1 VOO"
    Assets:Fidelity:Playground:VOO   -1 VOO {2026-05-21} @ 700.00 USD
    Assets:Fidelity:Cash              700.00 USD
    Income:Fidelity:VOO:PnL

The difference from last year’s post is {2026-05-21}: it’s a date, not a cost price — the chain reaction described in the previous section. Run it:

Realized P&L = 15.97 USD
Remaining position = 0.079 VOO, cost basis 53.3100 USD

Bought at 684.03, sold for 700.00, gain of 15.97.

Selling the fractional share. Now sell the fractional lot the DRIP left behind:

2027-04-15 * "Sell 0.079 VOO"
    Assets:Fidelity:Playground:VOO   -0.079 VOO {2026-06-30} @ 700.00 USD
    Assets:Fidelity:Cash              55.30 USD
    Income:Fidelity:VOO:PnL
Realized P&L = 1.99 USD

55.30 − 53.31 = 1.99, matching the 1099-B to the penny. Note that -0.079 is exactly the number I recorded going in, carried over unchanged. Nowhere in this did I touch that 674.8101265822784810126582278 string — the cost basis is what Beancount recovered by multiplying it back, exactly equal to 53.31.

Now record the same sale with the notation from the beginning. The lot is 0.079 VOO {678.58 USD}, cost basis 53.6078:

Realized P&L = 1.69 USD

Thirty cents off. That’s where the thirty cents finally goes: it doesn’t disappear and it doesn’t raise an error — it becomes a wrong capital gain flowing into Income:VOO:PnL. The ledger is perfectly self-consistent internally, bean-check passes, it just isn’t saying the same thing as the 1099-B.

The integer-share sale computes 15.97 either way; the difference has nowhere to show itself. The fractional-share sale is the first place the two notations diverge on a number you can see with the naked eye.

An Empty Posting Isn’t Always a Plug

A careful reader will notice that Income:Fidelity:VOO:PnL is also empty. How is that different from the Fees posting at the beginning?

The difference is whether the number that empty leg fills in has an independent true value. A fee does: it’s on the statement, or it’s exactly zero — there’s an answer that exists outside the ledger, and the plug filled one in before you got to check it against that answer. A capital gain does not: it’s defined as “proceeds minus cost basis,” with no second source that could tell you what it ought to be. Letting Beancount compute that subtraction isn’t absorbing an error — computing it is the entire meaning of that leg.

So the rule isn’t “no empty postings.” It’s: an empty posting is only for a quantity that is by definition derived. An empty leg that fills in a number for a quantity that should have had a true value of its own is a plug.

Two Principles Beyond Stocks

The example is specific, but two principles generalize beyond stocks.

One: when recording any non-cash asset, get the quantity and the total cost right, and leave the unit price to Beancount.

Three variables with two degrees of freedom isn’t a Fidelity quirk, and it isn’t a DRIP quirk. Every non-cash asset has this structure, and all three numbers always get rounded independently. Quantity and amount are the declared ones — quantity is what you actually hold, amount is the money that actually moved; unit price is their quotient, with no independent standing, and usually the one rounded the hardest. So {{}} isn’t a DRIP-specific trick, it’s the default notation for recording any asset with a cost: foreign currency, precious metals, crypto, private equity — anything of the form “so many units × so much each.”

Two: don’t add a posting that doesn’t match reality just to make the books balance, unless you genuinely have to.

When the books won’t balance, the imbalance itself is the data talking. Adding a posting that doesn’t exist in reality to silence it buys you nothing but a tidy-looking ledger. The “have to” case is real — the test is in the previous section: does the number that empty leg fills in have an independent true value of its own?