babilonia.tools.cashflow#

Aggregate and analyze cash flow from standardized bank statements.

This script scans a target directory for standardized bank statement files (T1 canonical format), groups them by year, concatenates daily records, and produces daily, monthly, and annual cash flow reports using the CashFlow analysis engine.

For each year found, the script:

  • Loads all matching T1 statement files.

  • Concatenates and reorders columns into a canonical layout.

  • Buckets rows by their actual transaction year (Data), not by the folder/file they were physically stored in.

  • Writes a consolidated daily cash flow CSV.

  • Computes monthly and annual cash flow summaries.

  • Writes monthly and annual CSV reports.

  • Displays formatted previews in the terminal.

After yearly processing, the script merges all yearly outputs into global daily, monthly, and annual datasets for the full system.

Processing can be restricted to a single year or applied to all available years. Terminal output is structured to facilitate inspection and logging.

Note

Some statement types do not align with calendar-month/year boundaries. Nubank credit-card invoices, for example, are filed under the year of the invoice, but a given invoice’s transactions can legitimately spill into December of the previous year (the invoice closes a few days into the new month). Because of this, file discovery is folder/invoice based, but reporting buckets are always derived from the actual Data column, never from the folder a file happened to live in. All T1 files are loaded up front (regardless of --year) and rows are bucketed by their real transaction year afterward, so spillover transactions land in the correct year’s report instead of being split or duplicated across two years.

Script Examples#

The script is intended for command-line execution.

Minimal PowerShell example (Windows)

Save as run_cashflow.ps1 and execute from PowerShell.

# ! Warning -- change paths and parameters

# Paths
$REPO   = "C:\path\to\repo"
$SCRIPT = "$REPO\babilonia\tools\cashflow.py"
$DATA   = "C:\data\bank_statements"

# Parameters
$TYPE = "bb-cc"
$YEAR = 2024

# Run script
python $SCRIPT `
    --folder $DATA `
    --type $TYPE `
    --year $YEAR
Minimal shell example (Linux)

Save as run_cashflow.sh and execute from a terminal.

#!/usr/bin/env bash

# ! Warning -- change paths and parameters

# Paths
REPO="/path/to/repo"
SCRIPT="$REPO/babilonia/tools/cashflow.py"
DATA="/data/bank_statements"

# Parameters
TYPE="bb-cc"
YEAR=2024

# Run script
python "$SCRIPT" --folder "$DATA" --type "$TYPE" --year "$YEAR"

Expected Folder Structure#

The input data is expected to follow a simple hierarchical layout:

bb/                                 # Bank
└── cc/                             # Bank account
    ├── cashflow.json                  # optional, see Plots below
    ├── 2022/
    │   ├── EXTRATO_BB_CC_2022-01_T1.csv
    │   └── EXTRATO_BB_CC_2022-02_T1.csv
    ├── 2023/
    │   └── EXTRATO_BB_CC_2023-01_T1.csv
    └── 2024/
        ├── EXTRATO_BB_CC_2024-01_T1.csv
        └── EXTRATO_BB_CC_2024-02_T1.csv

Each *_T1.csv file represents a canonical (Tier 1) standardized bank statement.

During execution, the script generates consolidated and aggregated outputs:

bb/                                 # Bank
└── cc/                             # Bank account
    ├── CAIXA_BB_CC_MENSAL_2023.jpg
    ├── CAIXA_BB_CC_MENSAL_2024.jpg
    ├── CAIXA_BB_CC_MENSAL_LT.jpg
    └── 2024/
        ├── CAIXA_BB_CC_2024_DIARIO.csv
        ├── CAIXA_BB_CC_2024_MENSAL.csv
        ├── CAIXA_BB_CC_2024_ANUAL.csv
        └── CAIXA_BB_CC_2024_REPORT.txt

Plots are written directly under the bank/account folder (not inside the per-year subfolder) so every year’s .jpg files sit side by side for easy comparison – they share a common y-axis scale computed once over the whole series (see Plots below). The year comes last in the plot filename (unlike the per-year CSV/report names) so files across years sort next to each other.

Global merged outputs (all years):

bb/                                 # Bank
└── cc/                             # Bank account
    ├── CAIXA_BB_CC_O_DIARIO.csv
    ├── CAIXA_BB_CC_O_MENSAL.csv
    └── CAIXA_BB_CC_O_ANUAL.csv

Data Levels#

  • Tier 1 (T1): Canonical, standardized statement files produced by the parsing stage.

  • Daily: Concatenated transaction-level data, bucketed by the real transaction year (from Data), not by source file/folder.

  • Monthly: Aggregated per month with cumulative fields. The on-screen preview appends SUBTOTAL and MEDIA (subtotal / 12) rows for Entradas/Saidas/Fluxo and their transaction counts; cumulative columns are left blank there, since summing a running total isn’t meaningful. These two rows are display-only and not written to the exported CSV.

  • Annual: Aggregated per year with cumulative fields.

  • Report: A .txt file per year, a verbatim copy of everything printed to the terminal for that year (header, daily/monthly/annual previews), produced by mirroring stdout while that year is processed so it can never drift from what was shown on screen. Table headers follow the language setting below.

  • Plots: A .jpg bar chart per year for the monthly scale, written to the bank/account folder (not the per-year subfolder). Entradas are drawn above zero, Saidas below zero, both anchored at the same baseline, and each bar is annotated with its value, compacted to K/M for large amounts. The y-axis limits are computed once from the whole series (every year available, 1.2x the absolute max flow) and reused for every year’s plot, so all years share the same scale and are directly comparable. A second panel to the right shows the same series as a horizontal histogram sharing that y-axis – a sideways projection of the distribution, binned by a fixed currency interval, inflows mirroring outflows across zero. Figure size, bar colors/width, histogram panel size/bins, background color and font sizes come from the config file below. Alongside the per-year plots, one more covers the full history: every year’s months in one chart (..._MENSAL_LT.jpg, “LT” for long-term), using the same y-scale, with annotations dropped (too many bars across several years) and only a January tick per year, labeled with that year’s number – no per-month letters. Its histogram bin size always comes from Scott’s rule on its own (larger) series, regardless of any fixed hist_bin_size set in cashflow.json for the per-year plots – the LT series is a different sample and keeps its own bin width. A daily plot is not produced for now – it added little over the monthly one – though the underlying babilonia.accounting.CashFlow.get_daily_summary() and the date_axis option on plot_cashflow_bars still support it.

Config File#

Plot style and the report language are both read from an optional cashflow.json sitting in the bank/account folder (next to the plots). Every key is optional, and so is the file itself: anything missing falls back to babilonia.accounting.PLOT_CONFIG_DEFAULTS.

{
    "figsize_mm": [150, 80],
    "dpi": 300,
    "color_inflows": "tab:blue",
    "color_outflows": "tab:red",
    "background_color": "#F7F7FB",
    "language": "en",
    "fontsize_title": 10,
    "fontsize_annotation": 5,
    "fontsize_ticks": 8,
    "bar_width_pct": 60,
    "hist_width_pct": 33,
    "hist_bin_size": null,
    "color_hist": null
}

language controls the printed table headers (terminal + .txt report: Ano -> Year, Entradas -> Inflows, etc. – see COLUMN_LABELS in this module) and the balance source shown in the terminal (SOURCE_LABELS), not the plots. Currently "en" (default) or "pt-br" (the data’s native language, a no-op); more may be added later. Every other key tunes plot_cashflow_bars() – see babilonia.accounting.CashFlow.load_plot_config() for details.

Balance vs Levels#

Optional, and off unless the account folder holds a levels.json. Without that file, everything below is skipped and the run is exactly as described above – SALDOS files, if any, are ignored.

With it, the script also tracks the account’s running balance against the levels in levels.json (its schema is documented in babilonia.accounting.CashFlow.load_levels()). Since a balance needs more than movements, it reads observed balances from one SALDOS file per year, in the year subfolder next to the statements:

bb/                                 # Bank
└── cdb/                            # Bank account
    ├── levels.json                 # turns this section on
    ├── 2025/
    │   ├── EXTRATO_BB_CDB_2025-12_T1.csv
    │   └── SALDOS_BB_CDB_2025.csv
    └── 2026/
        ├── EXTRATO_BB_CDB_2026-01_T1.csv
        ├── EXTRATO_BB_CDB_2026-02_T1.csv
        └── SALDOS_BB_CDB_2026.csv

A SALDOS file has the same layout as a T1 statement – Data, Categoria, Valor, Descricao – but each row is a balance, not a movement: Valor is the balance at the end of Data. Typically one row per month, though any number works, and months can be missing:

Data;Categoria;Valor;Descricao
2026-01-31;Saldo;48210.55;saldo do extrato
2026-02-28;Saldo;47390.10;saldo do extrato
2026-03-31;Saldo;48020.87;saldo do extrato

These are not the SALDOS_{BANK}_{ACCOUNT}.csv file read by babilonia.tools.report, which sits at the account root and holds one opening balance per year (Year, Value). This script does not read SALDOS files at the account root.

Observed balances anchor the series; the T1 flows fill in the months between them (see babilonia.accounting.CashFlow.get_balance_panel()). This is what keeps the balance right for accounts whose statements miss movements the bank makes on its own, such as interest, fees or tax withheld. With a levels.json but no SALDOS files, the balance is built from the flows alone, starting at zero – the script says so in the terminal. The reverse also works: with levels.json and SALDOS files but no T1 statements at all, the script skips everything else and builds the balance from the readings alone – each month shows the latest observed balance, carried flat into months without one, and the flows panel stays empty.

The full balance plot always covers the full history (--year does not cut it: a balance is cumulative; it only picks the zoom year below), padded to whole calendar years like the monthly outputs: a run in June still spans January to December, with the months ahead left blank – no balance, no flows, no status – while the levels stay drawn to December. Written to the bank/account folder:

bb/                                 # Bank
└── cdb/                            # Bank account
    ├── CAIXA_BB_CDB_SALDO.csv      # monthly balance, levels, status
    ├── CAIXA_BB_CDB_SALDO.jpg      # balance vs levels + flows, all years
    └── CAIXA_BB_CDB_SALDO_2026.jpg # the same, zoomed on one year

The one-year zoom exists because on a long series the latest months get squeezed at the right edge. It shows the latest year with a balance – the current year – or the year given with --year. It is a slice of the full series, so levels that depend on history (a peak, inflation growth) keep the same values as in the full plot.

The script does not modify input files; all outputs are written as new CSV, .txt and .jpg files.

Functions

load_saldos(folder, data_type)

Load every SALDOS_{BANK}_{ACCOUNT}_{YEAR}.csv of an account.

localize_columns(df, column_labels)

Rename df's columns for display, falling back to the original name for any column not covered by column_labels.

main()

run_balance_levels(folder, data_type, ...[, ...])

Build, print, export and plot the balance-vs-levels panel.

babilonia.tools.cashflow.localize_columns(df, column_labels)[source]#

Rename df’s columns for display, falling back to the original name for any column not covered by column_labels. Display-only – never used before a dataframe is exported to CSV.

babilonia.tools.cashflow.load_saldos(folder, data_type)[source]#

Load every SALDOS_{BANK}_{ACCOUNT}_{YEAR}.csv of an account.

Parameters:
  • folder (pathlib.Path) – Bank/account folder (year subfolders inside).

  • data_type (str) – Account type, e.g. "bb-cdb".

Returns:

Observed balances (typed Data and Valor, plus any other columns as read) and the list of files found. None for the data when there are no files.

Return type:

tuple

babilonia.tools.cashflow.run_balance_levels(folder, data_type, df_flows, levels, plot_config, char_w, zoom_year=None)[source]#

Build, print, export and plot the balance-vs-levels panel.

Writes the full-history CSV and plot, plus a one-year zoom plot: on a long series the latest months get squeezed at the right edge, so the zoom shows the current year at full width.

Parameters:
  • folder (pathlib.Path) – Bank/account folder, where the outputs are written.

  • data_type (str) – Account type, e.g. "bb-cdb".

  • df_flows (pandas.DataFrame) – Cash flow data with typed Data and Valor. May be empty if the account has SALDOS files.

  • levels (dict) – Level rules, as returned by babilonia.accounting.CashFlow.load_levels().

  • plot_config (dict) – Style overrides, as returned by babilonia.accounting.CashFlow.load_plot_config().

  • char_w (int) – Width of the terminal section rule, in characters.

  • zoom_year (int or str or None) – Year to zoom on. None picks the latest year with a balance (the “current” year).

Returns:

Number of output files written.

Return type:

int

babilonia.tools.cashflow.main()[source]#