/home/devscfvi/web.devsquantum.com/js/import.js
// ============================================
// TRADE IMPORT MODULE
// ============================================
const importState = {
format: 'app', // 'app' | 'binance' | 'coinbase' | 'ibkr'
parsedTrades: [],
fileName: null,
rawText: ''
};
// ============================================
// RENDER IMPORT VIEW
// ============================================
function renderImport() {
const terms = getTerms();
return `
<div class="min-h-screen p-4 md:p-6">
<div class="max-w-3xl mx-auto">
<!-- Header -->
<div class="flex items-center justify-between mb-6 pt-4">
<div>
<h1 class="text-2xl font-bold text-white">Import Trades</h1>
<p class="text-sm text-violet-300">Import from CSV or exchange export</p>
</div>
<button onclick="navigateTo('more')"
class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-3 rounded-xl shadow-lg active:scale-95 transition font-medium">
Back
</button>
</div>
<div class="space-y-4">
<!-- Step 1: Format Selection -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<div class="flex items-center gap-2 mb-4">
<div class="w-6 h-6 rounded-full gradient-bg flex items-center justify-center text-xs font-bold text-white">1</div>
<h3 class="text-lg font-semibold text-white">Choose Import Format</h3>
</div>
<div class="space-y-2">
<button onclick="selectImportFormat('app')" id="format-btn-app"
class="import-format-btn w-full text-left p-4 rounded-xl border transition active:scale-[0.99]
${importState.format === 'app'
? 'border-violet-500 bg-violet-500/15'
: 'border-violet-500/20 bg-slate-900/50 hover:border-violet-500/50'}">
<div class="flex items-center gap-3">
<span class="text-2xl">๐</span>
<div>
<div class="font-semibold text-white">App Export CSV</div>
<div class="text-xs text-slate-400 mt-0.5">Re-import trades exported from this app</div>
</div>
${importState.format === 'app' ? '<svg class="w-5 h-5 text-violet-400 ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
</div>
</button>
<button onclick="selectImportFormat('binance')" id="format-btn-binance"
class="import-format-btn w-full text-left p-4 rounded-xl border transition active:scale-[0.99]
${importState.format === 'binance'
? 'border-yellow-500 bg-yellow-500/10'
: 'border-violet-500/20 bg-slate-900/50 hover:border-violet-500/50'}">
<div class="flex items-center gap-3">
<span class="text-2xl">๐ก</span>
<div>
<div class="font-semibold text-white">Binance Trade History</div>
<div class="text-xs text-slate-400 mt-0.5">Orders โ Trade History โ Export โ CSV</div>
</div>
${importState.format === 'binance' ? '<svg class="w-5 h-5 text-yellow-400 ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
</div>
</button>
<button onclick="selectImportFormat('coinbase')" id="format-btn-coinbase"
class="import-format-btn w-full text-left p-4 rounded-xl border transition active:scale-[0.99]
${importState.format === 'coinbase'
? 'border-blue-500 bg-blue-500/10'
: 'border-violet-500/20 bg-slate-900/50 hover:border-violet-500/50'}">
<div class="flex items-center gap-3">
<span class="text-2xl">๐ต</span>
<div>
<div class="font-semibold text-white">Coinbase Transaction History</div>
<div class="text-xs text-slate-400 mt-0.5">Reports โ Generate Report โ CSV</div>
</div>
${importState.format === 'coinbase' ? '<svg class="w-5 h-5 text-blue-400 ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
</div>
</button>
<button onclick="selectImportFormat('ibkr')" id="format-btn-ibkr"
class="import-format-btn w-full text-left p-4 rounded-xl border transition active:scale-[0.99]
${importState.format === 'ibkr'
? 'border-orange-500 bg-orange-500/10'
: 'border-violet-500/20 bg-slate-900/50 hover:border-violet-500/50'}">
<div class="flex items-center gap-3">
<span class="text-2xl">๐ </span>
<div>
<div class="font-semibold text-white">IBKR โ Transaction History CSV</div>
<div class="text-xs text-slate-400 mt-0.5">Reports โ Activity โ Transaction History โ Download</div>
</div>
${importState.format === 'ibkr' ? '<svg class="w-5 h-5 text-orange-400 ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
</div>
</button>
<button onclick="selectImportFormat('ibkr-tlg')" id="format-btn-ibkr-tlg"
class="import-format-btn w-full text-left p-4 rounded-xl border transition active:scale-[0.99]
${importState.format === 'ibkr-tlg'
? 'border-orange-400 bg-orange-400/10'
: 'border-violet-500/20 bg-slate-900/50 hover:border-violet-500/50'}">
<div class="flex items-center gap-3">
<span class="text-2xl">๐ถ</span>
<div>
<div class="font-semibold text-white">IBKR โ .tlg File <span class="text-xs bg-green-500/20 text-green-300 px-1.5 py-0.5 rounded ml-1">Recommended</span></div>
<div class="text-xs text-slate-400 mt-0.5">Reports โ Activity โ Trades โ Export as .tlg (TraderVue format)</div>
</div>
${importState.format === 'ibkr-tlg' ? '<svg class="w-5 h-5 text-orange-300 ml-auto flex-shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/></svg>' : ''}
</div>
</button>
</div>
</div>
<!-- Step 2: File Upload -->
<div class="glass rounded-3xl p-6 border border-violet-500/20">
<div class="flex items-center gap-2 mb-4">
<div class="w-6 h-6 rounded-full gradient-bg flex items-center justify-center text-xs font-bold text-white">2</div>
<h3 class="text-lg font-semibold text-white">Upload CSV File</h3>
</div>
<input type="file" id="import-file-input" accept=".csv,.txt,.tlg"
onchange="handleImportFile(this)" class="hidden">
<div onclick="document.getElementById('import-file-input').click()"
class="border-2 border-dashed border-violet-500/30 rounded-xl p-8 text-center cursor-pointer hover:border-violet-400/60 transition group">
<svg class="w-12 h-12 mx-auto text-violet-400/40 group-hover:text-violet-400/60 mb-3 transition" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
<p id="import-file-label" class="text-slate-300 font-medium">
${importState.fileName || 'Click to upload or drag & drop'}
</p>
<p class="text-xs text-slate-500 mt-1">CSV or .tlg files ยท Max 5MB</p>
</div>
${importState.fileName ? `
<div class="mt-3 flex items-center gap-2 text-sm text-green-400">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
</svg>
${importState.fileName} loaded โ ${importState.parsedTrades.length} trade(s) found
</div>
` : ''}
</div>
<!-- Step 3: Preview -->
<div id="import-preview"></div>
</div>
</div>
</div>
`;
}
// Select format and re-render
function selectImportFormat(fmt) {
importState.format = fmt;
// Re-parse if we already have raw text
if (importState.rawText) {
importState.parsedTrades = parseCSVFile(importState.rawText, fmt);
}
const app = document.getElementById('app');
if (app) app.innerHTML = renderImport();
renderImportPreview();
}
// Handle file selection
function handleImportFile(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
if (file.size > 5 * 1024 * 1024) {
showToast('File too large (max 5MB)', 'error');
return;
}
importState.fileName = file.name;
const reader = new FileReader();
reader.onload = (e) => {
importState.rawText = e.target.result;
importState.parsedTrades = parseCSVFile(importState.rawText, importState.format);
// Re-render page with updated state
const app = document.getElementById('app');
if (app) app.innerHTML = renderImport();
renderImportPreview();
};
reader.readAsText(file);
}
// ============================================
// CSV PARSERS
// ============================================
function parseCSVFile(text, format) {
switch (format) {
case 'binance': return parseBinanceCSV(text);
case 'coinbase': return parseCoinbaseCSV(text);
case 'ibkr': return parseIBKRCSV(text);
case 'ibkr-tlg': return parseIBKRTLGFile(text);
default: return parseAppExportCSV(text);
}
}
// Parse a single CSV line (handles quoted fields)
function parseCSVLine(line) {
const result = [];
let inQuotes = false;
let current = '';
for (let i = 0; i < line.length; i++) {
const ch = line[i];
if (ch === '"') {
inQuotes = !inQuotes;
} else if (ch === ',' && !inQuotes) {
result.push(current.trim());
current = '';
} else {
current += ch;
}
}
result.push(current.trim());
return result;
}
// Parse all lines into array of row arrays
function parseCSVRows(text) {
return text
.split(/\r?\n/)
.filter(l => l.trim().length > 0)
.map(parseCSVLine);
}
// ---- App Export CSV Parser ----
// Headers: Date,Coin,Category,Order Type,Position,Entry,SL%,TP%,SL Price,TP Price,Max Loss,Max Profit,Fees,R:R,Status,Emotion,Discipline,Mistake,Comment
function parseAppExportCSV(text) {
const rows = parseCSVRows(text);
if (rows.length < 2) return [];
const header = rows[0].map(h => h.toLowerCase().trim());
const trades = [];
const idx = (name) => header.findIndex(h => h.includes(name));
const dateIdx = Math.max(idx('date'), 0);
const coinIdx = idx('coin');
const catIdx = idx('category');
const typeIdx = idx('order');
const posIdx = idx('position');
const entryIdx = idx('entry');
const slpIdx = idx('sl%');
const tpPIdx = idx('tp%');
const slIdx = idx('sl price');
const tpIdx = idx('tp price');
const lossIdx = idx('max loss');
const profIdx = idx('max profit');
const feeIdx = idx('fee');
const rrIdx = idx('r:r');
const statIdx = idx('status');
const emIdx = idx('emotion');
const miscIdx = idx('mistake');
const cmtIdx = idx('comment');
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
if (r.length < 5) continue;
trades.push({
coin: r[coinIdx] || '',
positionSize: parseFloat(r[posIdx]) || 0,
entryPrice: parseFloat(r[entryIdx]) || 0,
stopLossPercent: parseFloat(r[slpIdx]) || 0,
takeProfitPercent: parseFloat(r[tpPIdx]) || 0,
slPrice: parseFloat(r[slIdx]) || 0,
tpPrice: parseFloat(r[tpIdx]) || 0,
lossAmount: parseFloat(r[lossIdx]) || 0,
profitAmount: parseFloat(r[profIdx]) || 0,
rewardRiskRatio: parseFloat(r[rrIdx]) || 0,
fees: parseFloat(r[feeIdx]) || 0,
orderType: r[typeIdx] || 'market',
category: r[catIdx] || null,
emotion: r[emIdx] || null,
mistakeType: r[miscIdx] || null,
comment: r[cmtIdx] || '',
status: r[statIdx] || 'pending',
quantity: 0
});
}
return trades;
}
// ---- Binance CSV Parser ----
// Headers: Date(UTC),Pair,Side,Price,Executed,Amount,Fee
function parseBinanceCSV(text) {
const rows = parseCSVRows(text);
if (rows.length < 2) return [];
const header = rows[0].map(h => h.toLowerCase().replace(/[^a-z]/g, ''));
const findCol = (...names) => {
for (const n of names) {
const i = header.findIndex(h => h.includes(n));
if (i >= 0) return i;
}
return -1;
};
const dateIdx = findCol('date', 'time');
const pairIdx = findCol('pair', 'symbol', 'market');
const sideIdx = findCol('side', 'type', 'direction');
const priceIdx = findCol('price', 'avgprice');
const execIdx = findCol('executed', 'qty', 'quantity');
const amtIdx = findCol('amount', 'total', 'value');
const feeIdx = findCol('fee', 'commission');
const stripNum = (s) => parseFloat((s || '').toString().replace(/[^0-9.]/g, '')) || 0;
const stripCoin = (pair) => {
pair = (pair || '').toUpperCase();
return pair
.replace(/(USDT|BUSD|USDC|TUSD|BTC|ETH|BNB|USD)$/, '')
|| pair;
};
const trades = [];
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
if (r.length < 4) continue;
const side = (r[sideIdx] || '').toUpperCase();
if (side !== 'BUY') continue; // Only import BUY entries as new trade setups
const coin = stripCoin(r[pairIdx]);
const entryPrice = stripNum(r[priceIdx]);
const positionSize = stripNum(r[amtIdx]);
const quantity = stripNum(r[execIdx]);
const fees = stripNum(r[feeIdx]);
trades.push({
coin,
positionSize,
entryPrice,
stopLossPercent: 0,
takeProfitPercent: 0,
slPrice: 0,
tpPrice: 0,
lossAmount: 0,
profitAmount: 0,
rewardRiskRatio: 0,
fees,
quantity,
orderType: 'market',
category: null,
emotion: null,
mistakeType:null,
comment: `Imported from Binance โ ${r[dateIdx] || ''}`,
status: 'pending'
});
}
return trades;
}
// ---- Coinbase CSV Parser ----
// Headers: Timestamp,Transaction Type,Asset,Quantity Transacted,Spot Price Currency,Spot Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes
function parseCoinbaseCSV(text) {
// Skip any leading metadata rows (Coinbase adds header info before the CSV header)
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
let headerIdx = lines.findIndex(l => l.toLowerCase().includes('timestamp') || l.toLowerCase().includes('transaction type'));
if (headerIdx < 0) headerIdx = 0;
const csvText = lines.slice(headerIdx).join('\n');
const rows = parseCSVRows(csvText);
if (rows.length < 2) return [];
const header = rows[0].map(h => h.toLowerCase().trim());
const findCol = (...names) => {
for (const n of names) {
const i = header.findIndex(h => h.includes(n));
if (i >= 0) return i;
}
return -1;
};
const tsIdx = findCol('timestamp', 'date');
const typeIdx = findCol('transaction type', 'type');
const assetIdx = findCol('asset', 'coin', 'currency');
const qtyIdx = findCol('quantity transacted', 'amount', 'quantity');
const priceIdx = findCol('spot price at transaction', 'price', 'spot price');
const subtlIdx = findCol('subtotal');
const feeIdx = findCol('fees', 'fee');
const stripNum = (s) => parseFloat((s || '').toString().replace(/[^0-9.]/g, '')) || 0;
const trades = [];
for (let i = 1; i < rows.length; i++) {
const r = rows[i];
if (r.length < 4) continue;
const txType = (r[typeIdx] || '').toLowerCase();
if (!txType.includes('buy')) continue; // Only import Buy transactions
const coin = (r[assetIdx] || '').toUpperCase().trim();
const entryPrice = stripNum(r[priceIdx]);
const quantity = stripNum(r[qtyIdx]);
const positionSize = subtlIdx >= 0 ? stripNum(r[subtlIdx]) : entryPrice * quantity;
const fees = stripNum(r[feeIdx]);
trades.push({
coin,
positionSize,
entryPrice,
stopLossPercent: 0,
takeProfitPercent: 0,
slPrice: 0,
tpPrice: 0,
lossAmount: 0,
profitAmount: 0,
rewardRiskRatio: 0,
fees,
quantity,
orderType: 'market',
category: null,
emotion: null,
mistakeType:null,
comment: `Imported from Coinbase โ ${r[tsIdx] || ''}`,
status: 'pending'
});
}
return trades;
}
// ---- Interactive Brokers (IBKR) CSV Parser ----
// Format: multi-section CSV
// Header row: Transaction History,Header,Date,Account,Description,Transaction Type,Symbol,Quantity,Price,Price Currency,Gross Amount,Commission,Net Amount
// Data rows: Transaction History,Data,2026-02-19,U***03200,DERMATA...,Buy,DRMA,10.0,1.89,USD,-18.9,-0.219,-19.119
//
// Logic: Group multiple Buy/Sell fills by Symbol+Date โ one reconstructed trade per symbol-day
function parseIBKRCSV(text) {
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0);
// Find the Transaction History header row and collect data rows
let headerCols = null;
const dataRows = [];
for (const line of lines) {
const cols = parseCSVLine(line);
if (!cols || cols.length < 3) continue;
const section = (cols[0] || '').trim();
const rowType = (cols[1] || '').trim();
if (section === 'Transaction History' && rowType === 'Header') {
headerCols = cols;
} else if (section === 'Transaction History' && rowType === 'Data') {
dataRows.push(cols);
}
}
if (!headerCols || dataRows.length === 0) return [];
// Build column index lookup (headerCols[0]='Transaction History', [1]='Header', [2]='Date', ...)
// So the actual header names start at index 2
const hdrNames = headerCols.slice(2).map(h => h.toLowerCase().trim());
const col = (...names) => {
for (const name of names) {
const i = hdrNames.findIndex(h => h.includes(name));
if (i >= 0) return i + 2; // +2 because we sliced 2 off
}
return -1;
};
const dateCol = col('date');
const typeCol = col('transaction type');
const symbolCol = col('symbol');
const qtyCol = col('quantity');
const priceCol = col('price');
const grossCol = col('gross amount');
const commCol = col('commission');
const netCol = col('net amount');
if (symbolCol < 0) return [];
// Filter to valid trade rows (skip cash transfers where Symbol = '-')
const tradeRows = dataRows.filter(r => {
const sym = (r[symbolCol] || '').trim();
return sym && sym !== '-' && sym !== '' && /^[A-Z0-9.]+$/i.test(sym);
});
if (tradeRows.length === 0) return [];
// Group by Symbol+Date (multiple fills per day = one trade)
const groups = {};
for (const r of tradeRows) {
const symbol = (r[symbolCol] || '').trim().toUpperCase();
// Date field may include time like "2026-02-19, 10:30" โ grab just the date part
const rawDate = (r[dateCol] || '').trim();
const date = rawDate.split(',')[0].trim();
const key = `${symbol}__${date}`;
if (!groups[key]) groups[key] = { symbol, date, rows: [] };
groups[key].rows.push(r);
}
const trades = [];
for (const group of Object.values(groups)) {
const { symbol, date, rows } = group;
let totalBuyQty = 0;
let totalSellQty = 0;
let totalBuyGross = 0; // sum of abs(Gross Amount) for buy fills
let totalSellGross = 0; // sum of abs(Gross Amount) for sell fills
let totalFees = 0; // sum of abs(Commission) for all fills
let totalNetPnl = 0; // sum of Net Amount for all fills
for (const r of rows) {
const qty = parseFloat(r[qtyCol]) || 0;
const gross = parseFloat(r[grossCol]) || 0;
const comm = parseFloat(r[commCol]) || 0;
const net = parseFloat(r[netCol]) || 0;
const txType = (r[typeCol] || '').toLowerCase();
const isBuy = txType.includes('buy') || qty > 0;
const isSell = txType.includes('sell') || qty < 0;
if (isBuy) {
totalBuyQty += Math.abs(qty);
totalBuyGross += Math.abs(gross);
}
if (isSell) {
totalSellQty += Math.abs(qty);
totalSellGross += Math.abs(gross);
}
totalFees += Math.abs(comm);
totalNetPnl += net;
}
// Entry/exit price = weighted average of buy/sell fills
const entryPrice = totalBuyQty > 0 ? totalBuyGross / totalBuyQty : 0;
const exitPrice = totalSellQty > 0 ? totalSellGross / totalSellQty : 0;
const positionSize = totalBuyGross;
// Determine if trade is closed (sell qty โ buy qty, allow 1% rounding tolerance)
const isClosed = totalSellQty > 0 && (totalSellQty >= totalBuyQty * 0.99);
let status = 'pending';
let profitAmount = 0;
let lossAmount = 0;
if (isClosed) {
if (totalNetPnl > 0.005) {
status = 'win';
profitAmount = parseFloat(totalNetPnl.toFixed(2));
} else if (totalNetPnl < -0.005) {
status = 'loss';
lossAmount = parseFloat(Math.abs(totalNetPnl).toFixed(2));
} else {
status = 'pending'; // breakeven
}
}
const pnlLabel = isClosed
? ` | P&L: ${totalNetPnl >= 0 ? '+' : ''}$${totalNetPnl.toFixed(2)}`
: ' | Open position';
trades.push({
coin: symbol,
positionSize: parseFloat(positionSize.toFixed(2)),
entryPrice: parseFloat(entryPrice.toFixed(4)),
actualExitPrice: exitPrice > 0 ? parseFloat(exitPrice.toFixed(4)) : 0,
tradeDate: date, // actual IBKR trade date
stopLossPercent: 0,
takeProfitPercent: 0,
slPrice: 0,
tpPrice: 0,
lossAmount,
profitAmount,
rewardRiskRatio: 0,
fees: parseFloat(totalFees.toFixed(4)),
quantity: parseFloat(totalBuyQty.toFixed(4)),
orderType: 'market',
category: null,
emotion: null,
mistakeType: null,
comment: `Imported from IBKR โ ${date}${pnlLabel}`,
status,
_ibkrPnl: parseFloat(totalNetPnl.toFixed(2)) // used in preview table
});
}
// Sort by date descending
trades.sort((a, b) => b.tradeDate.localeCompare(a.tradeDate));
return trades;
}
// ---- IBKR .tlg Parser (Recommended) ----
// Format: pipe-delimited per-execution file used by TraderVue / IBKR export
// STK_TRD|<id>|<symbol>|<name>|<exchange>|<action>|<O/C>|<YYYYMMDD>|<HH:MM:SS>|<currency>|<qty>|<multiplier>|<price>|<value>|<commission>|<mult2>
// action: BUYTOOPEN | SELLTOCLOSE | BUYTOCOVER | SELLSHORT
// value: positive for buys (cost), negative for sells (proceeds)
// commission: always negative
//
// P&L formula: proceeds_from_sells - cost_of_buys - total_fees
// = -sum(value) for sells + (-sum(value)) for buys... simplified:
// = sum( -value - abs(commission) ) for all rows
// ---- IBKR .tlg Parser (Recommended) โ UPDATED ----
// Changes:
// 1. Splits multiple round-trips on the same symbol+day into separate trades
// by tracking open position qty and cutting a new trade whenever it hits 0.
// 2. Calculates exitPricePercent (% gain/loss from entry to exit).
function parseIBKRTLGFile(text) {
const lines = text.split(/\r?\n/).filter(l => l.trim().startsWith('STK_TRD'));
// โโ Parse every execution line into a structured row โโโโโโโโโโโโโโโโโโโโโโ
const executions = [];
for (const line of lines) {
const f = line.split('|');
if (f.length < 10) continue;
// Symbol: first ALL-CAPS alphabetic token (1-6 chars) after f[0]
let symbol = '';
for (let fi = 1; fi < f.length; fi++) {
const v = f[fi].trim();
if (/^[A-Z]{1,6}$/.test(v) && v !== 'BUY' && v !== 'SELL') { symbol = v; break; }
}
if (!symbol) symbol = (f[2] || '').trim().toUpperCase();
if (!symbol) continue;
// Action: first field matching BUY/SELL variants
let action = '';
for (let fi = 1; fi < f.length; fi++) {
const v = f[fi].trim().toUpperCase();
if (/^(BUY|SELL|BUYTOOPEN|BUYTOCOVER|SELLTOCLOSE|SELLSHORT)/.test(v)) { action = v; break; }
}
// Date: first 8-digit field looking like 20YYMMDD
let dateRaw = '';
for (let fi = 1; fi < f.length; fi++) {
const v = f[fi].trim();
if (/^20\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])$/.test(v)) { dateRaw = v; break; }
}
// Time: first field matching HH:MM:SS
let timeStr = '';
for (let fi = 1; fi < f.length; fi++) {
const v = f[fi].trim();
if (/^([01]\d|2[0-3]):[0-5]\d:[0-5]\d$/.test(v)) { timeStr = v; break; }
}
const qty = parseFloat((f[10] || '0').trim()) || 0;
const price = parseFloat((f[12] || '0').trim()) || 0;
const value = parseFloat((f[13] || '0').trim()) || 0;
const comm = parseFloat((f[14] || '0').trim()) || 0;
const date = dateRaw.length === 8
? `${dateRaw.slice(0, 4)}-${dateRaw.slice(4, 6)}-${dateRaw.slice(6, 8)}`
: (dateRaw || 'unknown');
executions.push({ symbol, action, date, time: timeStr, qty, price, value, comm });
}
// โโ Sort all executions by symbol, date, time so we can walk them in order โ
executions.sort((a, b) => {
if (a.symbol !== b.symbol) return a.symbol.localeCompare(b.symbol);
const da = `${a.date}T${a.time}`;
const db = `${b.date}T${b.time}`;
return da.localeCompare(db);
});
// โโ Walk executions per symbol, splitting on position-close boundaries โโโโโ
// Group executions by symbol first
const bySymbol = {};
for (const ex of executions) {
if (!bySymbol[ex.symbol]) bySymbol[ex.symbol] = [];
bySymbol[ex.symbol].push(ex);
}
const trades = [];
for (const [symbol, execs] of Object.entries(bySymbol)) {
// Walk the executions in time order, accumulating a running net qty.
// When running qty returns to 0 (or flips sign), that's a complete round-trip โ cut trade.
let runningQty = 0;
let currentGroup = [];
for (const ex of execs) {
const isBuy = ex.action === 'BUYTOOPEN' || ex.action === 'BUYTOCOVER' || ex.qty > 0;
const isSell = ex.action === 'SELLTOCLOSE' || ex.action === 'SELLSHORT' || ex.qty < 0;
const signedQty = isBuy ? Math.abs(ex.qty) : -Math.abs(ex.qty);
currentGroup.push(ex);
runningQty += signedQty;
// Position fully closed โ save this trade and start fresh
if (Math.abs(runningQty) < 0.001 && currentGroup.length > 0) {
trades.push(buildTradeFromGroup(symbol, currentGroup));
currentGroup = [];
runningQty = 0;
}
}
// Any remaining open executions = open/partial position
if (currentGroup.length > 0) {
trades.push(buildTradeFromGroup(symbol, currentGroup));
}
}
// Sort by date descending
trades.sort((a, b) => {
const da = `${b.tradeDate}T${b._firstTime || ''}`;
const db = `${a.tradeDate}T${a._firstTime || ''}`;
return da.localeCompare(db);
});
// Clean up internal field before returning
trades.forEach(t => { delete t._firstTime; });
return trades;
}
// โโ Builds a single trade object from a group of execution rows โโโโโโโโโโโโโโโ
function buildTradeFromGroup(symbol, rows) {
let totalBuyQty = 0;
let totalBuyGross = 0;
let totalSellQty = 0;
let totalSellGross = 0;
let totalFees = 0;
let totalNetPnl = 0;
for (const r of rows) {
const isBuy = r.action === 'BUYTOOPEN' || r.action === 'BUYTOCOVER' || r.qty > 0;
const isSell = r.action === 'SELLTOCLOSE' || r.action === 'SELLSHORT' || r.qty < 0;
if (isBuy) {
totalBuyQty += Math.abs(r.qty);
totalBuyGross += Math.abs(r.value);
}
if (isSell) {
totalSellQty += Math.abs(r.qty);
totalSellGross += Math.abs(r.value);
}
totalFees += Math.abs(r.comm);
totalNetPnl += -r.value - Math.abs(r.comm);
}
const entryPrice = totalBuyQty > 0 ? totalBuyGross / totalBuyQty : 0;
const exitPrice = totalSellQty > 0 ? totalSellGross / totalSellQty : 0;
const positionSize = totalBuyGross;
const isClosed = totalSellQty > 0 && totalSellQty >= totalBuyQty * 0.99;
// % gain/loss from entry to exit (positive = profit, negative = loss)
const exitPricePercent = (isClosed && entryPrice > 0)
? parseFloat((((exitPrice - entryPrice) / entryPrice) * 100).toFixed(3))
: 0;
let status = 'pending';
let profitAmount = 0;
let lossAmount = 0;
if (isClosed) {
if (totalNetPnl > 0.005) {
status = 'win';
profitAmount = parseFloat(totalNetPnl.toFixed(2));
} else if (totalNetPnl < -0.005) {
status = 'loss';
lossAmount = parseFloat(Math.abs(totalNetPnl).toFixed(2));
}
}
// Use the date of the first execution in this group
const tradeDate = rows[0].date;
const firstTime = rows[0].time || '';
const pnlLabel = isClosed
? ` | P&L: ${totalNetPnl >= 0 ? '+' : ''}$${totalNetPnl.toFixed(2)} (${exitPricePercent >= 0 ? '+' : ''}${exitPricePercent}%)`
: ' | Open position';
return {
coin: symbol,
positionSize: parseFloat(positionSize.toFixed(2)),
entryPrice: parseFloat(entryPrice.toFixed(4)),
actualExitPrice: exitPrice > 0 ? parseFloat(exitPrice.toFixed(4)) : 0,
exitPricePercent, // โ NEW: % move from entry to exit
tradeDate,
_firstTime: firstTime, // used for sorting, deleted before return
stopLossPercent: 0,
takeProfitPercent: 0,
slPrice: 0,
tpPrice: 0,
lossAmount,
profitAmount,
rewardRiskRatio: 0,
fees: parseFloat(totalFees.toFixed(4)),
quantity: parseFloat(totalBuyQty.toFixed(4)),
orderType: 'market',
category: null,
emotion: null,
mistakeType: null,
comment: `Imported from IBKR (.tlg) โ ${tradeDate}${pnlLabel}`,
status,
_ibkrPnl: parseFloat(totalNetPnl.toFixed(2))
};
}
// ============================================
// PREVIEW RENDERER
// ============================================
function renderImportPreview() {
const container = document.getElementById('import-preview');
if (!container) return;
const trades = importState.parsedTrades;
if (trades.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = `
<div class="glass rounded-3xl p-6 border border-violet-500/20 slide-up">
<div class="flex items-center gap-2 mb-4">
<div class="w-6 h-6 rounded-full gradient-bg flex items-center justify-center text-xs font-bold text-white">3</div>
<h3 class="text-lg font-semibold text-white">Preview โ ${trades.length} Trade(s)</h3>
</div>
<div class="overflow-x-auto rounded-xl border border-violet-500/20 mb-4">
<table class="w-full text-sm">
<thead>
<tr class="bg-violet-500/10 text-violet-300 text-left">
<th class="px-4 py-3 font-semibold whitespace-nowrap">Asset</th>
${(importState.format === 'ibkr' || importState.format === 'ibkr-tlg') ? `
<th class="px-4 py-3 font-semibold whitespace-nowrap text-amber-300">Trade Date</th>
` : ''}
<th class="px-4 py-3 font-semibold whitespace-nowrap">Position</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Entry</th>
${(importState.format === 'ibkr' || importState.format === 'ibkr-tlg') ? `
<th class="px-4 py-3 font-semibold whitespace-nowrap">Exit</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Qty</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Fees</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Net P&L</th>
` : `
<th class="px-4 py-3 font-semibold whitespace-nowrap">SL%</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">TP%</th>
`}
<th class="px-4 py-3 font-semibold whitespace-nowrap">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-violet-500/10">
${trades.slice(0, 25).map(t => `
<tr class="text-slate-300 hover:bg-violet-500/5 transition">
<td class="px-4 py-2.5 font-semibold text-white">${t.coin || 'โ'}</td>
${(importState.format === 'ibkr' || importState.format === 'ibkr-tlg') ? `
<td class="px-4 py-2.5 font-mono text-amber-300 text-xs whitespace-nowrap">${t.tradeDate || 'โ unknown'}</td>
` : ''}
<td class="px-4 py-2.5">${t.positionSize > 0 ? '$' + parseFloat(t.positionSize).toFixed(2) : 'โ'}</td>
<td class="px-4 py-2.5">${t.entryPrice > 0 ? '$' + parseFloat(t.entryPrice).toFixed(4) : 'โ'}</td>
${(importState.format === 'ibkr' || importState.format === 'ibkr-tlg') ? `
<td class="px-4 py-2.5 text-slate-300">${(t.actualExitPrice || 0) > 0 ? '$' + parseFloat(t.actualExitPrice).toFixed(4) : 'โ'}</td>
<td class="px-4 py-2.5 text-slate-400">${t.quantity > 0 ? parseFloat(t.quantity).toFixed(2) : 'โ'}</td>
<td class="px-4 py-2.5 text-slate-400">${t.fees > 0 ? '$' + parseFloat(t.fees).toFixed(2) : 'โ'}</td>
<td class="px-4 py-2.5 font-semibold ${(t._ibkrPnl || 0) >= 0 ? 'text-green-400' : 'text-red-400'}">
${t._ibkrPnl != null ? ((t._ibkrPnl >= 0 ? '+' : '') + '$' + parseFloat(t._ibkrPnl).toFixed(2)) : 'โ'}
</td>
` : `
<td class="px-4 py-2.5 text-red-300">${t.stopLossPercent > 0 ? t.stopLossPercent + '%' : 'โ'}</td>
<td class="px-4 py-2.5 text-green-300">${t.takeProfitPercent > 0 ? t.takeProfitPercent + '%' : 'โ'}</td>
`}
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs font-semibold
${t.status === 'win' ? 'bg-green-500/20 text-green-300'
: t.status === 'loss' ? 'bg-red-500/20 text-red-300'
: 'bg-yellow-500/20 text-yellow-300'}">
${t.status || 'pending'}
</span>
</td>
</tr>
`).join('')}
</tbody>
</table>
${trades.length > 25 ? `<p class="text-center text-xs text-slate-500 py-2">... and ${trades.length - 25} more trades</p>` : ''}
</div>
<!-- Notes for exchange imports -->
${importState.format !== 'app' ? `
<div class="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-4 mb-4 text-sm text-yellow-200">
<strong>Note:</strong> Exchange imports create <em>pending</em> trades. SL% and TP% are set to 0 โ you can update each trade after import.
</div>
` : ''}
<!-- Import Button -->
<button onclick="confirmImport()"
class="w-full gradient-bg text-white py-4 rounded-2xl font-bold text-lg shadow-lg active:scale-95 transition">
Import ${trades.length} Trade(s) โ
</button>
</div>
`;
}
// ============================================
// CONFIRM & EXECUTE IMPORT
// ============================================
async function confirmImport() {
const trades = importState.parsedTrades;
if (trades.length === 0) {
showToast('No trades to import', 'error');
return;
}
const mode = getTradeMode();
showToast('Importing...', 'info');
const result = await apiPost('bulkImportTrades', { trades, mode });
if (result.success) {
const msg = result.errors && result.errors.length > 0
? `Imported ${result.imported} trade(s). ${result.errors.length} row(s) had errors.`
: `Successfully imported ${result.imported} trade(s)!`;
showToast(msg, 'success');
// Reset import state
importState.parsedTrades = [];
importState.fileName = null;
importState.rawText = '';
// Navigate to trades list
setTimeout(() => navigateTo('history'), 1200);
} else {
showToast('Import failed. Please check your file format.', 'error');
}
}
// ============================================
// GLOBAL EXPORTS
// ============================================
window.renderImport = renderImport;
window.selectImportFormat = selectImportFormat;
window.handleImportFile = handleImportFile;
window.renderImportPreview = renderImportPreview;
window.confirmImport = confirmImport;
window.importState = importState;