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.
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
| Concept | Description | Importance |
|---|---|---|
| Profit and Loss (PnL) | The financial gain or loss realized over a period from trading activities. | Core metric for financial performance and regulatory reporting. |
| Reconciliation | The process of comparing two sets of records to ensure they match and to identify any differences. | Essential for data integrity, auditability, and compliance. |
| Exchange Data | Trading records, order fills, and account balances obtained from various trading platforms. | Primary source of truth for trade activity. |
| Discrepancy | Any difference found between expected PnL from internal calculations and reported PnL from an exchange. | Signals potential data issues, operational errors, or fraud. |
| Audit Trail | A chronological record of activities, providing evidence of transactions and system changes. | Critical for compliance, issue investigation, and accountability. |
| Data Normalization | Converting 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.
pip install pandas numpy matplotlib seaborn loguru tenacityRequirement 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) [2K [90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━[0m [32m61.6/61.6 kB[0m [31m2.7 MB/s[0m eta [36m0:00:00[0m [?25hInstalling collected packages: loguru Successfully installed loguru-0.7.3
3. Library Imports
Import all standard and third-party libraries.
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.
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.
@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.
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.
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.
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.
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.
# 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'])
[32m2026-06-09 07:10:43.739[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Initializing state.[0m [32m2026-06-09 07:10:43.741[0m | [1mINFO [0m | [36m__main__[0m:[36mcreate_pnl_state[0m:[36m25[0m - [1mInitializing PnL state for ['Exchange_Alpha', 'Exchange_Beta'] from 2023-01-01 to 2023-01-07[0m [32m2026-06-09 07:10:43.755[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36mcreate_pnl_state[0m:[36m40[0m - [34m[1mPnL state initialized successfully.[0m
{'start_date': Timestamp('2023-01-01 00:00:00'),
'end_date': Timestamp('2023-01-07 00:00:00')}# 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())
[32m2026-06-09 07:10:44.335[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Simulating trade data.[0m [32m2026-06-09 07:10:44.339[0m | [1mINFO [0m | [36m__main__[0m:[36msimulate_trade_data[0m:[36m29[0m - [1mSimulating 500 trades for Exchange_Alpha.[0m [32m2026-06-09 07:10:44.389[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36msimulate_trade_data[0m:[36m63[0m - [34m[1mSuccessfully simulated trade data for Exchange_Alpha. Trades generated: 500[0m [32m2026-06-09 07:10:44.969[0m | [1mINFO [0m | [36m__main__[0m:[36msimulate_trade_data[0m:[36m29[0m - [1mSimulating 520 trades for Exchange_Beta.[0m [32m2026-06-09 07:10:45.007[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36msimulate_trade_data[0m:[36m63[0m - [34m[1mSuccessfully simulated trade data for Exchange_Beta. Trades generated: 520[0m [32m2026-06-09 07:10:45.205[0m | [33m[1mWARNING [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m17[0m - [33m[1mIntroduced intentional PnL discrepancy for 5 trades in Exchange_Beta.[0m --- 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 |
# 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'])
[32m2026-06-09 07:10:45.877[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Calculating PnL.[0m [32m2026-06-09 07:10:45.878[0m | [1mINFO [0m | [36m__main__[0m:[36mcalculate_pnl_single_exchange[0m:[36m25[0m - [1mCalculating PnL for Exchange_Alpha.[0m [32m2026-06-09 07:10:45.910[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36mcalculate_pnl_single_exchange[0m:[36m71[0m - [34m[1mPnL calculated for Exchange_Alpha. Total PnL: 667957.46[0m [32m2026-06-09 07:10:45.911[0m | [1mINFO [0m | [36m__main__[0m:[36mcalculate_pnl_single_exchange[0m:[36m25[0m - [1mCalculating PnL for Exchange_Beta.[0m [32m2026-06-09 07:10:45.942[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36mcalculate_pnl_single_exchange[0m:[36m71[0m - [34m[1mPnL calculated for Exchange_Beta. Total PnL: 145945.81[0m --- 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 |
# 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.")
[32m2026-06-09 07:10:46.531[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Reconciling PnL.[0m [32m2026-06-09 07:10:46.533[0m | [1mINFO [0m | [36m__main__[0m:[36mreconcile_pnl[0m:[36m26[0m - [1mStarting PnL reconciliation across exchanges.[0m [32m2026-06-09 07:10:46.565[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36mreconcile_pnl[0m:[36m88[0m - [34m[1mPnL reconciliation completed. Total discrepancies found: 3[0m --- 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 |
# 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}")
[32m2026-06-09 07:10:47.156[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Generating summary metrics.[0m [32m2026-06-09 07:10:47.158[0m | [1mINFO [0m | [36m__main__[0m:[36mgenerate_reconciliation_report_summary[0m:[36m27[0m - [1mGenerating reconciliation report summary.[0m [32m2026-06-09 07:10:47.162[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36mgenerate_reconciliation_report_summary[0m:[36m49[0m - [34m[1mReconciliation report summary generated.[0m --- 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
# 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.")
[32m2026-06-09 07:10:47.704[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Visualizing PnL and discrepancies.[0m
# 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.")
[32m2026-06-09 07:10:48.943[0m | [1mINFO [0m | [36m__main__[0m:[36m<cell line: 0>[0m:[36m2[0m - [1mDEMO: Visualizing potential discrepancy sources.[0m [32m2026-06-09 07:10:48.944[0m | [1mINFO [0m | [36m__main__[0m:[36midentify_discrepancy_sources[0m:[36m27[0m - [1mAttempting to identify discrepancy sources.[0m [32m2026-06-09 07:10:48.991[0m | [34m[1mDEBUG [0m | [36m__main__[0m:[36midentify_discrepancy_sources[0m:[36m84[0m - [34m[1mDiscrepancy source identification completed.[0m --- 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
6. Production Considerations
Deploying a PnL reconciliation system in a production environment requires careful consideration of several factors to ensure reliability, accuracy, and efficiency.
| Aspect | Best Practices |
|---|---|
| Data Ingestion | Implement 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 & Logging | Comprehensive 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 & Scalability | Optimize 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 Validation | Implement checks for data consistency (e.g., matching trade IDs, correct timestamps, valid prices/quantities). Cross-validate total trade counts and aggregated volumes where possible. |
| Security | Secure 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 & Alerting | Set 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. |
| Auditability | Maintain 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 Management | Externalize configuration parameters (e.g., exchange API endpoints, reconciliation thresholds, date ranges) to make the system flexible and easy to update without code changes. |
| Idempotency | Design 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 Handling | Standardize 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.