// ImportExport.jsx — CSV/XLSX import and CSV/JSON export // ── EXPORT ──────────────────────────────────────────────────────────────────── const exportToCSV = (trades, filename = 'trades') => { const headers = ['id','date','symbol','type','direction','status','pnl','rMultiple', 'entryPrice','quantity','stopLoss','strategy','optionType','strike','expiry','contracts','premium', 'exits','signals','customSignal','labels','customLabel','notes']; const escape = (v) => { if (v == null) return ''; const s = typeof v === 'object' ? JSON.stringify(v) : String(v); return s.includes(',') || s.includes('"') || s.includes('\n') ? `"${s.replace(/"/g,'""')}"` : s; }; const rows = [headers.join(',')]; trades.forEach(t => { rows.push(headers.map(h => { const v = t[h]; if (h === 'exits' || h === 'signals' || h === 'labels') return escape(v || []); return escape(v); }).join(',')); }); const blob = new Blob([rows.join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}_${new Date().toISOString().slice(0,10)}.csv`; a.click(); URL.revokeObjectURL(url); }; const exportToJSON = (trades, filename = 'trades') => { const blob = new Blob([JSON.stringify(trades, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${filename}_${new Date().toISOString().slice(0,10)}.json`; a.click(); URL.revokeObjectURL(url); }; // ── BROKER DETECTION ────────────────────────────────────────────────────────── const detectBroker = (headers) => { const h = headers.map(x => x.toLowerCase().trim()); if (h.includes('exec time') || h.includes('pos effect')) return 'thinkorswim'; if (h.includes('fees & comm') || h.includes('security type')) return 'schwab'; if (h.includes('run-up') || h.includes('trade #')) return 'tradingview'; if (h.includes('rmultiple') || h.includes('r multiple')) return 'native'; return 'generic'; }; // ── BROKER PARSERS ──────────────────────────────────────────────────────────── const parseThinkorSwim = (rows, headers) => { // TOS Account Activity: Symbol, Qty, Price, Exec Time, Spread, Side, Pos Effect, Account const col = (h) => headers.findIndex(x => x.toLowerCase().includes(h.toLowerCase())); const idxSymbol = col('symbol'); const idxQty = col('qty'); const idxPrice = col('price'); const idxTime = col('exec time'); const idxSide = col('side'); const idxPos = col('pos effect'); const trades = []; rows.forEach((row, i) => { const symbol = row[idxSymbol]?.replace(/['"]/g,'').trim(); const side = row[idxSide]?.toLowerCase().trim(); const pos = row[idxPos]?.toLowerCase().trim(); const price = parseFloat(row[idxPrice]); const qty = Math.abs(parseInt(row[idxQty])); const rawDate = row[idxTime]?.trim(); if (!symbol || !side || isNaN(price)) return; const isEntry = pos === 'to open' || pos === 'open'; const direction = side === 'buy' ? 'long' : 'short'; const date = rawDate ? rawDate.slice(0,10).replace(/\//g,'-') : new Date().toISOString().slice(0,10); trades.push({ id: genId(), date, symbol: symbol.toUpperCase(), type: 'stock', direction, quantity: qty, entryPrice: isEntry ? price : undefined, exits: isEntry ? [] : [{ id: genId(), price, quantity: qty, date }], signals: [], labels: [], customSignal: '', customLabel: '', notes: '', status: isEntry ? 'open' : 'closed', pnl: null, rMultiple: null, snapshot: null, }); }); return trades; }; const parseSchwab = (rows, headers) => { const col = (h) => headers.findIndex(x => x.toLowerCase().includes(h.toLowerCase())); const idxDate = col('date'); const idxAction = col('action'); const idxSymbol = col('symbol'); const idxQty = col('quantity'); const idxPrice = col('price'); const idxAmt = col('amount'); return rows.filter(r => { const action = r[col('action')]?.toLowerCase() || ''; return action.includes('buy') || action.includes('sell'); }).map(row => { const action = row[idxAction]?.toLowerCase() || ''; const symbol = row[idxSymbol]?.replace(/['"]/g,'').trim() || ''; const price = parseFloat(row[idxPrice]?.replace(/[$,]/g,'')); const qty = Math.abs(parseFloat(row[idxQty]?.replace(/,/g,''))); const rawDate = row[idxDate]?.trim() || ''; const date = rawDate.slice(0,10).replace(/\//g,'-') || new Date().toISOString().slice(0,10); const direction = action.includes('buy') ? 'long' : 'short'; const amt = parseFloat(row[idxAmt]?.replace(/[$,\-]/g,'') || '0'); return { id: genId(), date, symbol: symbol.toUpperCase(), type: 'stock', direction, quantity: qty, entryPrice: price, stopLoss: null, exits: [], signals: [], labels: [], customSignal: '', customLabel: '', notes: '', status: 'open', pnl: null, rMultiple: null, snapshot: null, }; }); }; const parseTradingView = (rows, headers) => { const col = (h) => headers.findIndex(x => x.toLowerCase().includes(h.toLowerCase())); const idxType = col('type'); const idxDate = col('date'); const idxPrice = col('price'); const idxQty = col('contracts'); const idxProfit = col('profit'); return rows.map(row => { const type = row[idxType]?.toLowerCase() || ''; const rawDate = row[idxDate]?.trim() || new Date().toISOString().slice(0,10); const date = rawDate.slice(0,10); const price = parseFloat(row[idxPrice]) || 0; const qty = parseInt(row[idxQty]) || 1; const pnl = parseFloat(row[idxProfit]?.replace(/,/g,'')) || null; const direction = type.includes('short') ? 'short' : 'long'; return { id: genId(), date, symbol: 'UNKNOWN', type: 'stock', direction, quantity: qty, entryPrice: price, stopLoss: null, exits: pnl != null ? [{ id: genId(), price: price + (pnl / qty), quantity: qty, date }] : [], signals: [], labels: [], customSignal: '', customLabel: '', notes: `Imported from TradingView`, status: pnl != null ? 'closed' : 'open', pnl, rMultiple: null, snapshot: null, }; }); }; const parseNative = (rows, headers) => { const col = (h) => headers.findIndex(x => x.toLowerCase().replace(/\s/g,'') === h.toLowerCase().replace(/\s/g,'')); return rows.map(row => { const get = (h) => row[col(h)]; const tryParse = (v) => { try { return JSON.parse(v); } catch { return []; } }; return { id: get('id') || genId(), date: get('date') || '', symbol: (get('symbol') || '').toUpperCase(), type: get('type') || 'stock', direction: get('direction') || 'long', status: get('status') || 'closed', pnl: parseFloat(get('pnl')) || null, rMultiple: parseFloat(get('rMultiple') || get('r multiple')) || null, entryPrice: parseFloat(get('entryPrice') || get('entry price')) || undefined, quantity: parseInt(get('quantity')) || undefined, stopLoss: parseFloat(get('stopLoss') || get('stop loss')) || null, strategy: get('strategy') || 'single', optionType: get('optionType') || get('option type'), strike: parseFloat(get('strike')) || undefined, expiry: get('expiry') || '', contracts: parseInt(get('contracts')) || undefined, premium: parseFloat(get('premium')) || undefined, exits: tryParse(get('exits')), signals: tryParse(get('signals')), labels: tryParse(get('labels')), customSignal: get('customSignal') || '', customLabel: get('customLabel') || '', notes: get('notes') || '', snapshot: null, }; }); }; const parseGenericCSV = (rows, headers, mapping) => { return rows.map(row => { const get = (field) => { const colIdx = mapping[field]; if (colIdx == null || colIdx === -1) return undefined; return row[colIdx]; }; const pnl = parseFloat(get('pnl')) || null; return { id: genId(), date: get('date') || new Date().toISOString().slice(0,10), symbol: (get('symbol') || 'UNKNOWN').toUpperCase(), type: get('type') || 'stock', direction: get('direction') || 'long', quantity: parseInt(get('quantity')) || undefined, entryPrice: parseFloat(get('entryPrice')) || undefined, stopLoss: parseFloat(get('stopLoss')) || null, exits: [], signals: [], labels: [], customSignal: '', customLabel: '', notes: get('notes') || '', status: pnl != null ? 'closed' : 'open', pnl, rMultiple: parseFloat(get('rMultiple')) || null, snapshot: null, }; }); }; // ── CSV PARSER ──────────────────────────────────────────────────────────────── const parseCSVText = (text) => { const lines = text.trim().split('\n'); const parseRow = (line) => { const cells = []; let cur = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const ch = line[i]; if (ch === '"' && line[i+1] === '"') { cur += '"'; i++; } else if (ch === '"') { inQuotes = !inQuotes; } else if (ch === ',' && !inQuotes) { cells.push(cur.trim()); cur = ''; } else cur += ch; } cells.push(cur.trim()); return cells; }; const headers = parseRow(lines[0]); const rows = lines.slice(1).filter(l => l.trim()).map(parseRow); return { headers, rows }; }; // ── IMPORT MODAL ────────────────────────────────────────────────────────────── const OUR_FIELDS = [ { id: 'symbol', label: 'Symbol' }, { id: 'date', label: 'Date' }, { id: 'direction', label: 'Direction (long/short)' }, { id: 'type', label: 'Type (stock/option)' }, { id: 'entryPrice', label: 'Entry Price' }, { id: 'quantity', label: 'Quantity / Shares' }, { id: 'stopLoss', label: 'Stop Loss' }, { id: 'pnl', label: 'P&L ($)' }, { id: 'rMultiple', label: 'R Multiple' }, { id: 'notes', label: 'Notes' }, ]; const ImportModal = ({ onImport, onClose }) => { const { useState, useRef, useCallback } = React; const [step, setStep] = useState('drop'); // drop | mapping | preview const [csvData, setCsvData] = useState(null); // { headers, rows } const [broker, setBroker] = useState('generic'); const [mapping, setMapping] = useState({}); const [preview, setPreview] = useState([]); const [dragging, setDragging] = useState(false); const [error, setError] = useState(''); const fileRef = useRef(); const BROKER_LABELS = { thinkorswim: 'ThinkorSwim', schwab: 'Charles Schwab', tradingview: 'TradingView', native: 'TradeJournal Export', generic: 'Generic CSV' }; const processFile = async (file) => { setError(''); try { let text; if (file.name.match(/\.(xlsx|xls)$/i)) { // Load SheetJS dynamically if (!window.XLSX) { await new Promise((res, rej) => { const s = document.createElement('script'); s.src = 'https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js'; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); } const ab = await file.arrayBuffer(); const wb = window.XLSX.read(ab, { type: 'array' }); const ws = wb.Sheets[wb.SheetNames[0]]; text = window.XLSX.utils.sheet_to_csv(ws); } else { text = await file.text(); } const { headers, rows } = parseCSVText(text); if (!headers.length) { setError('Could not parse file — check format'); return; } const detected = detectBroker(headers); setBroker(detected); setCsvData({ headers, rows }); if (detected === 'generic') { // Init mapping: try to auto-match column names const autoMap = {}; OUR_FIELDS.forEach(f => { const idx = headers.findIndex(h => h.toLowerCase().includes(f.id.toLowerCase()) || h.toLowerCase().includes(f.label.toLowerCase().split(' ')[0])); autoMap[f.id] = idx >= 0 ? idx : -1; }); setMapping(autoMap); setStep('mapping'); } else { // Auto-parse and go to preview const parsed = runParser(detected, rows, headers, {}); setPreview(parsed); setStep('preview'); } } catch (e) { setError('Failed to parse file: ' + e.message); } }; const runParser = (b, rows, headers, map) => { if (b === 'thinkorswim') return parseThinkorSwim(rows, headers); if (b === 'schwab') return parseSchwab(rows, headers); if (b === 'tradingview') return parseTradingView(rows, headers); if (b === 'native') return parseNative(rows, headers); return parseGenericCSV(rows, headers, map); }; const handleDrop = useCallback((e) => { e.preventDefault(); setDragging(false); const file = e.dataTransfer.files[0]; if (file) processFile(file); }, []); const handleApplyMapping = () => { const parsed = runParser('generic', csvData.rows, csvData.headers, mapping); setPreview(parsed); setStep('preview'); }; const handleImport = () => { onImport(preview); onClose(); }; const overlay = { position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.75)', zIndex: 2000, display: 'flex', alignItems: 'center', justifyContent: 'center' }; const modal = { background: '#0d1018', border: '1px solid #1e2440', borderRadius: '14px', width: '600px', maxWidth: '95vw', maxHeight: '85vh', display: 'flex', flexDirection: 'column', boxShadow: '0 24px 64px rgba(0,0,0,0.7)' }; const labelS = { fontSize: '11px', color: '#55647d', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.07em' }; const selS = { background: '#0a0d14', border: '1px solid #1e2440', borderRadius: '6px', color: '#d8dde8', padding: '6px 10px', fontSize: '12px', fontFamily: 'Space Grotesk, sans-serif', width: '100%', outline: 'none' }; return React.createElement('div', { style: overlay, onClick: onClose }, [ React.createElement('div', { key: 'modal', style: modal, onClick: e => e.stopPropagation() }, [ // Header React.createElement('div', { key: 'hdr', style: { padding: '20px 24px', borderBottom: '1px solid #1e2440', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, [ React.createElement('div', { key: 'l' }, [ React.createElement('div', { key: 't', style: { fontSize: '16px', fontWeight: 700, color: '#d8dde8' } }, 'Import Trades'), React.createElement('div', { key: 's', style: { fontSize: '12px', color: '#55647d', marginTop: '2px' } }, 'CSV or Excel · ThinkorSwim · Charles Schwab · TradingView') ]), React.createElement('button', { key: 'x', onClick: onClose, style: { background: 'none', border: 'none', color: '#55647d', fontSize: '22px', cursor: 'pointer', lineHeight: 1 } }, '×') ]), // Step: Drop step === 'drop' && React.createElement('div', { key: 'drop', style: { padding: '32px', flex: 1, display: 'flex', flexDirection: 'column', gap: '16px', alignItems: 'center', justifyContent: 'center' } }, [ React.createElement('div', { key: 'zone', onDrop: handleDrop, onDragOver: e => { e.preventDefault(); setDragging(true); }, onDragLeave: () => setDragging(false), onClick: () => fileRef.current?.click(), style: { border: `2px dashed ${dragging ? '#5588ff' : '#1e2440'}`, borderRadius: '12px', padding: '40px 24px', textAlign: 'center', cursor: 'pointer', transition: 'all 0.15s', background: dragging ? 'rgba(85,136,255,0.05)' : 'transparent', width: '100%' } }, [ React.createElement('div', { key: 'icon', style: { fontSize: '36px', marginBottom: '12px', opacity: 0.5 } }, '📥'), React.createElement('div', { key: 'txt', style: { fontSize: '14px', color: '#8a9bb5', marginBottom: '6px' } }, 'Drop your file here or click to browse'), React.createElement('div', { key: 'sub', style: { fontSize: '12px', color: '#55647d' } }, '.csv · .xlsx · .xls') ]), React.createElement('input', { key: 'file', ref: fileRef, type: 'file', accept: '.csv,.xlsx,.xls', style: { display: 'none' }, onChange: e => e.target.files[0] && processFile(e.target.files[0]) }), error && React.createElement('div', { key: 'err', style: { color: '#f04060', fontSize: '13px' } }, error), // Supported formats React.createElement('div', { key: 'formats', style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px', width: '100%', marginTop: '8px' } }, ['ThinkorSwim', 'Charles Schwab', 'TradingView', 'Generic CSV'].map(f => React.createElement('div', { key: f, style: { background: '#0a0d14', border: '1px solid #1e2440', borderRadius: '8px', padding: '10px', textAlign: 'center' } }, [ React.createElement('div', { key: 'n', style: { fontSize: '12px', fontWeight: 500, color: '#8a9bb5' } }, f) ]) ) ) ]), // Step: Column Mapping (generic CSV) step === 'mapping' && React.createElement('div', { key: 'mapping', style: { padding: '24px', flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '12px' } }, [ React.createElement('div', { key: 'info', style: { fontSize: '13px', color: '#55647d', marginBottom: '4px' } }, `Detected ${csvData.rows.length} rows. Map your CSV columns to trade fields:` ), React.createElement('div', { key: 'grid', style: { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px' } }, OUR_FIELDS.map(f => React.createElement('div', { key: f.id }, [ React.createElement('label', { key: 'l', style: { ...labelS, display: 'block', marginBottom: '4px' } }, f.label), React.createElement('select', { key: 's', value: mapping[f.id] ?? -1, onChange: e => setMapping(m => ({ ...m, [f.id]: parseInt(e.target.value) })), style: selS }, [ React.createElement('option', { key: 'none', value: -1 }, '— skip —'), ...csvData.headers.map((h, i) => React.createElement('option', { key: i, value: i }, h)) ]) ]) ) ), React.createElement('button', { key: 'next', onClick: handleApplyMapping, style: { padding: '10px', background: '#5588ff', border: 'none', borderRadius: '8px', color: '#fff', fontSize: '14px', fontWeight: 600, cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', marginTop: '8px' } }, 'Preview →') ]), // Step: Preview step === 'preview' && React.createElement('div', { key: 'prev', style: { flex: 1, overflowY: 'auto', padding: '20px 24px' } }, [ React.createElement('div', { key: 'info', style: { fontSize: '13px', color: '#55647d', marginBottom: '12px' } }, `${preview.length} trades detected from ${BROKER_LABELS[broker] || broker}. Review before importing:` ), React.createElement('table', { key: 'tbl', style: { width: '100%', borderCollapse: 'collapse', fontSize: '12px' } }, [ React.createElement('thead', { key: 'th' }, React.createElement('tr', {}, ['Date','Symbol','Type','Dir','Entry','P&L','Status'].map(h => React.createElement('th', { key: h, style: { padding: '8px 10px', textAlign: 'left', color: '#55647d', fontWeight: 600, borderBottom: '1px solid #1e2440', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '0.06em' } }, h) ) )), React.createElement('tbody', { key: 'tb' }, preview.slice(0, 20).map((t, i) => React.createElement('tr', { key: i, style: { borderBottom: '1px solid #0a0d14' } }, [ React.createElement('td', { key: 'd', style: { padding: '7px 10px', color: '#8a9bb5', fontFamily: 'JetBrains Mono, monospace' } }, fmtDate(t.date)), React.createElement('td', { key: 's', style: { padding: '7px 10px', color: '#d8dde8', fontWeight: 600, fontFamily: 'JetBrains Mono, monospace' } }, t.symbol), React.createElement('td', { key: 'ty', style: { padding: '7px 10px', color: '#8a9bb5' } }, t.type), React.createElement('td', { key: 'dr', style: { padding: '7px 10px', color: t.direction === 'long' ? '#00d27a' : '#f04060' } }, t.direction), React.createElement('td', { key: 'e', style: { padding: '7px 10px', color: '#8a9bb5', fontFamily: 'JetBrains Mono, monospace' } }, fmtPrice(t.entryPrice)), React.createElement('td', { key: 'p', style: { padding: '7px 10px', color: t.pnl >= 0 ? '#00d27a' : '#f04060', fontFamily: 'JetBrains Mono, monospace' } }, fmtPnL(t.pnl)), React.createElement('td', { key: 'st', style: { padding: '7px 10px', color: t.status === 'open' ? '#ffb83f' : '#55647d' } }, t.status), ]) ) ) ]), preview.length > 20 && React.createElement('div', { key: 'more', style: { fontSize: '12px', color: '#55647d', textAlign: 'center', padding: '10px' } }, `+ ${preview.length - 20} more trades`) ]), // Footer React.createElement('div', { key: 'footer', style: { padding: '16px 24px', borderTop: '1px solid #1e2440', display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, [ step !== 'drop' && React.createElement('button', { key: 'back', onClick: () => setStep(step === 'preview' && broker === 'generic' ? 'mapping' : 'drop'), style: { background: 'none', border: '1px solid #1e2440', borderRadius: '7px', color: '#8a9bb5', padding: '8px 16px', cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', fontSize: '13px' } }, '← Back'), step === 'drop' && React.createElement('div', { key: 'sp' }), React.createElement('div', { key: 'right', style: { display: 'flex', gap: '8px' } }, [ React.createElement('button', { key: 'cancel', onClick: onClose, style: { background: '#111520', border: '1px solid #1e2440', borderRadius: '7px', color: '#8a9bb5', padding: '8px 16px', cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', fontSize: '13px' } }, 'Cancel'), step === 'preview' && React.createElement('button', { key: 'imp', onClick: handleImport, style: { background: '#5588ff', border: 'none', borderRadius: '7px', color: '#fff', fontWeight: 600, padding: '8px 20px', cursor: 'pointer', fontFamily: 'Space Grotesk, sans-serif', fontSize: '13px' } }, `Import ${preview.length} Trades`) ]) ]) ]) ]); }; Object.assign(window, { ImportModal, exportToCSV, exportToJSON });