Overview
The Interactive Brokers integration allows you to upload PDF statements and automatically extract trade data using AI-powered OCR and parsing. This is ideal for importing historical trades without requiring API access.
Supports Flex Query statements and standard trade confirmations from Interactive Brokers.
Features
PDF upload and text extraction
AI-powered order detection
Automatic instrument information parsing
FIFO trade matching (First-In-First-Out)
Commission and fee calculation
Futures contract symbol extraction
Multi-page statement support
How It Works
Processing Pipeline
PDF Upload : User uploads IBKR statement PDF
OCR Extraction : Extract text using pdf2json library
Order Parsing : Identify buy/sell orders with regex patterns
Instrument Parsing : Extract contract details (symbol, expiry, multiplier)
FIFO Matching : Match buys with sells using FIFO algorithm
P&L Calculation : Compute profit/loss and commissions
Trade Saving : Store matched trades in Deltalytix format
Setup Instructions
Prerequisites
Interactive Brokers account with trade history
PDF statement downloaded from IBKR portal
Valid PDF file (not password-protected)
Step 1: Upload PDF Statement
Upload your IBKR PDF statement:
const formData = new FormData ()
formData . append ( 'file' , pdfFile )
const response = await fetch ( '/api/imports/ibkr/ocr' , {
method: 'POST' ,
body: JSON . stringify ({
attachments: [{
type: 'application/pdf' ,
content: await pdfFile . arrayBuffer ()
}]
})
})
const { text } = await response . json ()
console . log ( 'Extracted text:' , text )
See app/api/imports/ibkr/ocr/route.ts:52-108
Step 2: Extract Orders and Instruments
Parse the extracted text to identify orders:
const response = await fetch ( '/api/imports/ibkr/extract-orders' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ text: extractedText })
})
const { orders , instruments } = await response . json ()
console . log ( `Found ${ orders . length } orders` )
console . log ( `Identified ${ instruments . length } instruments` )
See app/api/imports/ibkr/extract-orders/route.ts:135-175
Step 3: Match Trades with FIFO
Match buy and sell orders to create complete trades:
const response = await fetch ( '/api/imports/ibkr/fifo-computation' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
orders: orders ,
instruments: instruments
})
})
const trades = await response . json ()
console . log ( `Matched ${ trades . length } complete trades` )
API Reference
POST /api/imports/ibkr/ocr
Extract text from IBKR PDF statement.
Request Body:
{
attachments : [{
type: 'application/pdf'
content : ArrayBuffer | string // PDF content or base64
}]
}
Response:
{
text : string // Extracted text from PDF
}
Example:
const pdfBuffer = await file . arrayBuffer ()
const response = await fetch ( '/api/imports/ibkr/ocr' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
attachments: [{
type: 'application/pdf' ,
content: pdfBuffer
}]
})
})
const { text } = await response . json ()
See app/api/imports/ibkr/ocr/route.ts:52-108
Parse orders and instrument information from extracted text.
Request Body:
{
text : string // Extracted PDF text
}
Response:
{
orders : Array <{
rawSymbol : string // e.g., "MESM5"
side : 'BUY' | 'SELL'
quantity : number
price : number
timestamp : string // ISO 8601
commission : number
accountNumber : string // e.g., "U***1234"
orderId : string // Generated ID
orderType : string // e.g., "O", "C"
}>
instruments : Array <{
symbol : string // e.g., "MESM5"
description : string // e.g., "MES 20JUN25"
conid : string // Contract ID
underlying : string // e.g., "MES"
listingExchange : string // e.g., "CME"
multiplier : number // Contract size
expiry : string // e.g., "2025-06-20"
deliveryMonth : string // e.g., "2025-06"
instrumentType : string // e.g., "Futures"
}>
}
Example:
const response = await fetch ( '/api/imports/ibkr/extract-orders' , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({ text: extractedText })
})
const { orders , instruments } = await response . json ()
orders . forEach ( order => {
console . log ( ` ${ order . side } ${ order . quantity } ${ order . rawSymbol } @ $ ${ order . price } ` )
})
See app/api/imports/ibkr/extract-orders/route.ts:135-175
POST /api/imports/ibkr/fifo-computation
Match orders using FIFO algorithm and compute trade P&L.
Request Body:
{
orders : Order [] // From extract-orders endpoint
instruments : Instrument [] // From extract-orders endpoint
}
Response:
Array <{
instrument : string // Symbol (e.g., "MES")
quantity : number // Contracts traded
entryPrice : string // Entry price
closePrice : string // Exit price
entryDate : string // ISO 8601
closeDate : string // ISO 8601
entryId : string // Entry order ID
closeId : string // Exit order ID
pnl : number // Profit/loss in dollars
commission : number // Total commission
timeInPosition : number // Duration in seconds
side : 'long' | 'short' // Trade direction
orderIds : string [] // All related order IDs
}>
PDF Parsing Details
The OCR endpoint uses pdf2json for reliable PDF processing:
import PDFParser from 'pdf2json'
const pdfParser = new PDFParser ()
pdfParser . on ( 'pdfParser_dataReady' , ( pdfData ) => {
let extractedText = ''
pdfData . Pages . forEach (( page ) => {
page . Texts . forEach (( text ) => {
text . R . forEach (( run ) => {
extractedText += decodeURIComponent ( run . T ) + ' '
})
})
extractedText += ' \n '
})
console . log ( extractedText )
})
pdfParser . parseBuffer ( pdfBuffer )
See app/api/imports/ibkr/ocr/route.ts:6-50
Order Pattern Matching
Orders are extracted using regex patterns:
const orderPattern = /U \*\*\* ( \d + ) \s + ( [ A-Z0-9 ] + ) \s + ( \d {4} - \d {2} - \d {2} ) , \s * ( \d {2} : \d {2} : \d {2} ) \s + ( \d {4} - \d {2} - \d {2} ) \s + - \s + ( BUY | SELL ) \s + ( - ? \d + ) \s + ( [ \d, ] + \. \d + ) \s + ( - ? [ \d, ] + \. \d + ) \s + ( - ? [ \d, ] + \. \d + ) \s + ( [ \d, ] + \. \d + ) \s + ( [ A-Z ] + ) \s + ( [ OC ] ) / g
let match
while (( match = orderPattern . exec ( tradesText )) !== null ) {
const [
_ , accountId , symbol , date , time , settleDate ,
side , quantity , price , value , commission , fee ,
orderType , code
] = match
orders . push ({
rawSymbol: symbol ,
side: side as 'BUY' | 'SELL' ,
quantity: Math . abs ( parseInt ( quantity )),
price: parseFloat ( price . replace ( /,/ g , '' )),
timestamp: ` ${ date } T ${ time } ` ,
commission: parseFloat ( fee . replace ( /,/ g , '' )) +
parseFloat ( commission . replace ( /,/ g , '' )),
accountNumber: `U*** ${ accountId } ` ,
orderId: ` ${ side . charAt ( 0 ) }${ index }${ time . split ( ':' )[ 2 ] } ` ,
orderType: orderType
})
}
See app/api/imports/ibkr/extract-orders/route.ts:18-74
Instrument Parsing
Contract details are extracted from the “Financial Instrument Information” section:
const instrumentPattern = / ( [ A-Z0-9 ] + ) \s + ( [ A-Z0-9 ] + \s + [ A-Z0-9 ] + ) \s + ( \d + ) \s + ( [ A-Z0-9 ] + ) \s + ( [ A-Z ] + ) \s + ( \d + ) \s + ( \d {4} - \d {2} - \d {2} ) \s + ( \d {4} - \d {2} ) / g
while (( match = instrumentPattern . exec ( dataText )) !== null ) {
const [
_ , symbol , description , conid , underlying ,
exchange , multiplier , expiry , deliveryMonth
] = match
instruments . push ({
symbol: symbol . trim (),
description: description . trim (),
conid: conid ,
underlying: underlying ,
listingExchange: exchange ,
multiplier: parseInt ( multiplier ),
expiry: expiry ,
deliveryMonth: deliveryMonth ,
instrumentType: 'Futures'
})
}
See app/api/imports/ibkr/extract-orders/route.ts:77-133
FIFO Trade Matching
Trades are matched using First-In-First-Out (FIFO) algorithm:
Algorithm Overview
Group by Symbol : Separate orders by instrument
Sort by Timestamp : Process in chronological order
Match Buy/Sell : Pair earliest unmatched orders
Calculate P&L : Compute profit/loss per matched pair
Handle Partial Fills : Split orders if quantities don’t match
Example
// Input orders
const orders = [
{ side: 'BUY' , quantity: 2 , price: 5000 , timestamp: '2025-01-01T10:00:00' },
{ side: 'BUY' , quantity: 1 , price: 5010 , timestamp: '2025-01-01T11:00:00' },
{ side: 'SELL' , quantity: 2 , price: 5020 , timestamp: '2025-01-01T12:00:00' },
{ side: 'SELL' , quantity: 1 , price: 5030 , timestamp: '2025-01-01T13:00:00' },
]
// Output trades (FIFO matched)
const trades = [
{
quantity: 2 ,
entryPrice: '5000' ,
closePrice: '5020' ,
pnl: ( 5020 - 5000 ) * 2 * multiplier // Long trade
},
{
quantity: 1 ,
entryPrice: '5010' ,
closePrice: '5030' ,
pnl: ( 5030 - 5010 ) * 1 * multiplier // Long trade
}
]
UI Components
The IBKR integration includes React components for the import flow:
pdf-upload.tsx
Handles PDF file upload and validation:
import PdfUpload from '@/app/[locale]/dashboard/components/import/ibkr-pdf/pdf-upload'
< PdfUpload
onUploadComplete = {(extractedText) => {
console . log ( 'PDF uploaded and processed' )
setExtractedText ( extractedText )
}}
onError = {(error) => {
console . error ( 'Upload failed:' , error )
}}
/>
See app/[locale]/dashboard/components/import/ibkr-pdf/pdf-upload.tsx
pdf-processing.tsx
Displays extracted trades in a table with real-time parsing:
import PdfProcessing from '@/app/[locale]/dashboard/components/import/ibkr-pdf/pdf-processing'
< PdfProcessing
extractedText = { extractedText }
userId = { currentUserId }
processedTrades = { trades }
setProcessedTrades = { setTrades }
setError = { setError }
setStep = { setStep }
/>
See app/[locale]/dashboard/components/import/ibkr-pdf/pdf-processing.tsx:48-536
Features
Real-time order extraction streaming
Interactive trade table with sorting
Hover tooltips showing order details
P&L calculation with commission
Summary totals row
Progress indicators
Error Handling
Error : Invalid file type. Only PDF files are allowed.Cause : Non-PDF file uploadedSolution : Ensure file has .pdf extension and correct MIME type:if ( file . type !== 'application/pdf' ) {
throw new Error ( 'Only PDF files are supported' )
}
Error : PDF processing failed: [reason]Causes :
Password-protected PDF
Corrupted PDF file
Unsupported PDF version
Solution :
Remove password protection from PDF
Download fresh copy from IBKR
Try a different statement period
Error : Returns empty orders arrayCauses :
Statement doesn’t contain trades section
Unexpected PDF format
Different IBKR statement type
Solution :
Ensure PDF is a trade confirmation or Flex Query
Check that “Trades” section exists in PDF
Verify statement date range includes trades
Error : Order validation failedCause : Extracted order doesn’t match expected schemaSolution : Check order schema requirements:const orderSchema = z . object ({
rawSymbol: z . string (),
side: z . enum ([ 'BUY' , 'SELL' ]),
quantity: z . number (). positive (),
price: z . number (). positive (),
timestamp: z . string (), // ISO 8601
commission: z . number (). optional (),
accountNumber: z . string (). optional (),
orderId: z . string (). optional ()
})
Best Practices
Statement Quality Use high-quality PDFs directly from IBKR portal, not scanned copies
Date Ranges Import smaller date ranges (1-3 months) for better accuracy
Validation Always review extracted trades before saving to database
Backup Data Keep original PDF statements as backup records
Limitations
Supports futures contracts primarily (options/stocks may require schema updates)
Requires standard IBKR statement format
Cannot process password-protected PDFs
Text extraction depends on PDF quality
Regex patterns may need updates for different statement versions
Troubleshooting
Orders Not Matching
Check instrument information:
console . log ( 'Instruments:' , instruments )
// Verify symbols match between orders and instruments
Inspect order timestamps:
orders . forEach ( order => {
console . log ( ` ${ order . side } ${ order . rawSymbol } at ${ order . timestamp } ` )
})
// Ensure timestamps are valid ISO 8601
Verify FIFO logic:
Orders must be sorted by timestamp
Buy quantity must match available sell quantity
Partial fills create multiple trades
Incorrect P&L Calculation
Check contract multiplier:
const instrument = instruments . find ( i => i . symbol === order . rawSymbol )
console . log ( 'Multiplier:' , instrument ?. multiplier ) // e.g., 5 for MES
Verify commission calculation:
const totalCommission =
parseFloat ( clearingFee ) +
parseFloat ( exchangeFee ) +
parseFloat ( nfaFee )
Confirm price precision:
const price = parseFloat ( priceString . replace ( /,/ g , '' ))
// Remove thousands separator before parsing
Next Steps
View Imported Trades Review and analyze your IBKR trades
CSV Import Try CSV import for more control