Skip to main content
Version: v4

Events mode

Instead of manually maintaining aggregated values, events mode lets you import your transaction history. SuiviBourse reads a list of events (BUY, SELL, GRANT, DIVIDEND) and automatically computes:

  • Weighted average cost price
  • Purchased and owned quantities
  • Accumulated dividends
  • Transaction fees

Events mode is also what unlocks historical backfill: because SuiviBourse knows when each transaction happened, it can reconstruct the value of your portfolio over time.

Setup

Create a settings.yaml file and an events/ directory:

~/.config/SuiviBourse/
├── settings.yaml
└── events/
├── 2023.csv
├── 2024.csv
└── broker-export.xlsx
settings.yaml
mode: events
events:
source: ~/.config/SuiviBourse/events/
watch: true # Optional: reload immediately when files change
SettingRequiredDescription
modeYesMust be events
events.sourceYesPath to a single file or a directory of event files
events.watchNoWhen true, a file watcher reloads events immediately on change (default false)
tip

You can also select events mode with the SB_CONFIG_MODE=events environment variable — it takes priority over settings.yaml. See mode selection.

File format

Both CSV and XLSX files are supported. Every file must contain a header row; only date and event_type are structurally required. Which of the other columns apply depends on the event type (a share BUY needs symbol/name/ quantity/unit_price; a cash DEPOSIT needs account/amount).

events/2024.csv
date,event_type,symbol,name,quantity,unit_price,fee,amount,notes
2024-01-15,BUY,AAPL,Apple Inc,10,150.00,2.50,,Initial purchase
2024-02-01,BUY,MSFT,Microsoft,5,380.00,2.50,,
2024-03-01,DIVIDEND,AAPL,Apple Inc,,,,8.50,Q1 2024
2024-06-01,GRANT,AAPL,Apple Inc,1,,,,Stock split bonus
2024-09-15,SELL,AAPL,Apple Inc,3,180.00,2.00,,Partial sale

Columns

ColumnRequiredDescription
dateYesISO format YYYY-MM-DD
event_typeYesBUY, SELL, GRANT, DIVIDEND, DEPOSIT or WITHDRAWAL (case-insensitive)
symbolFor share eventsYahoo! Finance ticker (e.g. AAPL, MSFT); forbidden on cash events
nameFor share eventsDisplay name for the share; forbidden on cash events
quantityFor BUY / SELL / GRANTNumber of shares
unit_priceFor BUY / SELLPrice per share
feeOptionalTransaction fee (must be >= 0)
amountFor DIVIDEND / DEPOSIT / WITHDRAWALAmount received (dividend) or moved (cash)
notesOptionalFree-text comment (ignored by the app)
accountFor cash events, and for any event when accounts are declaredAccount id this event belongs to

date and event_type must always be present in the header. Empty cells are allowed for the columns that don't apply to a given event type, and cash-only files may omit the share columns entirely.

Event types

TypeRequired fieldsEffect on the portfolio
BUYquantity > 0, unit_price > 0+purchased quantity, +owned quantity, recomputes the weighted average cost price, +fee, −cash
SELLquantity > 0, unit_price > 0−owned quantity, +fee, +cash (proceeds)
GRANTquantity > 0+owned quantity only (free shares); cash-neutral
DIVIDENDamount > 0+received dividends, +cash
DEPOSITaccount, amount > 0+cash (external contribution)
WITHDRAWALaccount, amount > 0−cash (external withdrawal)

If any event fails validation, the whole ingestion is rejected and the previous valid configuration is kept — see Error resilience.

Aggregation rules

BUY — weighted average cost price

When you buy shares at different prices, SuiviBourse computes the weighted average cost price:

new_cost_price = (old_qty × old_price + new_qty × new_price) / total_qty

Example

  • Buy 10 shares at $150 → cost_price = 150
  • Buy 5 shares at $175 → cost_price = (10×150 + 5×175) / 15 = $158.33

A BUY increases both purchase.quantity and estate.quantity, and adds the fee to the running total.

SELL — reduces owned quantity

  • Decreases estate.quantity (the quantity you currently own).
  • Does not change purchase.quantity or the cost price.
  • The sale fee is added to the total fees.
  • The sale unit_price is recorded on the event but realized gains are not tracked.
Overselling is rejected

You cannot sell more shares than you currently own at that point in time. Doing so raises an aggregation error and the previous valid configuration is kept.

GRANT — free shares

  • Only increases estate.quantity.
  • Does not affect purchase.quantity or the cost price.
  • Use it for stock splits, employee grants and bonus shares.

DIVIDEND — received dividends

  • Increases estate.received_dividend by amount.
  • Also credits the account's cash by amount − fee.

Cash events & the cash ledger

DEPOSIT and WITHDRAWAL track the money that enters and leaves an account. Combined with the buy/sell/dividend flows, SuiviBourse keeps a cash balance per account, so you can see how much you contributed, how much it is worth, and how much is sitting idle as cash.

events/2024.csv
date,event_type,amount,fee,account,notes
2024-01-01,DEPOSIT,10000,,PEA,Initial funding
2024-06-01,WITHDRAWAL,500,1.5,PEA,Cash out

Rules

  • Required: date, account, amount (> 0 — the direction is carried by the event type, never by the sign). fee is optional (>= 0).
  • Forbidden on cash events: symbol, name, quantity, unit_price — providing any of them is a validation error.
  • Cash is siloed per account: a transfer between accounts is expressed as a WITHDRAWAL + DEPOSIT pair.

The cash ledger starts at 0.00 per account. The fee always makes the cash worse:

EventEffect on cash
DEPOSIT+ (amount − fee)
WITHDRAWAL− (amount + fee)
BUY− (quantity × unit_price + fee)
SELL+ (quantity × unit_price − fee)
DIVIDEND+ (amount − fee)
GRANTnone
SELL now credits cash

Previously the proceeds of a sale were not tracked. From now on, a SELL credits the account's cash with quantity × unit_price − fee.

Permissive policy — a negative cash balance is allowed: it only logs a non-blocking warning. This is what keeps a user who adds accounts without rewriting their whole DEPOSIT history running (they will see negative balances, not an app that refuses to start). Selling more shares than you own stays blocking, as before.

These values feed the Accounts dashboard (cash_balance, holdings_value, total_value, net_contributed), written to the account_metrics measurement only for opt-in users (accounts declared in settings.yaml).

Performance: XIRR & TWR

On top of the cash & value series, SuiviBourse computes money-weighted performance — answering "am I making money, given when I put it in?"

External vs internal flows — this is the whole definition of the metric:

  • External (they are the contribution, they do not count as performance): DEPOSIT, WITHDRAWAL, and GRANT (in-kind, valued at the day's price).
  • Internal (they do count as performance): BUY, SELL, DIVIDEND, and every fee.

XIRR (xirr) — the annualized money-weighted rate of return, solved in Python by bisection (no SQL, no new dependency). It needs at least one external flow; an account without any external flow produces no xirr at all (the field is absent, not 0).

TWR (twr_index) — the time-weighted return as a base-100 index, which strips out the effect of when you contributed. Its base 100 re-anchors as backfill fills earlier history.

Absolute gain (gain_absolu) — current value − contributions. It is the guard for a young XIRR: over a few weeks an annualized rate gives wild numbers, so the two are always shown side by side.

Why the global performance can be missing

The global portfolio_totals series is written only when all accounts share the same currency — pooling different currencies would need FX conversion, which is out of scope. So the dashboard's global block is empty both when no account is configured and when accounts use different currencies.

Accounts (opt-in)

By default every position is tracked in a single implicit account named default. If you hold the same share across several accounts (e.g. a PEA and a CTO, or several brokers), you can declare them as first-class accounts so each position is tracked — and priced — separately.

This is an opt-in feature: it is enabled only by adding an accounts: block to settings.yaml. Without it, nothing changes.

Declaring accounts

settings.yaml
mode: events
events:
source: ~/.config/SuiviBourse/events/
accounts:
- id: PEA # referenced by the `account` column in your event files
type: PEA # free-form label (PEA, CTO, 401k, …)
currency: EUR
label: Mon PEA # optional display name (defaults to `id`)
- id: CTO
type: CTO
currency: EUR
FieldRequiredDescription
idYesUnique identifier, referenced by the account column of your events
typeYesFree-form account type (PEA, CTO, 401k, …)
currencyYesAccount currency (EUR, USD, …)
labelNoDisplay name (defaults to id)

The account column

Once accounts are declared, every event line must carry an account column whose value matches a declared id:

events/2024.csv
date,event_type,symbol,name,quantity,unit_price,fee,amount,notes,account
2024-01-15,BUY,AAPL,Apple Inc,10,150.00,2.50,,,PEA
2024-01-16,BUY,AAPL,Apple Inc,5,160.00,1.00,,,CTO
  • A line without an account value is a validation error.
  • An account value that does not match any declared id is a validation error.
  • With the example above, AAPL is tracked as two independent positions — 10 shares in the PEA and 5 in the CTO — each with its own weighted average cost price.
What if I don't declare accounts?

The account column is simply ignored and every position falls under the implicit default account. Manual mode is unaffected as well.

How accounts appear downstream

  • InfluxDB — every point of portfolio_metrics written from v4.1 onwards carries an account tag (default when accounts aren't declared). Points written before the upgrade have no tag — see Upgrading to accounts.
  • Prometheus — the sb_* gauges gain an account label, so the same symbol held in two accounts no longer overwrites itself.
  • Grafana (dashboard 1) — the "patrimoine total" dashboard aggregates across all accounts: Owned quantity, Total Investment, etc. sum the accounts, and Cost price is their quantity-weighted average. It stays a single "total wealth" view and does not add an account selector.
Existing data

Points written before this feature existed have no account tag; InfluxDB exposes them as NULL. The dashboard queries read them with COALESCE(account, 'default'), so your historical data stays visible and is aggregated correctly. No migration or history rewrite is performed.

Key behaviours

Event ordering

Events are sorted by date before processing, regardless of:

  • their order within a file, and
  • which file they come from.

You can add past events at any time — they are processed in the correct chronological order.

Multi-file support

All .csv and .xlsx files in the events directory are loaded and merged. Organize them however you like:

  • by year (2023.csv, 2024.csv)
  • by broker (degiro.csv, interactive-brokers.xlsx)
  • by account (pea.csv, cto.csv)

Caching

Ingestion uses each file's modification time (mtime) to detect changes:

  • If no file changed → the cached, already-aggregated data is reused.
  • If any file changed → a full reload and re-aggregation happens.

Error resilience

If ingestion fails (invalid data, file error, overselling, …):

  • the previous valid configuration is kept,
  • price scraping keeps running normally, and
  • the error is logged for debugging.
tip

Fix your event files at your own pace — the app won't crash and will pick up your corrections on the next ingestion cycle (or immediately if watch: true).