Stonqs
How the figures work, and what the app does with them

Returns and figures

Your fee report says 5 EUR. Your portfolio paid 25.50.

5 min read

Buy 10 shares for 100 EUR with a 9.90 EUR commission, pay a 5.00 EUR account fee, and receive a 40 EUR dividend with 10.60 EUR withheld. Stonqs's charges report lists 5.00 EUR. The costs behind its cost rate are 25.50 EUR. Both are correct, because they answer different questions.

Take one purchase, one account fee and one dividend. All three are made up.

Date What happened Amount
1 Mar 2024 Bought 10 shares at 100 EUR, broker commission 9.90 EUR 1,000.00 EUR
1 Apr 2024 Account fee 5.00 EUR
1 Jun 2024 Dividend, 10.60 EUR withheld at source 40.00 EUR

The charges report counts 5.00 EUR. The costs the cost rate divides count 25.50 EUR: 14.90 EUR of fees (9.90 commission and 5.00 account fee) and 10.60 EUR of tax. Neither figure is wrong. The first is “what did the broker bill me separately”, the second is “what did holding this cost”. A fee list built from statement lines alone gives you the first.

The reason is the commission. A fee you can see on a statement line is not the same thing as a cost you can see in a report, because the 9.90 EUR does not stay a fee. It becomes part of what the shares cost, and a line that has become cost basis is no longer a charge. This post follows each cost line to the figure that counts it. If you want the table first, it is below.

The commission disappears into the cost basis

A buy commission raises the price of the lot: 10 shares at 100 EUR plus 9.90 EUR cost 1,009.90 EUR, or 100.99 EUR a share. That is how cost basis works, and it is why a later sale shows the right gain. The same document says the commission is therefore not also counted as a standalone fee, because counting it twice would overstate the cost.

So a list of “fees paid” built from the charges report understates what the portfolio cost by every commission you ever paid on a purchase. A commission or a tax on a sale goes the same way, netted from the proceeds: in the test’s own arithmetic, 4 shares sold at 120 EUR with 3 EUR commission and 2 EUR tax give a gain of 480 − 3 − 2 − 402 = 73 EUR.

The cost rate is not a yearly figure

The cost rate is the costs of the period divided by the capital at work during that period. It is not divided by the closing value, and it is not scaled to a year.

Capital at work is the Modified Dietz denominator: the opening value plus each deposit weighted by the share of the period it was invested (dietz_capital). A deposit on the last day of the period weighs nothing.

capital at work = opening value + Σ ( deposit × days remaining / days in period )
cost rate       = costs paid in the period / capital at work

Suppose 1,500 EUR was paid in on 1 March 2024 to fund the purchase above, and the window is 1 January to 31 December 2024 (365 days; 305 of them after the deposit).

capital at work = 1,500 × 305 / 365          = 1,253.42 EUR
fee rate        = 14.90 / 1,253.42           = 1.19 %
tax rate        = 10.60 / 1,253.42           = 0.85 %

I worked these two rates by hand from the formula and a made-up deposit; the charges test has none. The code’s own unit tests check the weighting (1,000 plus 500 paid in on day 11 of a 20-day period is 1,250 at work) and the principle: a 12.50 fee against 1,250 is 1 %. The 1,500 EUR deposit and the two rates above are not output of the app.

The Performance screen shows two tiles, “Fee rate” and “Tax rate”, each with the money behind it underneath. Both are the whole window’s costs over the whole window’s capital. The code divides the window’s costs by the window’s capital and never annualises, so with the same capital at work, three years of costs read three times one year of them. Compare only windows of the same length. With no capital at work the tile shows a dash and not 0 %, because zero would claim the fees were free.

Every cost line and which figure counts it

“Charges” is the Reports screen’s charges report. “Cost rate” means the costs behind the Performance screen’s fee rate and tax rate, which are also the costs column of the calculation sheet.

Cost line Charges report Cost rate Where it lives
Commission on a purchase no yes, as a fee in the lot’s cost basis
Commission or tax on a sale no yes netted from the proceeds
Standalone account or custody fee yes, as a fee yes, as a fee its own operation
Fee refund yes, subtracts yes, subtracts its own operation
Standalone tax operation yes, as a tax yes, as a tax its own operation
Tax refund yes, subtracts yes, subtracts its own operation
Tax withheld, in a column of the dividend row no yes, as a tax on the dividend row
Tax withheld, as a row of its own yes, as a tax yes, as a tax its own operation
Commission billed in another currency no yes, at its own rate in the lot’s cost basis
A fund’s ongoing charge no no not an operation in the export

A refund walks the total back. Add a 5.00 EUR refund of the account fee to the example and the fees behind the cost rate fall from 14.90 EUR to 9.90 EUR, which is exactly the trade commission left over.

Two rows deserve a note.

Withholding tax is on the dividend row or a row of its own, depending on your export. Import reads a tax column (named “tax”, “taxes” or “withholding tax”, among others) into the same row, so a dividend with a tax column carries its withholding. That is the example: the 10.60 EUR sits on the dividend, the charges report never sees it, and the cost rate counts it as tax. A row whose type reads as withholding or tax, such as a “Withholding Tax” cash transaction in an Interactive Brokers Flex export, becomes a standalone tax operation. The charges report then lists it, and the cost rate still counts it once. Check which shape your broker’s export has before comparing the two figures. This describes how the app records what you imported. It says nothing about whether tax can be reclaimed or how your country treats it; that is for your tax authority or a tax adviser.

A commission in another currency is converted at its own rate. A trade settled in EUR can carry a commission billed in USD. Take 12 USD at 0.90 EUR per USD on the trade date: 10.80 EUR. The cost basis becomes 1,000 + 10.80 = 1,010.80 EUR, or 101.08 EUR a share, the euro balance falls by 1,000 EUR and the dollar balance by 12 USD, and the cost rate counts 10.80 EUR once. The rate is fixed on the day it was paid, not re-estimated later. For how a rate gets into a return, see the currency effect.

What neither figure contains

A fund’s ongoing charge, the figure a factsheet calls the TER, is not a line in your broker’s export. Nothing was debited as an operation, so no transaction exists for Stonqs to count, and neither figure includes it. For the ongoing charge, the fund’s own factsheet is the place to look.

The cost rate also says nothing about what a cheaper broker would have charged. It sums what your export recorded and divides it by capital. Whether the export recorded everything is the part to check, and what a broker’s export actually contains is where that goes wrong first.

Rerun it

The numbers in the first table and in the currency example are asserted in core/tests/calc_examples/charges.rs, each case with its arithmetic worked longhand in a comment above it. In a checkout of the Stonqs repository at the release this post was written from:

cargo test -p sq-core --test calc_examples -- charges

All six cases pass there: 5.00 EUR against 14.90 EUR of fees and 10.60 EUR of tax, the refund case, the USD commission, and the USD withholding (a 100 EUR dividend with 15 USD withheld at 0.90 is 13.50 EUR of tax and 86.50 EUR net).

Sources