๐ฅ BRIM Architecture
BRIM (Broker Report Import Manager) is the system responsible for importing transaction data from broker export files (CSV, XLSX, PDF, or another format supported by a plugin). It is designed to be robust, user-friendly, and extensible.
๐ The BRIM Workflow
The import process follows a clear, multi-step wizard designed to give the user full control and visibility.
graph TD
A["1. Upload broker files<br/>(one batch_id per wizard session)"] --> B{"2. Plugin detection<br/>(can_parse of every plugin)"}
B -->|Single-file plugin| C["3. Parse & preview"]
B -->|Report-set plugin| S["3a. Combine the set<br/>(or reuse its combined file)"]
B -.->|No plugin recognises the file| R["Every plugin offered:<br/>the parse guard refuses it (400)"]
S --> C
C --> E["4. Unify assets"]
E --> X["5. Fix flagged rows"]
X --> I{"6. Duplicate check"}
I --> F{"7. Opening-date gate"}
F --> G["8. Review & confirm"]
G -->|Report set with truth points| Z["9. Align with the bank<br/>(gapFix)"]
G --> H["Bulk transaction editor"]
Z --> H
H --> DB[(Database)]
subgraph User Actions
A
E
X
F
G
Z
end
The wizard steps are upload โ select โ analyze โ assets โ fix โ duplicates โ review โ gapFix.
assets, fix, duplicates and gapFix are conditional: each is skipped entirely when it has
nothing to ask (no merged extractions, no flagged rows, no cross-file collisions, no difference with
the bank's truth points), so a clean single-file import is still a three-click flow. gapFix is
reached only from the review's Import, and only for a report set.
๐ช Step-by-step
-
Upload โ The user uploads one or more broker export files. They can be dragged into the broker page or accessed from the Files tab. The wizard sends every file of one session with the same
batch_id: the files uploaded together for one broker form a report set when a report-set plugin reads them. -
Plugin Detection โ BRIM auto-discovers
BRIMProviderclasses from the plugin folders ofbackend/app/services/brim_providers/(<code>/plugin.py, see Plugin layout and translations) and asks every plugin'scan_parse(file_path); the plugins that accept the file are stored in its sidecar ascompatible_plugins, highestdetection_priorityfirst. The wizard pre-selects a plugin per file: the report-set plugin that recognises it, else the broker'sdefault_import_pluginwhen compatible, else the first compatible plugin. The Generic CSV provider (broker_generic_csv,detection_priority0) is not a catch-all: it recognises columns from their header names and aliases (HEADER_MAPPINGS, no manual mapping), and itscan_parseaccepts only a.csvwhose header row names both a date and a type column. A file that no plugin recognises is offered every plugin; parsing it with a single-file plugin re-runs that plugin'scan_parse, whose refusal answers HTTP 400 with the plugin's reason (cannot_parse_reason) and moves the file tofailed/. The file's list of compatible plugins is stored with the app version that detected it (plugins_signature): after an update, the first read of an original detected by another version detects it again and saves the new list, so a plugin added or changed since the upload is offered for older files (combined files are never detected again). See The lifecycle ofcompatible_plugins. -
Parse & Preview โ The selected plugin's
parse()method returns aBRIMParseOutputcontaining standardizedTXCreateItemobjects, warnings, extracted assets, optional field TODOs, and optional per-asset notices. Nothing is saved yet. The user sees a table of parsed transactions before proceeding. A report set is first combined into one file (step 3a, see Report sets), and that combined file is what gets parsed; its parse also returns the bank's truth points (checkpoints,verifications) and the broker'shistory_start. -
Unify assets โ When several files (or several rows) describe the same security under different spellings, the extractions are merged into one group with one elected primary identifier. Only shown when there is something to merge.
-
Fix flagged rows โ The rows the plugin booked but could not fully understand: its
field_todos. The user settles each one โ correct it, split it, or keep it as read โ grouped into three foldable panels by the nature of the question (misread trade / bundled amount / charge with no instrument).This step runs before the duplicate check on purpose. A purchase the plugin could only record as a cash withdrawal would be compared against cash withdrawals: a real duplicate is missed, an imaginary one invented. Correcting afterwards cannot repair that.
-
Duplicate Detection โ Each parsed transaction is compared against existing rows by
(broker_id, date, type, amount, quantity). Matches are flagged with a confidence level so the user can skip or force-import them. Cross-file collisions (the same movement in two files being imported together) get their own arbitration step; database collisions simply arrive deselected at the review. -
Opening-date gate โ In the frontend wizard only, rows whose transaction date is strictly earlier than the broker
opened_atdate are classified with the status keybefore_opening, deselected, and made non-importable. The exact shipped comparison istxDate < info.openedAt, so the opening day itself is valid. There is no backend enum for this status. -
Review & Confirm โ The user reviews the final list. Importing is blocked while any selected row still points at an unresolved asset. On Import, the wizard hands the selected rows to the bulk transaction editor (
TransactionBulkModal), whose save (POST /api/v1/transactions/commit) goes through the standardTransactionService. -
Align with the bank (
gapFix) โ Report sets only. Before the hand-off, the wizard sends the truth points toPOST /api/v1/brokers/import/gap-fix(backend/app/services/brim_gap_fix.py). For each checkpoint, in date order, the backend compares the cash balance and positions the bank states with what LibreFolio will know at that date โ the saved transactions, minus the ones the bulk editor is deleting, plus its unsaved rows, the wizard's selection and the corrections of the earlier checkpoints โ and proposes only the difference, as ordinary transactions taggedgap_fix. Verifications are compared, never corrected, and the endpoint writes nothing. The step opens only when there is something to show (a correction, a failed verification, an error); every correction starts selected, and the ones the user keeps join the review's rows in the editor.
๐งบ Report sets
Some banks split one account across several exports โ Danske Bank (broker_danske_bank/plugin.py) is read
from a custody export and a cash export. A report-set plugin declares the export roles it combines
(report_roles; is_report_set_plugin is true when the list is not empty), and its exports are
imported as one set:
- Membership โ the set is the original files of one upload batch (
batch_id) for one broker that the plugin can read (collect_membersinbackend/app/services/brim_report_sets.py). The plugin never parses an original on its own: that request answers 422set_required, and the file is not moved tofailed/. - Preview โ
POST /api/v1/brokers/import/sets/previewreads the members through the plugin (detect_role,describe_member,describe_set) and reports roles, missing roles, coverage, segments, gaps and the broker history already in LibreFolio. It writes nothing. - Combine โ
POST /api/v1/brokers/import/sets/combinerefuses an incomplete set (422 with the missing roles), then reuses the combined file already built from exactly these members by this plugin version, or calls the plugin's purecombine()and saves the result next to the originals as an ordinary BRIM file withkind = "combined". The analysis step then parses that combined file. - History and gap-fix โ the parse of a combined file applies the broker history already in
LibreFolio (
apply_history): checkpoints older than the eve of the history start are dropped, because what precedes the history is already represented; on a later import, a checkpoint from the history start on closes a gap (The history start (H0)). The review then continues to thegapFixstep described above.
The plugin contract and the API payloads are in Multi-report plugins (report sets).
๐ File Lifecycle
backend/data/{prod|test}/broker_reports/
โโโ .locks/ โ broker_<id>.lock โ the cross-process metadata lock
โโโ uploaded/ โ new files awaiting processing
โ โโโ broker_<id>/ โ {file_id}.{ext} + {file_id}.json sidecar (same layout under parsed/ and failed/)
โโโ parsed/ โ successfully parsed files, ready for review/import
โโโ failed/ โ files that failed parsing or were rejected
Each file is stored with a companion .json metadata sidecar recording status, original filename, and any errors encountered. A file keeps its broker_<id>/ folder for its whole life: a move changes only the status folder.
๐๏ธ Sidecar fields
| Field | Written by | Meaning |
|---|---|---|
status, uploaded_at, processed_at, error_message |
Upload, moves | Where the file is in its lifecycle |
compatible_plugins, plugins_signature |
Upload; re-detection on read | The plugins whose can_parse accepted the file, and the app version that detected them (detection_signature()). A combined file lists only the plugin that built it and is never detected again |
batch_id |
Upload | The upload batch: the files of one wizard session, which a report-set plugin reads as one set |
kind |
Combine | combined for a file built by a report-set plugin; absent means original |
derived_from |
Combine | On a combined file: the originals it was built from (file_id, role, filename; deleted: true once the original is deleted) |
combined_into |
Combine | On an original: the combined files built from it |
combine_plugin_code, combine_plugin_version, combine_summary |
Combine | The plugin version that built a combined file, and its summary |
members_key |
Combine | SHA-256 of the sorted member ids: the order-insensitive identity of a set, used to find a reusable combined file |
last_parse_result, parsed_plugin_code, parsed_plugin_version |
Parse | The cached preview; a plugin version bump since then marks it stale (parse_is_stale) |
A combined file keeps working when one of its originals is deleted, because it carries the values it
was built from; deleting a combined file removes it from its originals' combined_into.
โก Concurrency & off-loading
Uploading and parsing are the two hottest paths in BRIM, and both used to do blocking
synchronous work directly inside async def handlers. While one file parsed, the whole
application โ prices, FX, navigation โ stood still, and concurrent requests from the browser were
serialised by the server no matter how parallel the client was.
| Layer | What runs where |
|---|---|
| Upload | save_uploaded_file runs in asyncio.to_thread. A thread is enough: the transfer itself is the OS's work; we only write the file and re-read it to sniff compatible plugins |
| Plugin discovery | auto_discover() (backend/app/services/provider_registry.py) imports the plugin modules once, under the registry's own RLock, and marks discovery done only after every module has run. The first uploads after a start run on worker threads: one that arrives mid-discovery waits for the full catalogue instead of detecting against a half-filled one |
| Parse | backend/app/services/brim_parse_pool.py โ a lazy ProcessPoolExecutor, min(cpu_count, 4) workers. Parsing is CPU work: a thread would leave it serialised by the GIL |
| Sidecar writes | Every write of a broker's sidecars holds that broker's lock (broker_metadata_lock: a re-entrant thread lock plus an advisory flock on broker_reports/.locks/broker_<id>.lock). combine_set holds it, in a worker thread, from the reuse check to the save of the combined file: analysing the same set twice โ a double click, two tabs โ reuses the first combined file instead of building a second one. See The broker metadata lock |
| Candidate search | search_asset_candidates_bulk issues one query per file instead of up to five per extracted asset, then applies the same five priorities in memory |
| Frontend | lib/utils/core/requestConcurrency.ts โ mapWithConcurrency bounds the upload and parse loops instead of awaiting one file at a time |
๐งต Why forkserver/spawn, never fork
This process owns an event loop and a thread pool. Forking them is the classic way to produce
deadlocks that only appear in production. The pool prefers forkserver and falls back to
spawn; parse_file is DB-free (filesystem + registry only), so it is safe to run in a child.
Two decisions that look like bugs and are not
The fallback is permanent. If the pool cannot start โ or a worker dies taking a segfaulting C parser with it โ BRIM falls back to a thread for the rest of the process lifetime. A degraded parse is a slow parse; a failed one is a lost import. And an environment that cannot spawn workers will not start being able to halfway through an import: retrying per file would pay the start-up cost again for nothing.
A parser error is not a broken pool. A failure at submit is always ours; while
awaiting, only BrokenProcessPool counts. Everything else โ ValueError for the wrong
plugin, FileNotFoundError, BRIMParseError โ propagates intact. Treating OSError as pool
trouble would disable the pool forever on the first missing file, and turn the one useful
message the user gets ("this layout is not supported") into a spinner.
% in bond names
The bulk search reproduces in Python predicates that were SQL, and LIKE reads % as a
wildcard. Security names are full of them (BTP Valore 3,35%), so a naive Python in
would be stricter than the SQL it replaces and would drop candidates silently.
_like_to_regex translates %โ.* and _โ. with everything else escaped. It is the most
likely way the two paths can diverge, and therefore the central case of the equivalence test.
The pool is shut down from the FastAPI lifespan, next to the other pools.
๐ The browser connection limit
No web API exposes the per-host connection cap. navigator.connection describes link quality
(Chromium only); navigator.hardwareConcurrency counts CPU cores. HTTP/1.1 settles by convention
on 6 per host; HTTP/2 multiplexes over one connection. So the frontend limit is a declared
heuristic, not a measurement:
One slot is always left free: an import that starves the rest of the application of connections looks like a freeze, and the user has no way to tell "busy" from "broken".
๐ Deduplication Logic
Before final import, BRIM compares each parsed transaction against the database on:
| Field | Used in match |
|---|---|
broker_id |
โ |
date |
โ |
type |
โ
โ a DEPOSIT or WITHDRAWAL also meets saved transfer and exchange legs (below) |
quantity |
โ |
amount |
โ |
description |
Used for confidence upgrade |
asset_id |
Used for confidence upgrade |
Confidence levels:
| Level | Meaning |
|---|---|
POSSIBLE |
Key fields match |
LIKELY |
Key fields + description match |
POSSIBLE_WITH_ASSET |
Key fields + asset resolved |
LIKELY_WITH_ASSET |
Key fields + description + asset all match |
๐ Merged transfers and exchanges
An incoming DEPOSIT or WITHDRAWAL is also compared with the saved legs of CASH_TRANSFER and
FX_CONVERSION pairs (_MERGED_LEG_TYPES, read by detect_tx_duplicates in
backend/app/services/brim_provider.py): same broker, date and quantity, same amount โ sign
included โ and currency. A deposit thus meets the leg that received the money, a withdrawal the
leg that sent it.
On such a merged leg, the description matches when it contains the incoming one (both
normalised by _description_key: whitespace removed, upper case), since merging a pair joins the
descriptions of its two sides with a newline. The row is then a LIKELY duplicate, so it arrives
unticked.
Why: an export "since the last one" โ the Scalable exporter's, for one โ starts on the last day it
covered, so it delivers that day again. Matched on its own type only, a transfer already merged
would come back as new; matched without the containment, as a POSSIBLE duplicate, which is ticked
by default. Either way, its cash would count twice.
๐งฉ Plugin System
Every BRIM plugin is a BRIMProvider subclass registered via the provider registry. Key contract:
class MyBrokerProvider(BRIMProvider):
@property
def provider_code(self) -> str:
return "broker_mybroker"
@property
def detection_priority(self) -> int:
return 100 # Higher = tried first
def can_parse(self, file_path) -> bool:
"""Return True when this file belongs to this broker."""
...
def parse(self, file_path, broker_id) -> BRIMParseOutput:
"""Convert broker rows into TXCreateItem objects inside BRIMParseOutput."""
...
๐ค What a parse returns
BRIMParseOutput has four channels, and choosing between them is what shapes the wizard:
| Channel | Meaning | Rendered as |
|---|---|---|
transactions |
rows the plugin read | review table |
validation_issues |
rows the schema refused | parse-detail modal, red |
warnings (BRIMNotice) |
a statement about the file โ info (blue) or warning (amber) |
parse-detail + confirmation modal |
field_todos (BRIMFieldTodo) |
a field of one accepted row is a guess | the Fix flagged rows step |
extracted_assets[].notices (BRIMAssetNotice) |
something about the instrument | amber banner in the asset-create modal |
Notices and todos can both carry BRIMEvidence โ the source rows as a small table, with
1-based line numbers that render a jump into the file preview. BRIMFieldTodo.context also
carries a small set of keys the correction step reads directly (split_hint,
split_suggestions, compare_nominal, โฆ).
The full contract, with worked examples from broker_credit_agricole/plugin.py, is in
Talking to the import wizard.
Plugins are auto-discovered once per process, the first time the registry is used (see Plugin discovery under Concurrency & off-loading above). See the BRIM Plugin Guide for a complete walkthrough of creating a new plugin.
๐๏ธ Plugin layout and translations
Each plugin is a folder of backend/app/services/brim_providers/, named after its provider_code:
backend/app/services/brim_providers/
โโโ __init__.py โ the family's docstring
โโโ _brim_io.py, _brim_output.py, _scalable.py โ helpers shared by the plugins (discovery skips them)
โโโ static/ โ icons, served under /api/v1/uploads/plugin/brim/โฆ
โโโ sample_reports/ โ every plugin's sample exports, read by the tests
โโโ broker_directa/
โโโ __init__.py โ its docstring and nothing else
โโโ plugin.py โ the code, loaded as backend.app.services.brim_providers.broker_directa.plugin
โโโ i18n/ โ en.json, it.json, fr.json, es.json: brimPlugins.broker_directa.name and .description
The package re-exports nothing: code and tests import a plugin's names from its plugin module,
and patch them there. The UI shows a plugin's name and description in the user's language, from
brimPlugins.<code>.name and .description, and falls back to its English provider_name and
description when a key is missing. Those two keys live in the plugin's own i18n/ fragment,
which the frontend build merges into the translation catalogue;
./dev.py test utils gate-plugin-i18n-ownership keeps them there, and every plugin's code equal
to its folder's name.
A plugin mounted into a container with a docker-compose volume therefore has its code but not its
translations, and shows the English fallback (post-1.2 backlog, U-B1). The wizard's wording of a
plugin's notices and refusals (importWizard.brimNotice.<code>, importWizard.brimEvidence.<code>,
importWizard.parseRefusal.<code>) belongs to no plugin: it lives in the importWizard subsystem.
How to write the folder and its keys: BRIM Plugin Guide โ Auto-Discovery and Register the name and description.
๐๏ธ Plugin notes
What the user pages of the import plugins leave out. The richest plugins have a page of their own: Crรฉdit Agricole, Danske Bank and Scalable Capital.
๐ณ๐ฑ DEGIRO
broker_degiro/plugin.py reads the Account Statement CSV in any language. Its message language, its
linked FX_CONVERSION pairs and the orders list it claims without importing are in the
BRIM Plugin Guide; this is the
layout behind them.
- Twelve columns, read by position, the same in every language: date ยท time ยท value date ยท product ยท ISIN ยท description ยท FX rate ยท currency ยท amount (unnamed) ยท balance currency ยท balance (unnamed, ignored) ยท Order Id.
- Detection (
_layout, behindcan_parseandcannot_parse_reason): the first data row has aDD-MM-YYYYdate and anHH:MMtime. With the ninth and eleventh header cells unnamed, 12 cells (13 when the last is empty) andISINfifth is the statement; 16 cells or more andISINfourth is the orders list. - Continuation lines. A line with no date and no time continues the row above: DEGIRO wraps a long description or Order Id, and its cells are appended verbatim.
- Numbers. A value's decimal mark is read from the value when it is certain. Only one separator
followed by exactly three digits (
1.000,49,785) is ambiguous: it takes the mark voted by the file's certain amounts and balances. - Classification, first match wins: no amount โ skipped; a paired FX leg โ
FX_CONVERSION; an unreadable amount or date, or no currency โdegiro_unknown_rows; an FX rate without a partner โdegiro_unpaired_fx; a trade (ISIN, Order Id and<verb> <quantity> โฆ@<price>, or the old Germanzu je) โBUYorSELLby sign (degiro_quantity_unreadablewhen its quantity cannot be read), the same after aPREFIX:โ corporate action (degiro_not_imported); FX words without a partner โdegiro_unpaired_fx; another negative row of an order โFEE, orTAXfor a transaction tax; a zero amount โdegiro_not_importedwith an ISIN, skipped without; then the multilingual word table (_WORD_RULES), all languages on every row. - Signs are checked, never flipped: a dividend, interest or deposit must be positive, a tax,
fee or withdrawal negative, else
degiro_unexpected_sign; negative interest is aFEE. Trades, dividends, taxes and fees get one fake asset per ISIN (the product name when there is none). - Notices: one
warningper code, with its rows as evidence under the file's own header (an unnamed column takes the previous name plus#). Tests:./dev.py test external brim-degiro.
๐ฎ๐น Intesa Sanpaolo
broker_intesa/plugin.py reads two exports, CSV or XLSX (_brim_io.read_rows), told apart by content:
- Portfolio snapshot (patrimonio), when the first 40 rows contain
controvalore di carico fiscale. A row withDescrizioneandISINcells opens a section (Fondi e Sicav, Titoli di stato); a row starting withTotale,LiquiditรorSaldocloses it. Each holding with an ISIN and a positiveNumero QuoteorQuantitรbecomes a cashlessADJUSTMENTwithcost_basis_mode="manual"and a per-unitcost_basis_overridein EUR: the totalControvalore di carico fiscale โฌdivided by the quantity (no override, and a warning, when the cost is missing). The first non-zero figure of theSaldo totaleorSaldo disponibilitรrow becomes one EURDEPOSIT. All are dated the latestData Ultima QuotaorData-Oraof the holdings (today, with a warning, when there is none) and taggedsnapshot-seed. - Movements list, when a header row holds
Operazione,DettagliandImporto; it readsData,Operazione,Dettagli,Valuta(the currency, EUR when empty) andImporto.Operazionemaps by lower-cased substring:cedolโINTEREST,dividendโDIVIDEND,commissionโFEE,ritenut,impostaorbolloโTAX. Any other row is skipped with a warning, so noBUYorSELLis ever produced. Income is linked to an asset named fromDettagli(theCedole Su <CCY> <nominal>prefix removed) and keyed by that name, with no ISIN; fees and taxes carry no asset. Only this layout callsattach_maturity_notices. - Warnings are plain Italian strings (coerced to
warningnotices with codelegacy), and there are no field todos. Holding back the movements that predate the snapshot is the wizard's opening-date gate (step 7 above), not the plugin's.
๐ eToro
broker_etoro/plugin.py reads the Account Activity sheet of the eToro account statement, saved as CSV:
eToro itself downloads the statement as XLS.
- Detection: a
.csvwhose first line containsdate,type,details,amount,unitsandrealized equity, in any case. Columns are read by name (csv.DictReader). - Type, by lower-cased substring:
open positionโBUY,position closedโSELL,dividendโDIVIDEND,interest paymentโINTEREST,withdraw requestโWITHDRAWAL,depositโDEPOSIT.withdraw fee,withdrawal conversion feeandconversion feeare aFEEwhen the amount is not zero and skipped silently when it is;overnight fee,overnight refundandsdrtare always skipped silently; any other type โ warning. - Asset and currency come from
Details(SYMBOL/CCY, such asNKE/USD): the symbol is the fake asset'sextracted_symbol(no ISIN, no name), and the three letters after the slash are the currency of the row'sAmount. Without that shape, the whole text is the symbol and the currency is USD.BUY,SELLandDIVIDENDwithout a symbol are skipped with a warning. - Known gap: eToro states
Amountin the account currency. In the sample, theAmountof eachKER/EURdividend equals itsRealized Equity Change, yet the row is tagged EUR; the user page asks the user to check such rows. - Quantity from
Units(-reads as 0, and aFEEalways has 0); signs are set by type (SELLquantity,BUYandFEEamounts negative). Numbers: parentheses mean negative; with both.and,the last one is the decimal mark; a lone,is decimal when at most two digits follow it. Dates:DD/MM/YYYY HH:MM:SSorDD/MM/YYYY, else a warning and the row is skipped. Warnings are plain English strings.
๐๏ธ Fineco
broker_fineco/plugin.py reads the Movimenti Dossier Titoli report, CSV only.
- Detection: within the first 15 lines,
operazione,data valuta,isin,controvaloreand thedossiermetadata marker. The header is the first row whose first cell isOperazione; the metadata above it (Dossier:,Intestatario:,Titoli e operazioni) is skipped. - Columns by position: 0
Operazione(trade date) ยท 1Data valutaยท 2Descrizioneยท 3Titoloยท 4Isinยท 5Segnoยท 6Quantitaยท 7Divisaยท 8Prezzoยท 9Cambio(ignored) ยท 10Controvalore. A header longer than 11 cells is the layout with commissions: columns 11 to 14 are summed (absolute values) into oneFEEin EUR (FEE_CURRENCY, tagfee) when the sum is positive. A data row shorter than 11 cells is skipped with a warning. - Type, by lower-cased substring of
Descrizione:compravenditawithSegnoAorVโBUYorSELL(anotherSegnoโ warning),dividendoโDIVIDEND,cedolโINTEREST,rimborsoโSELL,aumento capitaleโ cashlessADJUSTMENT; anything else โ warning. - Date:
Data valuta, elseOperazione(DD/MM/YYYY). Currency:Divisa, EUR when empty. Numbers: a value containing,is Italian (.thousands), otherwise a dot decimal. - Above par: a
rimborsoSELLwhoseTitolopasses_brim_io.looks_like_bond(BTP,BOT,CCT,CTZ,BUND,OAT,OBBLIG,CCTEU,BOND), withPrezzoabove 100, goes throughmodel_bond_maturity(ctv=Controvalore, price=Prezzo, held_qty=|Quantita|): aSELLof the par principal and anINTERESTfor the surplus (tagmaturity_premium). - Assets are keyed by
Isin; a row without one gets an asset of its own (UNKNOWN_ROW_<n>). Every transaction,FEEincluded, carries the row's asset. - Warnings are plain English strings, although the module docstring says Italian; only the
descriptions of the generated legs (
Commissioni: โฆ,Rimborso a scadenza โ premio/rivalutazione oltre la pari: โฆ) are Italian.
๐ Related
- Crรฉdit Agricole Importer โ Layouts, causale registry, account trades and maturities
- Generic CSV Provider โ Format reference + sign conventions + LLM tip
- Providers List โ All currently supported brokers
- BRIM Plugin Guide โ How to write a new broker plugin
- Multi-report plugins (report sets) โ The report-set contract, its API and the gap-fix