Infrastructure·Compliance Reports·Intermediate

Pnl Reconciliation Report

Generate detailed PnL reconciliation reports that systematically compare internally calculated strategy PnL against exchange-reported realized PnL from official trade history, algorithmically identifying, categorizing, and explaining any discrepancies to ensure accurate performance reporting and accounting integrity.

complianceinfrastructure

Compliance & Audit: Reconcile PnL Across Exchanges

This notebook provides a framework for reconciling Profit and Loss (PnL) data across multiple trading exchanges. In financial operations, especially in high-frequency or multi-venue trading, ensuring that PnL figures align across different platforms is crucial for accurate reporting, risk management, and compliance.

Discrepancies can arise due to various factors, including differences in trade execution times, fee structures, data reporting delays, timezone mismatches, or even subtle bugs in system integrations. This report aims to identify, quantify, and report such discrepancies.

Key Concepts

ConceptDescriptionImportance
Profit and Loss (PnL)The financial gain or loss realized over a period from trading activities.Core metric for financial performance and regulatory reporting.
ReconciliationThe process of comparing two sets of records to ensure they match and to identify any differences.Essential for data integrity, auditability, and compliance.
Exchange DataTrading records, order fills, and account balances obtained from various trading platforms.Primary source of truth for trade activity.
DiscrepancyAny difference found between expected PnL from internal calculations and reported PnL from an exchange.Signals potential data issues, operational errors, or fraud.
Audit TrailA chronological record of activities, providing evidence of transactions and system changes.Critical for compliance, issue investigation, and accountability.
Data NormalizationConverting data from different sources into a common format and standard for comparison.Ensures 'apples-to-apples' comparison during reconciliation.

2. Dependency Installation

This section installs all necessary Python packages required for data manipulation, logging, and visualization.

[1]
pip install pandas numpy matplotlib seaborn loguru tenacity
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)
Collecting loguru
  Downloading loguru-0.7.3-py3-none-any.whl.metadata (22 kB)
Requirement already satisfied: tenacity in /usr/local/lib/python3.12/dist-packages (9.1.4)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
Downloading loguru-0.7.3-py3-none-any.whl (61 kB)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 61.6/61.6 kB 2.7 MB/s eta 0:00:00
[?25hInstalling collected packages: loguru
Successfully installed loguru-0.7.3

3. Library Imports

Import all standard and third-party libraries.

[2]
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from collections import deque
import time
import random
from typing import Dict, Any, List, Optional
from loguru import logger
from tenacity import retry, wait_exponential, stop_after_attempt, before_sleep_log
import sys

# Configure loguru logger
logger.remove()
logger.add(sys.stdout, format="<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>")
1

4. Core Functions

This section defines the core functions used throughout the PnL reconciliation process. Each function is designed to perform a specific task, adhering to the specified code style and documentation requirements.

Function Name: create_pnl_state

This function initializes the global state dictionary that will hold all relevant data and configuration throughout the reconciliation process. It sets up initial empty dataframes and parameters.

Parameters:

  • start_date (str): The start date for the reconciliation period in 'YYYY-MM-DD' format.
  • end_date (str): The end date for the reconciliation period in 'YYYY-MM-DD' format.
  • exchanges (List[str]): A list of exchange names to be reconciled.

Returns:

  • Dict[str, Any]: An initialized state dictionary.
[3]
def create_pnl_state(start_date: str, end_date: str, exchanges: List[str]) -> Dict[str, Any]:
    """
    Initializes the state dictionary for PnL reconciliation.

    Parameters
    ----------
    start_date : str
        The start date for the reconciliation period in 'YYYY-MM-DD' format.
    end_date : str
        The end date for the reconciliation period in 'YYYY-MM-DD' format.
    exchanges : List[str]
        A list of exchange names to be reconciled.

    Returns
    -------
    Dict[str, Any]
        An initialized state dictionary.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA', 'ExchangeB'])
    >>> 'reconciliation_period' in state
    True
    """
    logger.info(f"Initializing PnL state for {exchanges} from {start_date} to {end_date}")
    state = {
        'reconciliation_period': {
            'start_date': pd.to_datetime(start_date),
            'end_date': pd.to_datetime(end_date)
        },
        'exchanges': exchanges,
        'trade_data': {exchange: pd.DataFrame() for exchange in exchanges},
        'pnl_data': {exchange: pd.DataFrame() for exchange in exchanges},
        'reconciliation_report': pd.DataFrame(),
        'metrics': {},
        'config': {
            'trade_columns': ['timestamp', 'symbol', 'side', 'price', 'quantity', 'fees', 'exchange']
        }
    }
    logger.debug("PnL state initialized successfully.")
    return state

Function Name: simulate_trade_data

This function generates synthetic trade data for a given exchange within a specified date range. It simulates realistic trading activity, including random prices, quantities, and fees. It is used to mock data from different exchanges.

Parameters:

  • state (Dict[str, Any]): The current state dictionary.
  • exchange_name (str): The name of the exchange for which to generate data.
  • num_trades (int): The number of trades to simulate.
  • symbols (List[str]): A list of trading symbols (e.g., 'BTC/USD').

Returns:

  • Dict[str, Any]: The updated state dictionary with simulated trade data.
[4]
@retry(wait=wait_exponential(multiplier=1, min=4, max=10), stop=stop_after_attempt(3), before_sleep=before_sleep_log(logger, 'warning'))
def simulate_trade_data(state: Dict[str, Any], exchange_name: str, num_trades: int, symbols: List[str]) -> Dict[str, Any]:
    """
    Simulates realistic trade data for a given exchange.

    Parameters
    ----------
    state : Dict[str, Any]
        The current state dictionary.
    exchange_name : str
        The name of the exchange for which to generate data.
    num_trades : int
        The number of trades to simulate.
    symbols : List[str]
        A list of trading symbols (e.g., 'BTC/USD').

    Returns
    -------
    Dict[str, Any]
        The updated state dictionary with simulated trade data.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA'])
    >>> updated_state = simulate_trade_data(state, 'ExchangeA', 10, ['BTC/USD'])
    >>> not updated_state['trade_data']['ExchangeA'].empty
    True
    """
    logger.info(f"Simulating {num_trades} trades for {exchange_name}.")
    try:
        start_date = state['reconciliation_period']['start_date']
        end_date = state['reconciliation_period']['end_date']
        time_range = (end_date - start_date).days * 24 * 60 * 60 # total seconds

        data = []
        for _ in range(num_trades):
            timestamp_offset = random.uniform(0, time_range)
            timestamp = start_date + datetime.timedelta(seconds=timestamp_offset)
            symbol = random.choice(symbols)
            side = random.choice(['BUY', 'SELL'])
            price = round(random.uniform(1000.0, 50000.0), 2)
            quantity = round(random.uniform(0.01, 5.0), 4)
            fees = round(price * quantity * random.uniform(0.0001, 0.001), 2) # 0.01% to 0.1% fee
            data.append({
                'timestamp': timestamp,
                'symbol': symbol,
                'side': side,
                'price': price,
                'quantity': quantity,
                'fees': fees,
                'exchange': exchange_name
            })

        df = pd.DataFrame(data)
        df['timestamp'] = pd.to_datetime(df['timestamp'])
        df = df.sort_values(by='timestamp').reset_index(drop=True)

        # Add random jitter to timestamps to simulate real-world slight differences
        if random.random() > 0.5: # Apply jitter sometimes
            df['timestamp'] = df['timestamp'] + pd.to_timedelta(np.random.normal(0, 10, len(df)), unit='ms')

        state['trade_data'][exchange_name] = df
        logger.debug(f"Successfully simulated trade data for {exchange_name}. Trades generated: {len(df)}")

        # Introduce a random delay to simulate network latency for retry mechanism
        time.sleep(random.uniform(0.1, 0.5) + random.uniform(0, 0.2)) # Base delay + jitter

    except Exception as e:
        logger.error(f"Error simulating trade data for {exchange_name}: {e}")
        raise # Re-raise to trigger retry
    return state

Function Name: calculate_pnl_single_exchange

This function calculates the PnL for a single exchange based on its trade data. It considers buy and sell orders to determine realized profit or loss and subtracts any incurred fees. This calculation assumes a simple FIFO (First-In, First-Out) accounting method for simplicity, but more complex methods could be integrated.

Parameters:

  • state (Dict[str, Any]): The current state dictionary containing trade data.
  • exchange_name (str): The name of the exchange for which to calculate PnL.

Returns:

  • Dict[str, Any]: The updated state dictionary with calculated PnL data for the specified exchange.
[5]
def calculate_pnl_single_exchange(state: Dict[str, Any], exchange_name: str) -> Dict[str, Any]:
    """
    Calculates the PnL for a single exchange based on its trade data.

    Parameters
    ----------
    state : Dict[str, Any]
        The current state dictionary.
    exchange_name : str
        The name of the exchange for which to calculate PnL.

    Returns
    -------
    Dict[str, Any]
        The updated state dictionary with calculated PnL data.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA'])
    >>> state = simulate_trade_data(state, 'ExchangeA', 10, ['BTC/USD'])
    >>> updated_state = calculate_pnl_single_exchange(state, 'ExchangeA')
    >>> 'daily_pnl' in updated_state['pnl_data']['ExchangeA'].columns
    True
    """
    logger.info(f"Calculating PnL for {exchange_name}.")
    trade_df = state['trade_data'][exchange_name].copy()

    if trade_df.empty:
        logger.warning(f"No trade data found for {exchange_name}. PnL will be empty.")
        state['pnl_data'][exchange_name] = pd.DataFrame(columns=['date', 'symbol', 'realized_pnl', 'total_fees'])
        return state

    pnl_records = []
    # Group by symbol to calculate PnL for each asset
    for symbol, group in trade_df.groupby('symbol'):
        holdings_cost_basis = deque() # (quantity, cost_price)
        realized_pnl = 0.0
        total_fees = 0.0

        for _, trade in group.iterrows():
            trade_value = trade['price'] * trade['quantity']
            total_fees += trade['fees']

            if trade['side'] == 'BUY':
                holdings_cost_basis.append((trade['quantity'], trade['price']))
            elif trade['side'] == 'SELL':
                quantity_to_sell = trade['quantity']
                while quantity_to_sell > 0 and holdings_cost_basis:
                    held_quantity, held_cost = holdings_cost_basis[0]
                    if held_quantity <= quantity_to_sell:
                        realized_pnl += (trade['price'] - held_cost) * held_quantity
                        quantity_to_sell -= held_quantity
                        holdings_cost_basis.popleft()
                    else:
                        realized_pnl += (trade['price'] - held_cost) * quantity_to_sell
                        holdings_cost_basis[0] = (held_quantity - quantity_to_sell, held_cost)
                        quantity_to_sell = 0
            # For simplicity, we ignore unrealized PnL and focus on realized for reconciliation

        pnl_records.append({
            'date': trade_df['timestamp'].max().normalize(), # Using max trade date for simplicity
            'symbol': symbol,
            'realized_pnl': realized_pnl - total_fees,
            'total_fees': total_fees
        })

    pnl_df = pd.DataFrame(pnl_records)
    if not pnl_df.empty:
        pnl_df['date'] = pd.to_datetime(pnl_df['date'])
    state['pnl_data'][exchange_name] = pnl_df
    logger.debug(f"PnL calculated for {exchange_name}. Total PnL: {pnl_df['realized_pnl'].sum() if not pnl_df.empty else 0:.2f}")
    return state

Function Name: reconcile_pnl

This function compares the PnL data from different exchanges to identify discrepancies. It standardizes the PnL data by date and symbol, then performs a merge operation to highlight where PnL figures differ.

Parameters:

  • state (Dict[str, Any]): The current state dictionary containing PnL data for all exchanges.

Returns:

  • Dict[str, Any]: The updated state dictionary with the reconciliation report.
[6]
def reconcile_pnl(state: Dict[str, Any]) -> Dict[str, Any]:
    """
    Reconciles PnL data across multiple exchanges.

    Parameters
    ----------
    state : Dict[str, Any]
        The current state dictionary.

    Returns
    -------
    Dict[str, Any]
        The updated state dictionary with the reconciliation report.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA', 'ExchangeB'])
    >>> state = simulate_trade_data(state, 'ExchangeA', 10, ['BTC/USD'])
    >>> state = simulate_trade_data(state, 'ExchangeB', 12, ['BTC/USD'])
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeA')
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeB')
    >>> updated_state = reconcile_pnl(state)
    >>> 'discrepancy' in updated_state['reconciliation_report'].columns
    True
    """
    logger.info("Starting PnL reconciliation across exchanges.")
    combined_pnl = pd.DataFrame()

    # Consolidate PnL data from all exchanges
    for exchange_name in state['exchanges']:
        pnl_df = state['pnl_data'][exchange_name].copy()
        if not pnl_df.empty:
            pnl_df['exchange'] = exchange_name
            combined_pnl = pd.concat([combined_pnl, pnl_df])

    if combined_pnl.empty:
        logger.warning("No PnL data to reconcile. Reconciliation report will be empty.")
        state['reconciliation_report'] = pd.DataFrame(columns=['date', 'symbol', 'exchange_a_pnl', 'exchange_b_pnl', 'discrepancy'])
        return state

    # Pivot to compare PnL side-by-side
    pivot_columns = ['date', 'symbol']
    if len(state['exchanges']) < 2:
        logger.warning("Cannot reconcile PnL with less than two exchanges. Report will show individual PnL.")
        state['reconciliation_report'] = combined_pnl
        return state

    first_exchange = state['exchanges'][0]
    second_exchange = state['exchanges'][1]

    # Use a more robust merging approach for multiple exchanges
    reconciliation_df = pd.DataFrame()
    if not combined_pnl.empty:
        # Ensure 'date' and 'symbol' are correct types for merging
        combined_pnl['date'] = pd.to_datetime(combined_pnl['date']).dt.normalize()

        reconciliation_df = combined_pnl.pivot_table(
            index=['date', 'symbol'],
            columns='exchange',
            values='realized_pnl'
        ).reset_index()
        reconciliation_df.columns.name = None # Remove columns name 'exchange'

        # Fill NaNs with 0 where an exchange might not have trades for a symbol/date
        for ex in state['exchanges']:
            if ex not in reconciliation_df.columns:
                 reconciliation_df[ex] = 0.0 # Add missing exchange columns if needed
            reconciliation_df[ex] = reconciliation_df[ex].fillna(0.0)

        # Calculate discrepancy for the first two exchanges as a primary check
        if first_exchange in reconciliation_df.columns and second_exchange in reconciliation_df.columns:
            reconciliation_df['discrepancy'] = reconciliation_df[first_exchange] - reconciliation_df[second_exchange]
            reconciliation_df['percentage_discrepancy'] = np.where(
                reconciliation_df[first_exchange] != 0,
                (reconciliation_df['discrepancy'] / reconciliation_df[first_exchange]) * 100,
                np.where(
                    reconciliation_df[second_exchange] != 0,
                    (reconciliation_df['discrepancy'] / reconciliation_df[second_exchange]) * 100,
                    0.0
                )
            )
        else:
            logger.warning(f"Could not find both {first_exchange} and {second_exchange} in pivoted PnL for discrepancy calculation.")
            reconciliation_df['discrepancy'] = 0.0
            reconciliation_df['percentage_discrepancy'] = 0.0

    state['reconciliation_report'] = reconciliation_df
    logger.debug(f"PnL reconciliation completed. Total discrepancies found: {len(reconciliation_df[reconciliation_df['discrepancy'].abs() > 0])}")
    return state

Function Name: generate_reconciliation_report_summary

This function generates a summary of the reconciliation report, highlighting key metrics like total PnL per exchange, total absolute discrepancy, and average percentage discrepancy. It provides an overview of the reconciliation outcome.

Parameters:

  • state (Dict[str, Any]): The current state dictionary containing the reconciliation report.

Returns:

  • Dict[str, Any]: The updated state dictionary with summary metrics.
[7]
def generate_reconciliation_report_summary(state: Dict[str, Any]) -> Dict[str, Any]:
    """
    Generates a summary of the PnL reconciliation report.

    Parameters
    ----------
    state : Dict[str, Any]
        The current state dictionary.

    Returns
    -------
    Dict[str, Any]
        The updated state dictionary with summary metrics.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA', 'ExchangeB'])
    >>> state = simulate_trade_data(state, 'ExchangeA', 10, ['BTC/USD'])
    >>> state = simulate_trade_data(state, 'ExchangeB', 12, ['BTC/USD'])
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeA')
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeB')
    >>> state = reconcile_pnl(state)
    >>> updated_state = generate_reconciliation_report_summary(state)
    >>> 'total_pnl_exchangeA' in updated_state['metrics']
    True
    """
    logger.info("Generating reconciliation report summary.")
    report_df = state['reconciliation_report']
    metrics = {}

    if report_df.empty:
        logger.warning("Reconciliation report is empty. No summary metrics generated.")
        return state

    first_exchange = state['exchanges'][0]
    second_exchange = state['exchanges'][1]

    for ex in state['exchanges']:
        if ex in report_df.columns:
            metrics[f'total_pnl_{ex}'] = report_df[ex].sum()

    if 'discrepancy' in report_df.columns:
        metrics['total_absolute_discrepancy'] = report_df['discrepancy'].abs().sum()
        metrics['max_absolute_discrepancy'] = report_df['discrepancy'].abs().max()
        metrics['average_percentage_discrepancy'] = report_df['percentage_discrepancy'].abs().mean()
        metrics['count_of_discrepancies'] = len(report_df[report_df['discrepancy'].abs() > 0.001]) # Threshold for non-zero

    state['metrics'] = metrics
    logger.debug("Reconciliation report summary generated.")
    return state

Function Name: identify_discrepancy_sources

This function attempts to identify potential sources of discrepancies by analyzing trade volumes and counts per symbol across exchanges. While not a definitive root cause analysis, it can highlight symbols or periods with significant differences that warrant further investigation.

Parameters:

  • state (Dict[str, Any]): The current state dictionary containing trade data and the reconciliation report.

Returns:

  • Dict[str, Any]: The updated state dictionary, potentially with insights into discrepancy sources.
[8]
def identify_discrepancy_sources(state: Dict[str, Any]) -> Dict[str, Any]:
    """
    Identifies potential sources of discrepancies by comparing trade data details.

    Parameters
    ----------
    state : Dict[str, Any]
        The current state dictionary.

    Returns
    -------
    Dict[str, Any]
        The updated state dictionary with insights into discrepancy sources.

    Examples
    --------
    >>> state = create_pnl_state('2023-01-01', '2023-01-07', ['ExchangeA', 'ExchangeB'])
    >>> state = simulate_trade_data(state, 'ExchangeA', 10, ['BTC/USD'])
    >>> state = simulate_trade_data(state, 'ExchangeB', 12, ['BTC/USD'])
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeA')
    >>> state = calculate_pnl_single_exchange(state, 'ExchangeB')
    >>> state = reconcile_pnl(state)
    >>> updated_state = identify_discrepancy_sources(state)
    >>> 'trade_volume_comparison' in updated_state['metrics']
    True
    """
    logger.info("Attempting to identify discrepancy sources.")
    trade_data_comparisons = {}

    if len(state['exchanges']) < 2:
        logger.warning("Insufficient exchanges to compare trade data for discrepancy sources.")
        return state

    all_trades = pd.DataFrame()
    for ex in state['exchanges']:
        df = state['trade_data'][ex].copy()
        if not df.empty:
            df['date'] = df['timestamp'].dt.normalize()
            all_trades = pd.concat([all_trades, df])

    if all_trades.empty:
        logger.warning("No trade data available to identify discrepancy sources.")
        return state

    # Compare trade counts and volumes per symbol and date
    trade_summary = all_trades.groupby(['date', 'symbol', 'exchange']).agg(
        trade_count=('timestamp', 'count'),
        total_quantity=('quantity', 'sum'),
        total_value=('price', lambda x: (x * all_trades.loc[x.index, 'quantity']).sum()),
        total_fees=('fees', 'sum')
    ).reset_index()

    pivot_trade_summary = trade_summary.pivot_table(
        index=['date', 'symbol'],
        columns='exchange',
        values=['trade_count', 'total_quantity', 'total_value', 'total_fees']
    ).reset_index()

    pivot_trade_summary.columns = [f'{col[0]}_{col[1]}' if col[1] else col[0] for col in pivot_trade_summary.columns.values]
    pivot_trade_summary = pivot_trade_summary.fillna(0)

    first_ex = state['exchanges'][0]
    second_ex = state['exchanges'][1]

    # Calculate differences for key metrics
    metrics_to_compare = ['trade_count', 'total_quantity', 'total_value', 'total_fees']
    for metric in metrics_to_compare:
        col1 = f'{metric}_{first_ex}'
        col2 = f'{metric}_{second_ex}'
        if col1 in pivot_trade_summary.columns and col2 in pivot_trade_summary.columns:
            pivot_trade_summary[f'{metric}_diff'] = pivot_trade_summary[col1] - pivot_trade_summary[col2]
            pivot_trade_summary[f'{metric}_abs_diff'] = pivot_trade_summary[f'{metric}_diff'].abs()
            pivot_trade_summary[f'{metric}_pct_diff'] = np.where(
                pivot_trade_summary[col1] != 0,
                (pivot_trade_summary[f'{metric}_diff'] / pivot_trade_summary[col1]) * 100,
                np.where(
                    pivot_trade_summary[col2] != 0,
                    (pivot_trade_summary[f'{metric}_diff'] / pivot_trade_summary[col2]) * 100,
                    0.0
                )
            )

    state['metrics']['trade_volume_comparison'] = pivot_trade_summary
    logger.debug("Discrepancy source identification completed.")
    return state

5. Demonstration/Visualization

This section demonstrates the usage of the core functions with simulated data, visualizing the reconciliation process and its outcomes. It includes generating trade data, calculating PnL, performing reconciliation, and presenting key insights through plots and summary tables.

[9]
# 5.1. Initialize State
logger.info("DEMO: Initializing state.")
start_date_str = '2023-01-01'
end_date_str = '2023-01-07'
exchanges_to_reconcile = ['Exchange_Alpha', 'Exchange_Beta']

pnl_reconciliation_state = create_pnl_state(start_date_str, end_date_str, exchanges_to_reconcile)
display(pnl_reconciliation_state['reconciliation_period'])
2026-06-09 07:10:43.739 | INFO     | __main__:<cell line: 0>:2 - DEMO: Initializing state.
2026-06-09 07:10:43.741 | INFO     | __main__:create_pnl_state:25 - Initializing PnL state for ['Exchange_Alpha', 'Exchange_Beta'] from 2023-01-01 to 2023-01-07
2026-06-09 07:10:43.755 | DEBUG    | __main__:create_pnl_state:40 - PnL state initialized successfully.
{'start_date': Timestamp('2023-01-01 00:00:00'),
 'end_date': Timestamp('2023-01-07 00:00:00')}
[10]
# 5.2. Simulate Trade Data for Each Exchange
logger.info("DEMO: Simulating trade data.")
symbols_traded = ['BTC/USD', 'ETH/USD', 'LTC/USD']

# Simulate for Exchange Alpha
pnl_reconciliation_state = simulate_trade_data(pnl_reconciliation_state, 'Exchange_Alpha', 500, symbols_traded)

# Simulate for Exchange Beta (with some intentional differences for discrepancies)
pnl_reconciliation_state = simulate_trade_data(pnl_reconciliation_state, 'Exchange_Beta', 520, symbols_traded)

# Introduce an intentional discrepancy in one of the exchanges
if not pnl_reconciliation_state['trade_data']['Exchange_Beta'].empty:
    # Change price of a few trades to create PnL difference
    num_discrepant_trades = min(5, len(pnl_reconciliation_state['trade_data']['Exchange_Beta']))
    indices_to_modify = pnl_reconciliation_state['trade_data']['Exchange_Beta'].sample(n=num_discrepant_trades).index
    pnl_reconciliation_state['trade_data']['Exchange_Beta'].loc[indices_to_modify, 'price'] *= (1 + np.random.uniform(-0.01, 0.01, num_discrepant_trades)) # +/- 1% difference
    logger.warning(f"Introduced intentional PnL discrepancy for {num_discrepant_trades} trades in Exchange_Beta.")

print("\n--- Sample Trade Data for Exchange_Alpha ---")
display(pnl_reconciliation_state['trade_data']['Exchange_Alpha'].head())
print("\n--- Sample Trade Data for Exchange_Beta ---")
display(pnl_reconciliation_state['trade_data']['Exchange_Beta'].head())
2026-06-09 07:10:44.335 | INFO     | __main__:<cell line: 0>:2 - DEMO: Simulating trade data.
2026-06-09 07:10:44.339 | INFO     | __main__:simulate_trade_data:29 - Simulating 500 trades for Exchange_Alpha.
2026-06-09 07:10:44.389 | DEBUG    | __main__:simulate_trade_data:63 - Successfully simulated trade data for Exchange_Alpha. Trades generated: 500
2026-06-09 07:10:44.969 | INFO     | __main__:simulate_trade_data:29 - Simulating 520 trades for Exchange_Beta.
2026-06-09 07:10:45.007 | DEBUG    | __main__:simulate_trade_data:63 - Successfully simulated trade data for Exchange_Beta. Trades generated: 520
2026-06-09 07:10:45.205 | WARNING  | __main__:<cell line: 0>:17 - Introduced intentional PnL discrepancy for 5 trades in Exchange_Beta.

--- Sample Trade Data for Exchange_Alpha ---
timestamp symbol side price quantity fees exchange
0 2023-01-01 00:26:03.991950 ETH/USD BUY 43069.52 3.4780 111.69 Exchange_Alpha
1 2023-01-01 00:37:37.314947 BTC/USD BUY 49242.68 1.0680 36.56 Exchange_Alpha
2 2023-01-01 01:27:03.880345 LTC/USD BUY 33375.80 0.8376 10.01 Exchange_Alpha
3 2023-01-01 01:32:00.879947 BTC/USD SELL 17229.73 1.9435 31.02 Exchange_Alpha
4 2023-01-01 01:37:38.059426 BTC/USD BUY 30177.05 4.4458 30.26 Exchange_Alpha

--- Sample Trade Data for Exchange_Beta ---
timestamp symbol side price quantity fees exchange
0 2023-01-01 00:44:52.078397 LTC/USD SELL 41233.42 4.5922 117.26 Exchange_Beta
1 2023-01-01 00:52:25.002608 ETH/USD BUY 13359.58 3.1307 28.48 Exchange_Beta
2 2023-01-01 01:14:17.737262 ETH/USD BUY 46122.51 0.6561 20.67 Exchange_Beta
3 2023-01-01 01:22:23.715910 BTC/USD SELL 17642.51 3.5877 10.77 Exchange_Beta
4 2023-01-01 01:24:32.618650 ETH/USD BUY 47066.78 3.1073 22.70 Exchange_Beta
[11]
# 5.3. Calculate PnL for Each Exchange
logger.info("DEMO: Calculating PnL.")
for ex in exchanges_to_reconcile:
    pnl_reconciliation_state = calculate_pnl_single_exchange(pnl_reconciliation_state, ex)

print("\n--- PnL Data for Exchange_Alpha ---")
display(pnl_reconciliation_state['pnl_data']['Exchange_Alpha'])
print("\n--- PnL Data for Exchange_Beta ---")
display(pnl_reconciliation_state['pnl_data']['Exchange_Beta'])
2026-06-09 07:10:45.877 | INFO     | __main__:<cell line: 0>:2 - DEMO: Calculating PnL.
2026-06-09 07:10:45.878 | INFO     | __main__:calculate_pnl_single_exchange:25 - Calculating PnL for Exchange_Alpha.
2026-06-09 07:10:45.910 | DEBUG    | __main__:calculate_pnl_single_exchange:71 - PnL calculated for Exchange_Alpha. Total PnL: 667957.46
2026-06-09 07:10:45.911 | INFO     | __main__:calculate_pnl_single_exchange:25 - Calculating PnL for Exchange_Beta.
2026-06-09 07:10:45.942 | DEBUG    | __main__:calculate_pnl_single_exchange:71 - PnL calculated for Exchange_Beta. Total PnL: 145945.81

--- PnL Data for Exchange_Alpha ---
date symbol realized_pnl total_fees
0 2023-01-06 BTC/USD -266820.447983 5021.51
1 2023-01-06 ETH/USD 636078.112878 5730.12
2 2023-01-06 LTC/USD 298699.795937 6830.10

--- PnL Data for Exchange_Beta ---
date symbol realized_pnl total_fees
0 2023-01-06 BTC/USD 181003.802938 6655.39
1 2023-01-06 ETH/USD -17582.844996 5899.51
2 2023-01-06 LTC/USD -17475.150900 5937.04
[12]
# 5.4. Reconcile PnL and Generate Report
logger.info("DEMO: Reconciling PnL.")
pnl_reconciliation_state = reconcile_pnl(pnl_reconciliation_state)

print("\n--- PnL Reconciliation Report ---")
display(pnl_reconciliation_state['reconciliation_report'])

# Filter and display discrepancies
discrepancies_df = pnl_reconciliation_state['reconciliation_report']
if 'discrepancy' in discrepancies_df.columns:
    significant_discrepancies = discrepancies_df[discrepancies_df['discrepancy'].abs() > 0.01] # Define a threshold
    print("\n--- Significant PnL Discrepancies (Absolute value > 0.01) ---")
    display(significant_discrepancies)
else:
    print("No 'discrepancy' column found in the reconciliation report.")
2026-06-09 07:10:46.531 | INFO     | __main__:<cell line: 0>:2 - DEMO: Reconciling PnL.
2026-06-09 07:10:46.533 | INFO     | __main__:reconcile_pnl:26 - Starting PnL reconciliation across exchanges.
2026-06-09 07:10:46.565 | DEBUG    | __main__:reconcile_pnl:88 - PnL reconciliation completed. Total discrepancies found: 3

--- PnL Reconciliation Report ---
date symbol Exchange_Alpha Exchange_Beta discrepancy percentage_discrepancy
0 2023-01-06 BTC/USD -266820.447983 181003.802938 -447824.250921 167.837306
1 2023-01-06 ETH/USD 636078.112878 -17582.844996 653660.957874 102.764259
2 2023-01-06 LTC/USD 298699.795937 -17475.150900 316174.946837 105.850406

--- Significant PnL Discrepancies (Absolute value > 0.01) ---
date symbol Exchange_Alpha Exchange_Beta discrepancy percentage_discrepancy
0 2023-01-06 BTC/USD -266820.447983 181003.802938 -447824.250921 167.837306
1 2023-01-06 ETH/USD 636078.112878 -17582.844996 653660.957874 102.764259
2 2023-01-06 LTC/USD 298699.795937 -17475.150900 316174.946837 105.850406
[13]
# 5.5. Generate Reconciliation Report Summary
logger.info("DEMO: Generating summary metrics.")
pnl_reconciliation_state = generate_reconciliation_report_summary(pnl_reconciliation_state)

print("\n--- Reconciliation Summary Metrics ---")
for key, value in pnl_reconciliation_state['metrics'].items():
    if isinstance(value, (int, float)):
        print(f"{key}: {value:.2f}")
    else:
        print(f"{key}: {value}")
2026-06-09 07:10:47.156 | INFO     | __main__:<cell line: 0>:2 - DEMO: Generating summary metrics.
2026-06-09 07:10:47.158 | INFO     | __main__:generate_reconciliation_report_summary:27 - Generating reconciliation report summary.
2026-06-09 07:10:47.162 | DEBUG    | __main__:generate_reconciliation_report_summary:49 - Reconciliation report summary generated.

--- Reconciliation Summary Metrics ---
total_pnl_Exchange_Alpha: 667957.46
total_pnl_Exchange_Beta: 145945.81
total_absolute_discrepancy: 1417660.16
max_absolute_discrepancy: 653660.96
average_percentage_discrepancy: 125.48
count_of_discrepancies: 3.00
[14]
# 5.6. Visualize PnL and Discrepancies
logger.info("DEMO: Visualizing PnL and discrepancies.")

report_df = pnl_reconciliation_state['reconciliation_report']

if not report_df.empty and len(exchanges_to_reconcile) >= 2:
    first_ex = exchanges_to_reconcile[0]
    second_ex = exchanges_to_reconcile[1]

    # Plot 1: Daily PnL Comparison
    plt.figure(figsize=(14, 7))
    plt.plot(report_df['date'], report_df[first_ex], label=f'{first_ex} PnL', marker='o')
    plt.plot(report_df['date'], report_df[second_ex], label=f'{second_ex} PnL', marker='x')
    plt.title('Daily PnL Comparison Across Exchanges')
    plt.xlabel('Date')
    plt.ylabel('Realized PnL')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()

    # Plot 2: Daily Absolute Discrepancy
    if 'discrepancy' in report_df.columns:
        plt.figure(figsize=(14, 7))
        plt.bar(report_df['date'], report_df['discrepancy'].abs(), color='salmon')
        plt.title('Daily Absolute PnL Discrepancy')
        plt.xlabel('Date')
        plt.ylabel('Absolute Discrepancy')
        plt.grid(True)
        plt.tight_layout()
        plt.show()

    # Plot 3: Discrepancy Distribution
    if 'discrepancy' in report_df.columns and not report_df['discrepancy'].empty:
        plt.figure(figsize=(10, 6))
        sns.histplot(report_df['discrepancy'], bins=30, kde=True)
        plt.title('Distribution of PnL Discrepancies')
        plt.xlabel('Discrepancy Amount')
        plt.ylabel('Frequency')
        plt.grid(True)
        plt.tight_layout()
        plt.show()
else:
    logger.warning("Not enough data or exchanges for PnL comparison plots.")
2026-06-09 07:10:47.704 | INFO     | __main__:<cell line: 0>:2 - DEMO: Visualizing PnL and discrepancies.
cell output
cell output
cell output
[15]
# 5.7. Identify Discrepancy Sources Visualization
logger.info("DEMO: Visualizing potential discrepancy sources.")

pnl_reconciliation_state = identify_discrepancy_sources(pnl_reconciliation_state)
trade_comp_df = pnl_reconciliation_state['metrics'].get('trade_volume_comparison')

if trade_comp_df is not None and not trade_comp_df.empty:
    print("\n--- Trade Volume Comparison (potential discrepancy sources) ---")
    display(trade_comp_df.head())

    # Visualize total quantity difference by symbol
    if 'total_quantity_diff' in trade_comp_df.columns:
        plt.figure(figsize=(12, 6))
        sns.barplot(x='symbol', y='total_quantity_diff', data=trade_comp_df.groupby('symbol')['total_quantity_diff'].sum().reset_index())
        plt.title('Total Quantity Difference by Symbol Across Exchanges')
        plt.xlabel('Symbol')
        plt.ylabel('Quantity Difference')
        plt.xticks(rotation=45)
        plt.grid(axis='y')
        plt.tight_layout()
        plt.show()

    # Visualize total fees difference by symbol
    if 'total_fees_diff' in trade_comp_df.columns:
        plt.figure(figsize=(12, 6))
        sns.barplot(x='symbol', y='total_fees_diff', data=trade_comp_df.groupby('symbol')['total_fees_diff'].sum().reset_index())
        plt.title('Total Fees Difference by Symbol Across Exchanges')
        plt.xlabel('Symbol')
        plt.ylabel('Fees Difference')
        plt.xticks(rotation=45)
        plt.grid(axis='y')
        plt.tight_layout()
        plt.show()
else:
    logger.warning("No trade volume comparison data to visualize discrepancy sources.")
2026-06-09 07:10:48.943 | INFO     | __main__:<cell line: 0>:2 - DEMO: Visualizing potential discrepancy sources.
2026-06-09 07:10:48.944 | INFO     | __main__:identify_discrepancy_sources:27 - Attempting to identify discrepancy sources.
2026-06-09 07:10:48.991 | DEBUG    | __main__:identify_discrepancy_sources:84 - Discrepancy source identification completed.

--- Trade Volume Comparison (potential discrepancy sources) ---
date symbol total_fees_Exchange_Alpha total_fees_Exchange_Beta total_quantity_Exchange_Alpha total_quantity_Exchange_Beta total_value_Exchange_Alpha total_value_Exchange_Beta trade_count_Exchange_Alpha trade_count_Exchange_Beta ... trade_count_pct_diff total_quantity_diff total_quantity_abs_diff total_quantity_pct_diff total_value_diff total_value_abs_diff total_value_pct_diff total_fees_diff total_fees_abs_diff total_fees_pct_diff
0 2023-01-01 BTC/USD 1230.90 1356.49 73.0186 76.2317 3.775169e+06 4.101412e+06 30.0 31.0 ... -3.333333 -3.2131 3.2131 -4.400386 -3.262430e+05 3.262430e+05 -8.641813 -125.59 125.59 -10.203103
1 2023-01-01 ETH/USD 656.84 1106.98 67.8990 77.9508 2.882580e+06 4.010841e+06 25.0 31.0 ... -24.000000 -10.0518 10.0518 -14.804047 -1.128261e+06 1.128261e+06 -39.140648 -450.14 450.14 -68.531149
2 2023-01-01 LTC/USD 1002.41 1161.77 65.5686 84.0108 3.737923e+06 4.040808e+06 29.0 31.0 ... -6.896552 -18.4422 18.4422 -28.126573 -3.028849e+05 3.028849e+05 -8.103025 -159.36 159.36 -15.897687
3 2023-01-02 BTC/USD 788.80 1639.25 57.3644 117.6244 3.060646e+06 5.673373e+06 23.0 42.0 ... -82.608696 -60.2600 60.2600 -105.047730 -2.612727e+06 2.612727e+06 -85.365211 -850.45 850.45 -107.815669
4 2023-01-02 ETH/USD 757.62 1132.81 73.5397 86.0720 3.275031e+06 3.730246e+06 26.0 31.0 ... -19.230769 -12.5323 12.5323 -17.041544 -4.552150e+05 4.552150e+05 -13.899563 -375.19 375.19 -49.522188

5 rows × 22 columns

cell output
cell output

6. Production Considerations

Deploying a PnL reconciliation system in a production environment requires careful consideration of several factors to ensure reliability, accuracy, and efficiency.

AspectBest Practices
Data IngestionImplement robust ETL (Extract, Transform, Load) pipelines for pulling data from various exchanges. Use APIs with proper authentication and rate limiting. Implement exponential backoff for retries on transient network errors. Validate incoming data for schema conformity and completeness.
Error Handling & LoggingComprehensive logging (using loguru or similar) at different levels (DEBUG, INFO, WARNING, ERROR, CRITICAL) for every stage of the process. Implement structured logging for easier analysis. Set up alerting for critical errors or significant discrepancies.
Performance & ScalabilityOptimize data processing for large datasets (e.g., using vectorized operations in Pandas, Dask for larger-than-memory datasets). Consider distributed computing frameworks if reconciliation needs to scale across many exchanges or high trade volumes.
Data ValidationImplement checks for data consistency (e.g., matching trade IDs, correct timestamps, valid prices/quantities). Cross-validate total trade counts and aggregated volumes where possible.
SecuritySecure API keys and credentials (e.g., using environment variables, secret management services). Ensure data at rest and in transit is encrypted. Implement access controls for sensitive PnL reports.
Monitoring & AlertingSet up dashboards to monitor the reconciliation process health, data freshness, and key metrics (e.g., number of discrepancies, total discrepancy value). Configure alerts for discrepancies exceeding predefined thresholds.
AuditabilityMaintain a clear audit trail of all reconciliation runs, including input data versions, configuration parameters, and reported discrepancies. This is crucial for regulatory compliance and issue investigation.
Configuration ManagementExternalize configuration parameters (e.g., exchange API endpoints, reconciliation thresholds, date ranges) to make the system flexible and easy to update without code changes.
IdempotencyDesign reconciliation runs to be idempotent, meaning running the process multiple times with the same inputs produces the same result. This simplifies error recovery.
Time Zone HandlingStandardize all timestamps to UTC to avoid issues arising from different time zone interpretations across exchanges. Ensure consistent handling of market open/close times.

7. Conclusion

This notebook has outlined a robust framework for PnL reconciliation across multiple trading exchanges. We've covered:

  • State Management: Using a dictionary-based approach to manage the reconciliation process state.
  • Data Simulation: Generating realistic, albeit synthetic, trade data for various exchanges.
  • PnL Calculation: A simplified methodology for calculating realized PnL per exchange.
  • Reconciliation Logic: Merging and comparing PnL figures to identify discrepancies.
  • Reporting & Visualization: Generating summary metrics and visualizing discrepancies to aid in investigation.
  • Production Best Practices: Key considerations for deploying such a system in a live environment.

By following this structured approach, financial institutions can enhance their operational control, ensure data integrity, and meet compliance requirements for accurate PnL reporting.