A Python-based tool for calculating GIPS-compliant investment returns across multiple brokerage accounts (IBKR and Exante). The system processes broker-specific reports and generates standardized returns metrics including Time-Weighted Returns (TWR), fee-adjusted net returns, and comprehensive NAV tracking.
- GIPS-Compliant Calculations: Beginning-of-period weighting, internal dispersion, large cash flow detection
- Multi-Brokerage Support: IBKR and Exante with extensible architecture
- Comprehensive Fee Tracking: Management fees (quarterly) and performance fees (with hurdle rate)
- NAV Tracking: Gross NAV (actual) and Net NAV (after fees) with flow handling
- Configurable: YAML-based configuration for clients, fees, and reporting periods
- Composite Returns: Asset-weighted composite across all accounts
pandas==2.2.3
numpy==2.0.2
openpyxl==3.1.5
PyYAML==6.0.1
numpy_financial==1.0.0
pytest==8.2.1
pytest-cov==5.0.0
hypothesis==6.98.8
Install dependencies:
pip install -r requirements.txtCopy the example configuration and customize:
cp config.example.yaml config.yamlEdit config.yaml to configure:
- Brokerages: Enable/disable IBKR, Exante
- Clients: List of clients to process
- Fees: Management fee (quarterly), performance fee rate, hurdle rate
- Periods: Fiscal year start month, reporting years
Example configuration:
# Fee structure
fees:
management_fee_quarterly: 0.0025 # 0.25% per quarter = 1% annual
performance_fee_rate: 0.25 # 25% of gains above hurdle
hurdle_rate_annual: 0.06 # 6% annual hurdle
# Fiscal year configuration
periods:
fiscal_year_start_month: 1 # January (calendar year)
reporting_years: [2022, 2023, 2024, 2025]Use NAV adjustments when you need to apply a portfolio-level correction from a specific date onward.
Define real values only in your local config.yaml (gitignored), not in committed files.
nav_adjustments:
- brokerage: "IBKR"
clients: ["ClientA", "ClientB"]
start_date: "YYYY-MM-DD"
amount: 0.0
currency: "EUR"
distribute: "weights" # equal | per_account | weights
weights:
ClientA: 1.0
ClientB: 1.0config.yaml is ignored by git; keep any real NAV adjustment amounts and weights there.
If you want reports in a different currency than your base_currency, set
report_currency and provide an FX rates workbook.
Configuration fields:
currency:
base_currency: EUR
report_currency: USD
fx_rates_file: "FX-Rates.xlsx"
fx_rates_sheet: "USDEUR" # optional; defaults to first sheet
fx_date_column: "TIME_PERIOD"
fx_rate_column: null # optional explicit rate column nameExpected FX input is a date-indexed rate list (one row per available rate date). The loader will:
- parse
fx_date_columnas dates, - auto-detect either
EUR/USDorUSD/EURstyle columns, - invert automatically when needed,
- use the last available prior rate for missing dates.
Example FX table:
| TIME_PERIOD | EUR/USD |
|---|---|
| 2025-01-01 | 1.1025 |
| 2025-01-02 | 1.0981 |
| 2025-01-03 | 1.1040 |
Notes:
- If
report_currency == base_currency, no FX conversion is applied. - If currencies differ and
fx_rates_fileis missing, the run fails fast.
ReturnsCalculator/
├── input/ # Input directory for raw broker files
│ ├── IBKR/ # IBKR input files (*.csv)
│ └── Exante/ # Exante input files (*.csv)
├── results/ # Generated results
│ ├── Combined/ # Asset-weighted composite results
│ ├── IBKR_{client}/ # Per-client IBKR results
│ └── Exante_{client}/ # Per-client Exante results
├── logs/ # Processing logs
├── tests/ # Test suite
├── config.yaml # Your configuration (gitignored)
├── config.example.yaml # Example configuration
├── twr_calculator.py # Main calculator engine
├── fee_calculator.py # Fee calculation logic
├── flow_utils.py # Cash flow utilities
├── config_loader.py # Configuration loading
├── IBKR.py # IBKR data processing
├── Exante.py # Exante data processing
└── utils.py # Utility functions
-
Export broker reports:
- IBKR: Export performance report as CSV from Client Portal/TWS
- Exante: Export account statement as CSV
-
Place files in input directories:
input/IBKR/{client_name}.csv input/Exante/{client_name}.csv -
Run the calculator:
python twr_calculator.py
# Use default config.yaml
python twr_calculator.py
# Specify custom config
python twr_calculator.py --config my_config.yaml
# Override input path
python twr_calculator.py --input-path /path/to/input
# Legacy mode (for backward compatibility)
python twr_calculator.py --legacy -i inputThe system processes broker files in three main steps:
-
Account Discovery
- Scans input directory for CSV files
- Identifies clients from filenames
- Creates processing directories
-
Broker-specific Processing
- IBKR: Extracts NAV from allocation reports, flows from deposits/withdrawals
- Exante: Converts to NAV.xlsx and Trades.xlsx
-
Returns Calculation
- Calculates sub-period returns between cash flows
- Computes monthly TWR through geometric linking
- Applies fees and tracks Net NAV
- Eliminates impact of cash flow timing
- Calculated between each cash flow (sub-period returns)
- Geometrically linked for monthly/annual periods
- GIPS-compliant for performance comparison
- Gross TWR: Pure investment performance before fees
- Net TWR: Returns after management and performance fees
- Net TWR = Compounded product of annual net returns
- Rate: 0.25% per quarter (1% annual)
- Timing: Deducted at start of each fiscal quarter
- Base: Current Net NAV
- Rate: 25% of gains above hurdle
- Hurdle: 1.5% per quarter, cumulative since the last high water mark (non-compounding)
- Timing: Deducted at end of each fiscal quarter (after management fee adjustment)
- Base: Net NAV after management fees; performance fee is computed on net index
- High Water Mark: Stored post-fee; only new highs above the hurdle trigger fees
HWM/Hurdle Logic:
HWM = highest post-fee net index achieved at a quarter end
Threshold = HWM + (1.5% × quarters_since_HWM_was_set)
At inception: HWM = 1.0, Q1 threshold = 1.015
If net index > threshold: fee on excess, update HWM to post-fee index, reset counter
If net index <= threshold: no fee, HWM unchanged, counter increments next quarter
The system maintains two parallel NAV series:
| NAV Type | Description |
|---|---|
| Gross NAV | Actual NAV from accounts (includes flows) |
| Net NAV | Gross NAV minus accumulated fees |
Net NAV = Gross NAV - Accumulated Fees
Fee Drag = Gross NAV - Net NAV
Results are saved with timestamps (YYYYMMDD_HHMMSS):
- Monthly TWR values
- Start/end-of-month NAVs
- Monthly flows
- Sub-period returns
- Daily NAV snapshots
- Flow attribution
Contains comprehensive metrics:
Performance Metrics:
- Total Absolute Return (Gross/Net TWR)
- Annualized Return (Gross/Net)
- Initial and Final NAV
- Total Flows
- Fee Impact
Quarterly NAV Tracking:
- Gross NAV (Actual from accounts)
- Net NAV (After fees)
- Management fee per quarter
- Performance fee per quarter
- Cumulative fee impact
Annual Summaries:
- Year-over-year NAV progression
- Annual fee totals
- Active accounts per period
Total Absolute Return (Gross TWR): 150.25%
Total Absolute Return (Net TWR): 112.40%
Annualized Return (Gross TWR): 25.10%
Annualized Return (Net TWR): 20.35%
Initial NAV (Actual): €100,000.00
Final NAV - Gross: €285,450.50
Final NAV - Net: €248,330.25
Total Flows: €25,000.00
Total Fees: €37,120.25 (Mgmt: €4,120 + Perf: €33,000)
Fee Impact: €37,120.25
2022 Returns (Reporting Period): Gross 22.50% → Net 18.15%
2023 Returns (Reporting Period): Gross 15.10% → Net 12.30%
2024 Returns (Reporting Period): Gross 35.40% → Net 28.90%
2025 Returns (Reporting YTD): Gross 12.20% → Net 9.80%
- Beginning-of-Period Weighting: Composite weights use start-of-month NAV
- Internal Dispersion: Calculated for composites with 6+ accounts
- Large Cash Flow Detection: Flags flows >10% of NAV
- Active Account Tracking: Only includes accounts with data for each period
- Geometric Linking: Sub-period returns properly compounded
# Run all tests
pytest
# Run with coverage
pytest --cov=. --cov-report=html
# Run specific test file
pytest tests/test_fee_calculator.py
# Run in parallel
pytest -n auto- Monetary values are processed in
base_currency; optional conversion toreport_currencyis supported with configured FX rates - Fiscal year defaults to January start (calendar year)
- Logs are generated in the logs directory
- Configuration file (
config.yaml) is gitignored for security
- Validates input file formats
- Handles missing or corrupted data
- Logs processing errors and warnings
- Skips invalid return calculations
- Provides detailed error messages
Requires Python 3.8+ with:
- pandas: Data processing and analysis
- numpy: Numerical computations
- openpyxl: Excel file handling
- PyYAML: Configuration loading
See requirements.txt for complete list.