/home/devscfvi/app.devsquantum.com/js/import.js
// ============================================
// TRADE IMPORT MODULE
// ============================================
const importState = {
format: 'app', // 'app' | 'binance' | 'coinbase'
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>
</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"
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 files only ยท 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);
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;
}
// ============================================
// 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>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Position</th>
<th class="px-4 py-3 font-semibold whitespace-nowrap">Entry $</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, 20).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>
<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>
<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 > 20 ? `<p class="text-center text-xs text-slate-500 py-2">... and ${trades.length - 20} more</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;