Mathew K Analytics

Lesson 19 · Python for Banking and Finance

Understanding End-of-Day Processing Logic in Banking with Python

We will learn how banks process transactions and balances at the end of each day. EOD logic is crucial for accurate account balances, interest calculation,…

📓 Full notebook

Download .ipynb

End-of-Day (EOD) Processing Logic in Banking#

  • We will learn how banks process transactions and balances at the end of each day.
  • EOD logic is crucial for accurate account balances, interest calculation, and regulatory reports.
  • You will build Python tools to mimic real-world EOD processing using synthetic banking data.
  • By the end, you will know how to summarize transactions, calculate balances, and spot possible issues.
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')
np.random.seed(42)

Understanding the Data for EOD Processing#

  • We will use sample banking transactions, customers, and accounts.
  • Transactions record debits and credits by customer and account.
  • Accounts link to customers and types (savings, cheque, credit, etc).
  • Common pitfalls: incorrect grouping, missing timestamps, or misclassified transactions.
n_transactions = 1000
n_customers = 200
df = pd.DataFrame({
    'transaction_id': range(1, n_transactions + 1),
    'customer_id': np.random.choice([f'CUST_{i:04d}' for i in range(1, n_customers + 1)], n_transactions),
    'amount': np.round(np.random.normal(150, 60, n_transactions), 2),
    'transaction_type': np.random.choice(['Debit', 'Credit'], n_transactions),
    'channel': np.random.choice(['ATM', 'Online', 'Branch', 'POS'], n_transactions),
    'date': pd.date_range(start='2024-01-01', periods=n_transactions, freq='h')
})
print(df.shape)
print(df.head(3))
(1000, 6)
   transaction_id customer_id  amount transaction_type channel  \
0               1   CUST_0103  238.77           Credit     ATM   
1               2   CUST_0180  269.17           Credit     POS   
2               3   CUST_0093   58.62           Credit  Online   

                 date  
0 2024-01-01 00:00:00  
1 2024-01-01 01:00:00  
2 2024-01-01 02:00:00  
customer_ids = [f'CUST_{i:04d}' for i in range(1, 201)]
customers = pd.DataFrame({
    'customer_id': customer_ids,
    'segment': ['Retail'] * 150 + ['Business'] * 50,
    'region': ['Metro'] * 100 + ['Regional'] * 100
})
print(customers.shape)
print(customers.head(3))
(200, 3)
  customer_id segment region
0   CUST_0001  Retail  Metro
1   CUST_0002  Retail  Metro
2   CUST_0003  Retail  Metro
accounts = pd.DataFrame({
    'account_id': [f'ACC_{i:05d}' for i in range(1, 201)],
    'customer_id': customer_ids,
    'account_type': np.random.choice(['Savings', 'Cheque', 'Credit'], size=200),
    'open_date': pd.date_range(start='2015-01-01', periods=200, freq='30D')
})
print(accounts.shape)
print(accounts.head(3))
(200, 4)
  account_id customer_id account_type  open_date
0  ACC_00001   CUST_0001       Credit 2015-01-01
1  ACC_00002   CUST_0002      Savings 2015-01-31
2  ACC_00003   CUST_0003      Savings 2015-03-02

Beginner Example 1: Daily Transaction Counts#

  • A simple EOD check is to count the number of transactions per day.
  • This shows overall banking activity and helps detect spikes or outages.
daily_counts = df.groupby(df['date'].dt.date).size()
print(daily_counts.head(3))
date
2024-01-01    24
2024-01-02    24
2024-01-03    24
dtype: int64

Beginner Example 2: Daily Totals for Debits and Credits#

  • EOD processing checks that money in and out is tracked daily.
  • Let us summarize daily totals by transaction type.
daily_sums = df.groupby([df['date'].dt.date, 'transaction_type'])['amount'].sum().unstack().fillna(0)
print(daily_sums.head(3))
transaction_type   Credit    Debit
date                              
2024-01-01        2549.47  1222.29
2024-01-02        2531.83  1462.02
2024-01-03        1970.79  1807.13

Beginner Example 3: Find Missing Transaction Days#

  • Sometimes no transactions are recorded on holidays or weekends.
  • Missing days can cause confusion or errors in EOD routines.
all_days = pd.date_range(df['date'].min().date(), df['date'].max().date(), freq='D')
transaction_days = pd.to_datetime(daily_counts.index)
missing_days = set(all_days.date) - set(transaction_days.date)
print(sorted(list(missing_days))[:5])
[]

Intermediate Example 1: EOD Account Balance Calculation#

  • EOD balance is a core step in all account processing.
  • For each account and day, calculate the running balance.
df['amount_signed'] = np.where(df['transaction_type'] == 'Debit', -df['amount'], df['amount'])
df_merged = df.merge(accounts[['account_id', 'customer_id']], on='customer_id')
eod_balances = df_merged.groupby(['account_id', df_merged['date'].dt.date])['amount_signed'].sum().groupby('account_id').cumsum()
eod_balances = eod_balances.reset_index(name='balance')
print(eod_balances.head(5))
  account_id        date  balance
0  ACC_00001  2024-01-06   166.63
1  ACC_00001  2024-01-21     4.00
2  ACC_00001  2024-01-23  -121.43
3  ACC_00001  2024-01-30   -34.93
4  ACC_00001  2024-01-31  -200.02
acct_summary = eod_balances.groupby('account_id').tail(1).sort_values(by='balance', ascending=False)
print(acct_summary.head(5))
    account_id        date  balance
666  ACC_00144  2024-02-07  1012.19
760  ACC_00161  2024-02-03   824.86
287  ACC_00061  2024-02-09   815.00
477  ACC_00104  2024-02-07   800.81
320  ACC_00069  2024-02-06   743.06

Intermediate Example 2: Interest Accrual at EOD#

  • Banks often calculate interest at EOD, especially for savings accounts.
  • We will estimate interest on balances above $1000.
interest_rate = 0.01 / 365
eod_interest = eod_balances.copy()
eod_interest['interest'] = np.where(eod_interest['balance'] > 1000, eod_interest['balance'] * interest_rate, 0)
print(eod_interest[['account_id', 'balance', 'interest']].head(5))
  account_id  balance  interest
0  ACC_00001   166.63       0.0
1  ACC_00001     4.00       0.0
2  ACC_00001  -121.43       0.0
3  ACC_00001   -34.93       0.0
4  ACC_00001  -200.02       0.0
total_interest = eod_interest.groupby('account_id')['interest'].sum().sort_values(ascending=False)
print(total_interest.head(5))
account_id
ACC_00144    0.126666
ACC_00001    0.000000
ACC_00003    0.000000
ACC_00004    0.000000
ACC_00005    0.000000
Name: interest, dtype: float64

Intermediate Example 3: Suspicious Transaction Patterns at EOD#

  • EOD systems must flag suspicious transaction spikes for review.
  • Let us mark days where a customer makes more than four transactions in one day.
suspicious = df.groupby([df['customer_id'], df['date'].dt.date]).size().reset_index(name='num_tx')
flagged = suspicious[suspicious['num_tx'] > 4]
print(flagged.head(5))
Empty DataFrame
Columns: [customer_id, date, num_tx]
Index: []

Advanced Example 1: EOD Region-Wise Consolidated Balances#

  • Banks aggregate customer balances by region for reporting and liquidity management.
  • Let us summarize final EOD balances by customer region.
acct_region = acct_summary.merge(accounts[['account_id', 'customer_id']], on='account_id')
acct_region = acct_region.merge(customers[['customer_id', 'region']], on='customer_id')
region_totals = acct_region.groupby('region')['balance'].sum()
print(region_totals)
region
Metro      -420.21
Regional    159.41
Name: balance, dtype: float64

Advanced Example 2: EOD Closure File Generation#

  • EOD systems normally write daily closure files with balances and summaries for audit.
  • Let us export the end-of-day balance data for compliance review.
closure_file = 'eod_closure_sample.csv'
acct_summary.to_csv(closure_file, index=False)
print(f'Closure file created: {closure_file}')
Closure file created: eod_closure_sample.csv

Error Handling Example: Invalid or Duplicate Transactions#

  • EOD processing should catch errors such as duplicate transactions or invalid amounts.
  • Let us check for negative or zero transaction amounts and duplicate IDs.
invalid = df[df['amount'] <= 0]
dupes = df[df['transaction_id'].duplicated()]
print('Invalid amount rows:', len(invalid))
print('Duplicate transaction IDs:', len(dupes))
Invalid amount rows: 7
Duplicate transaction IDs: 0
# Remove invalid transactions for clean EOD processing
df_clean = df[(df['amount'] > 0) & (~df['transaction_id'].duplicated())]
print(df_clean.shape)
(993, 7)

Best Practices and Patterns for EOD Processing#

  • Always clean input data for out-of-range or duplicate values.
  • Include audit steps (logging, export files) in your logic.
  • Use clear groupby and merge steps to avoid mixing up customer or account data.
  • Separate code for transaction processing and reporting.

End-to-End Mini Problem: Balance Proof Across Days#

  • Suppose auditors ask to prove that the closing balance for day N matches the opening for day N+1 for one account.
  • Let us fetch one account's day-by-day balances and check for mismatches.
acct_id = acct_summary.iloc[0]['account_id']
account_balances = eod_balances[eod_balances['account_id'] == acct_id].sort_values('date').reset_index(drop=True)
account_balances['prev_balance'] = account_balances['balance'].shift(1)
account_balances['diff'] = account_balances['balance'] - account_balances['prev_balance'].fillna(0)
print(account_balances[['date', 'balance', 'prev_balance', 'diff']].head(5))
         date  balance  prev_balance    diff
0  2024-01-11   218.58           NaN  218.58
1  2024-01-12   316.46        218.58   97.88
2  2024-01-18   587.68        316.46  271.22
3  2024-01-20   755.47        587.68  167.79
4  2024-01-22  1191.88        755.47  436.41

Congratulations! You have built an End-of-Day Processing Engine#

  • You tracked daily transactions, calculated balances, checked errors, and generated EOD closure files.

  • Practice: Can you add more EOD checks, such as overdraft detection?

  • For more banking Python tutorials, search YouTube for "Finance Python EOD" and subscribe to learn more.

Found this useful?

All lessons, notebooks and datasets here are free. If they helped you, a coffee keeps new lessons coming.