Sign In
BUMELHA
martingale strategy
0
🤖
Configure your strategy
Fill in the parameters and click Create
Active Strategy 📈
Today's Profits USDT +0.00
Capital in Active Ordersi
0
Live Unrealized P&Li
0
Profit History USDTi
+0
Total Profits USDTi
0
Updated in real time
Total Profits 0.000 USDT

Asset Allocation
No trade data yet

Win Rate
Based on closed trades
0%
Total Withdrawn
0.00
Net Investment
🎯 My Goals
Tracked against your portfolio P&L
🛡️
Beat Inflation & Zakat
Target: Outpace 3% inflation + 2.5% Zakat = 5.5%/yr
No data yet
🏠
Exceed Real Estate
Target: Outpace 6% annual real estate return
No data yet
P&L Performance
Today
0.00
0 trades
This Week
0.00
0 trades
This Month
0.00
0 trades
This Year
0.00
0 trades
Cumulative Profit Over Time (USDT)
Win / Loss Distribution
Profit per Pair (USDT)
📊 My Performance vs Zakat (2.5%) & Inflation (3%)
Compares your cumulative P&L growth against obligatory Zakat and inflation benchmarks
🏠 My Performance vs Real Estate (6% yearly)
Compares your cumulative P&L growth against a 6% annual real estate return on the same invested capital
Nisab Status
Rate
2.5%
Next Due
1 Jan 2027
☪️ ZAKAT DUE ON 1 JAN
0.00 USDT
= 0.00 USDT × 2.5%
Zakat Payment History
No Zakat payments recorded yet.
💸 Exchange Fees Dashboard
Total fees paid to exchange across all closed trades · Set rates in ⚙️ Settings
Rates:
Buy Fee 0.08% / Sell Fee 0.10%
Total Fees Buy
0.00
USDT
Total Fees Sell
0.00
USDT
Total Fees
0.00
USDT
💱 AED ↔ USDT Conversion Fees
Spread/fee paid when converting between AED and USDT · Set rate in ⚙️ Settings
Rate: —
Deposit Conversions (AED→USDT)
0.00
0 conversions
Withdrawal Conversions (USDT→AED)
0.00
0 conversions
Total Conversion Fees
0.00
USDT equivalent
Conversion History
# Date Type AED Amount USDT Amount Implied Rate Fee Paid
No conversion data recorded yet. Add the AED amount when recording capital or withdrawals.
🏆
No pair statistics yet
Close some trades to see per-pair performance
💜 Charity
Total Charity Paid
70,900
AED
Planned
0
AED
Actual
0
AED
Over / Under
0
AED
Planned vs Actual (last 6 months)
Payment History
Income
0
AED
Expenses
0
AED
Net
0
AED
Income vs Expenses
Income
Expenses
🤝 Money Lent (A/R)
Total Lent (AED)
0.00
0 loans
Outstanding (AED)
0.00
0.00 repaid
Lent Per Person
Loan Details
📈 Market
Coin
Price
24h %
24h Volume
Loading market data…
Data via CoinGecko
💰 Capital Management
Add Capital Entry (after AED→USDT conversion)
+
USDT
No fee data
Deposit
0.00
0 entries · tap to view
Withdrawal
0.00
0 withdrawals · tap to view
Totali
0.00
0.00% vs capital added
Total P&L
+0.00 USDT
0.00% on initial capital

Withdrawal / Capital Reduction (USDT→AED conversion)
USDT
No fee data
📊 Capital Flow Over Time
Capital Added
Withdrawn
Net Capital Balance
🎯
Level 1
Tester
0 / 100 pts to next level
Total Points
0
⚡ Points Activity Log
🏅 Personal Records
⏱️
Daily Records — The Volatility Smasher
📅
Weekly Records — The Consistency Driver
🪐
Monthly Records — The Strategy Master
'; var w=window.open('','_blank'); if(w){w.document.write(h);w.document.close();setTimeout(function(){w.print();},500);} else showToast('Allow popups to print PDF'); } function clearBudgetMonth(){ var m=_bdgData[_bdgCurrentMonth]; if(!m) return; m.income=[]; m.expenses=[]; bdgSave(); renderBudget(); showToast('Cleared — '+bdgMonthLabel(_bdgCurrentMonth)); } function importBudgetCSV(){ var inp=document.createElement('input'); inp.type='file'; inp.accept='.csv,.txt'; inp.onchange=function(e){ var file=e.target.files[0]; if(!file) return; var reader=new FileReader(); reader.onload=function(ev){ try{ var ls=ev.target.result.split('\n').filter(Boolean); var m=_bdgData[_bdgCurrentMonth]; if(!m) return; ls.slice(1).forEach(function(line){ var parts=line.split(',').map(function(p){return p.replace(/^"|"$/g,'').trim();}); if(parts.length<4) return; var type=parts[0].toLowerCase(),cat=parts[1]||'Other',label=parts[2],amount=parseFloat(parts[3])||0; if(!label) return; if(type==='income') m.income.push({id:'bi_'+Date.now()+Math.random(),label:label,amount:amount}); else if(type==='expense') m.expenses.push({id:'be_'+Date.now()+Math.random(),label:label,amount:amount,cat:cat}); }); bdgSave(); renderBudget(); showToast('Imported'); }catch(err){showToast('Import error');} }; reader.readAsText(file); }; inp.click(); } // (A/R loading handled by the A/R module below) // Global button dispatcher // ── Filter ── // ── Render ── // ── Detail overlay ── // ── Add/Edit modal ── // ══════════════════════════════════════════════ // ══════════════════════════════════════════════ // ── Payment modal ── // ── Reminder modal ── // ── Export ── // ── Legal document ── function jsonpFetch(url, timeoutMs){ return new Promise(function(resolve, reject){ var cbName = 'jsonpcb_' + Math.random().toString(36).slice(2); var script = document.createElement('script'); var done = false; var to = setTimeout(function(){ if(done) return; done=true; cleanup(); reject(new Error('timeout')); }, timeoutMs||6000); function cleanup(){ try{ delete window[cbName]; }catch(_){ window[cbName]=undefined; } if(script.parentNode) script.parentNode.removeChild(script); clearTimeout(to); } window[cbName] = function(data){ if(done) return; done=true; cleanup(); resolve(data); }; script.onerror = function(){ if(done) return; done=true; cleanup(); reject(new Error('script error')); }; script.src = url + (url.indexOf('?')>=0?'&':'?') + 'callback=' + cbName; document.head.appendChild(script); }); } async function fetchCoinTicker(symbol){ // CryptoCompare supports JSONP via callback — works even when fetch() is blocked try{ var cc = await jsonpFetch('https://min-api.cryptocompare.com/data/pricemultifull?fsyms='+symbol+'&tsyms=USDT', 6000); if(cc && cc.RAW && cc.RAW[symbol] && cc.RAW[symbol].USDT){ var raw = cc.RAW[symbol].USDT; var p = parseFloat(raw.PRICE); if(p>0){ return { price: p, change24h: parseFloat(raw.CHANGEPCT24HOUR)||0, volume: parseFloat(raw.TOTALVOLUME24HTO)||0, }; } } }catch(_){} // Fallback to USD if USDT pair not available try{ var cc2 = await jsonpFetch('https://min-api.cryptocompare.com/data/pricemultifull?fsyms='+symbol+'&tsyms=USD', 6000); if(cc2 && cc2.RAW && cc2.RAW[symbol] && cc2.RAW[symbol].USD){ var raw2 = cc2.RAW[symbol].USD; var p2 = parseFloat(raw2.PRICE); if(p2>0){ return { price: p2, change24h: parseFloat(raw2.CHANGEPCT24HOUR)||0, volume: parseFloat(raw2.TOTALVOLUME24HTO)||0, }; } } }catch(_){} // Last resort: try Binance via fetch (works if WebView allows network) try{ var r = await fetch('https://data-api.binance.vision/api/v3/ticker/24hr?symbol='+symbol+'USDT',{cache:'no-store'}); if(r.ok){ var d = await r.json(); var bp = parseFloat(d.lastPrice); if(bp>0) return {price:bp, change24h:parseFloat(d.priceChangePercent)||0, volume:parseFloat(d.quoteVolume)||0}; } }catch(_){} return null; } async function loadMarketPrices(){ var btn = document.getElementById('mktRefreshBtn'); var src = document.getElementById('mktSource'); var list = document.getElementById('mktList'); if(btn){ btn.disabled=true; btn.textContent='Loading…'; } if(src) src.textContent = 'Connecting…'; if(list) list.innerHTML = '
Connecting to live feed…
'; // Build wanted-symbol set var coins = MARKET_COINS.slice(); (favorites||[]).forEach(function(f){ if(!coins.find(function(c){ return c.symbol===f.toUpperCase(); })) coins.push({symbol:f.toUpperCase(),name:f.toUpperCase()}); }); var nameMap = {}; coins.forEach(function(c){ nameMap[c.symbol] = c.name; }); var wanted = {}; coins.forEach(function(c){ wanted[c.symbol+'USDT'] = true; }); // ── Primary: Binance WebSocket all-market mini-ticker (same protocol as Entry Price) ── var gotData = false; function finish(){ if(btn){ btn.disabled=false; btn.textContent='↻ Refresh'; } renderMarketList(document.getElementById('mktSearch')&&document.getElementById('mktSearch').value.trim().toUpperCase()); } try { if(_mktWS){ try{ _mktWS.close(); }catch(_){} _mktWS=null; } var ws = new WebSocket('wss://stream.binance.com:9443/ws/!miniTicker@arr'); _mktWS = ws; var wsTimeout = setTimeout(function(){ if(!gotData){ try{ ws.close(); }catch(_){} if(src) src.textContent='Live feed timed out — retrying via JSONP…'; loadMarketPricesJSONP(coins, nameMap, finish); } }, 6000); ws.onmessage = function(ev){ try { var arr = JSON.parse(ev.data); if(!Array.isArray(arr)) return; var rows = []; arr.forEach(function(t){ if(wanted[t.s]){ var sym = t.s.replace('USDT',''); var close = parseFloat(t.c), open = parseFloat(t.o); var chg = (open>0)? ((close-open)/open*100) : 0; rows.push({ id: sym.toLowerCase(), name: nameMap[sym]||sym, symbol: sym, price: close, change24h: chg, volume: parseFloat(t.q)||0, mcap: parseFloat(t.q)||0, }); } }); if(rows.length){ clearTimeout(wsTimeout); gotData = true; _mktData = rows; if(src) src.textContent = '🟢 Live · Binance · '+new Date().toLocaleTimeString(); renderMarketList(document.getElementById('mktSearch')&&document.getElementById('mktSearch').value.trim().toUpperCase()); if(btn){ btn.disabled=false; btn.textContent='↻ Refresh'; } // Keep streaming for ~3s then close to save battery setTimeout(function(){ try{ ws.close(); }catch(_){} _mktWS=null; }, 3000); } } catch(_){} }; ws.onerror = function(){ clearTimeout(wsTimeout); if(!gotData){ if(src) src.textContent='WebSocket blocked — trying JSONP…'; loadMarketPricesJSONP(coins, nameMap, finish); } }; ws.onclose = function(){ if(_mktWS===ws) _mktWS=null; }; } catch(e){ loadMarketPricesJSONP(coins, nameMap, finish); } } var _mktWS = null; // JSONP fallback async function loadMarketPricesJSONP(coins, nameMap, done){ var src = document.getElementById('mktSource'); var list = document.getElementById('mktList'); var results = await Promise.allSettled(coins.map(function(c){ return fetchCoinTicker(c.symbol).then(function(data){ if(!data) return null; return {id:c.symbol.toLowerCase(),name:c.name,symbol:c.symbol,price:data.price,change24h:data.change24h,volume:data.volume,mcap:data.volume}; }); })); _mktData = results.filter(function(r){ return r.status==='fulfilled'&&r.value&&r.value.price>0; }).map(function(r){ return r.value; }); if(_mktData.length>0){ if(src) src.textContent = 'CryptoCompare · '+new Date().toLocaleTimeString()+' · '+_mktData.length+' coins'; } else { if(src) src.textContent = 'Network unavailable in this viewer'; if(list) list.innerHTML = '
Could not load live prices.
This in-app viewer blocks market connections.

Open the file in Chrome browser for live data.
'; } if(done) done(); } function goToStrategy(id){ switchStrategy(id); var btn = document.querySelector('.tab-btn[onclick*="\'strategy\'"]'); if(btn) switchTab('strategy', btn); } function showClosePriceModal(marketPrice, hasMarket, onConfirm){ var overlay = document.getElementById('closePriceOverlay'); var input = document.getElementById('closePriceInput'); var label = document.getElementById('closePriceMarketLabel'); var okBtn = document.getElementById('closePriceOK'); var cancelBtn = document.getElementById('closePriceCancel'); if(!overlay){ onConfirm(null); return; } // fallback, should not happen input.value = ''; label.textContent = hasMarket ? ('Leave blank to close at market price (' + fmt(marketPrice,2) + ')') : 'Leave blank to close at last TP price'; overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; function cleanup(){ overlay.style.display='none'; document.body.style.overflow=''; okBtn.onclick=null; cancelBtn.onclick=null; overlay.onclick=null; } okBtn.onclick = function(){ var val = input.value.trim(); cleanup(); onConfirm(val === '' ? null : val); }; cancelBtn.onclick = function(){ cleanup(); }; overlay.onclick = function(e){ if(e.target===overlay) cleanup(); }; } function closeStrategyFromList(id){ switchStrategy(id); var marketEl = document.getElementById('entryPriceMin'); var marketPrice = marketEl ? parseFloat(marketEl.value) : NaN; var hasMarket = !isNaN(marketPrice) && marketPrice > 0; showClosePriceModal(marketPrice, hasMarket, function(val){ var customInput = document.getElementById('customSellPrice'); if(val !== null){ var p = parseFloat(val); if(isNaN(p) || p<=0){ alert('Invalid price.'); return; } if(customInput) customInput.value = p; } else { if(customInput) customInput.value = hasMarket ? marketPrice : ''; } closeTrade(); saveCurrentStrategy(); renderStrategiesList(); renderCurrentStrategyChip(); }); } function renderStrategiesList(){ var box = document.getElementById('strategiesListContainer'); if(!box) return; if(!strategies || strategies.length === 0){ box.innerHTML = '
🤖
No strategies yet
Create one from the Strategy tab
'; return; } box.innerHTML = strategies.map(function(s){ var isActive = s.id === activeStratId; var orders = s.orders || []; var filled = orders.filter(function(o){ return o.activated; }); var total = orders.length; var invest = filled.length ? filled[filled.length-1].execAmount : 0; var avgPrice = filled.length ? filled[filled.length-1].avgPrice : (orders[0] ? orders[0].price : 0); var tpPrice = filled.length ? filled[filled.length-1].tpPrice : (orders[0] ? orders[0].tpPrice : 0); var qty = filled.length ? filled[filled.length-1].execQty : 0; var potentialProfit = (qty && tpPrice) ? (qty*tpPrice - invest) : 0; var prices = filled.map(function(o){return o.price;}); var priceRange = prices.length ? (Math.min.apply(null,prices).toFixed(2) + ' ~ ' + Math.max.apply(null,prices).toFixed(2)) : '--'; var pnlColor = potentialProfit>=0 ? 'var(--green)' : 'var(--red)'; return '
' + '
' + '
' + '
Spot Martingale
' + '
' + s.base + '/USDT ' + (s.priceDrop||'') + '%
' + '
' + '' + '
' + '
' + '' + (isActive ? 'Active' : 'Saved') + '  |  ' + filled.length + '/' + total + ' orders filled' + '
' + '
' + '
Investment USDT
' + fmt(invest,2) + '
' + '
Potential Profit (at TP)
' + (potentialProfit>=0?'+':'') + fmt(potentialProfit,2) + '
' + '
' + '
' + tdGridItem('Avg. Price', fmt(avgPrice,2)) + tdGridItem('Price Range', priceRange) + tdGridItem('Take-Profit Price', fmt(tpPrice,2), 'var(--accent)') + '
' + '
'; }).join(''); } function deleteStrategyById(id){ deleteStrategy(id); } // Close coin list when clicking outside document.addEventListener('click', function(e){ if(!e.target.closest('.strat-search-wrap') && !e.target.closest('.strat-coin-list')){ closeStrategyCoinList(); } }); // ════════════════════════════════════════════════ // ACCENT THEME (Blue / Orange) // ════════════════════════════════════════════════ function setAccentTheme(theme){ saveSetting('accentTheme', theme); applyAccentTheme(theme); var blueBtn = document.getElementById('accentBlue'); var orangeBtn = document.getElementById('accentOrange'); if(blueBtn) blueBtn.classList.toggle('active', theme==='blue'); if(orangeBtn) orangeBtn.classList.toggle('active', theme==='orange'); } function applyAccentTheme(theme){ document.body.classList.toggle('theme-orange', theme==='orange'); } // charts declared in Script 2 // ════════════════════════════════════════════════ // AED ↔ USDT CONVERSION FEES // ════════════════════════════════════════════════ function getAedRate(){ var s = JSON.parse(localStorage.getItem('mg_settings') || '{}'); return parseFloat(s.aedRate) || 3.6725; } function updateConversionPreview(which){ var rate = getAedRate(); if(which === 'cap'){ var usdt = parseFloat(document.getElementById('initialCapital').value); var aed = parseFloat(document.getElementById('capAedAmount').value); var prevEl = document.getElementById('capConvPreview'); if(!prevEl) return; if(!usdt || !aed || isNaN(usdt) || isNaN(aed)){ prevEl.textContent='No fee data'; prevEl.style.color='var(--muted)'; return; } var expectedUsdt = aed / rate; var fee = expectedUsdt - usdt; // positive = you lost USDT to fee var feePct = expectedUsdt>0 ? (fee/expectedUsdt*100) : 0; prevEl.innerHTML = 'Fee: ' + fmt(Math.abs(fee),2) + ' USDT (' + Math.abs(feePct).toFixed(2) + '%)'; prevEl.style.color = fee > 0.01 ? 'var(--amber)' : 'var(--green)'; } else { var usdtW = parseFloat(document.getElementById('withdrawAmount').value); var aedW = parseFloat(document.getElementById('wdAedAmount').value); var prevElW = document.getElementById('wdConvPreview'); if(!prevElW) return; if(!usdtW || !aedW || isNaN(usdtW) || isNaN(aedW)){ prevElW.textContent='No fee data'; prevElW.style.color='var(--muted)'; return; } var expectedAed = usdtW * rate; var feeAed = expectedAed - aedW; // positive = you lost AED to fee var feeUsdt = feeAed / rate; var feePctW = expectedAed>0 ? (feeAed/expectedAed*100) : 0; prevElW.innerHTML = 'Fee: ' + fmt(Math.abs(feeUsdt),2) + ' USDT (' + Math.abs(feePctW).toFixed(2) + '%)'; prevElW.style.color = feeAed > 0.5 ? 'var(--amber)' : 'var(--green)'; } } function renderConversionDashboard(){ var rate = getAedRate(); var rdEl = document.getElementById('convRateDisplay'); if(rdEl) rdEl.textContent = 'Rate: 1 USDT = ' + rate.toFixed(2) + ' AED'; var conversions = []; (capitalHistory||[]).forEach(function(c, i){ if(c.aedAmount && c.aedAmount > 0){ var expectedUsdt = c.aedAmount / rate; var feeUsdt = expectedUsdt - c.amount; conversions.push({ type: 'Deposit', typeIcon: '📥', date: c.date, ts: c.ts, aed: c.aedAmount, usdt: c.amount, impliedRate: c.amount>0 ? (c.aedAmount/c.amount) : 0, feeUsdt: feeUsdt, }); } }); (withdrawalHistory||[]).forEach(function(w, i){ if(w.aedAmount && w.aedAmount > 0){ var expectedAed = w.amount * rate; var feeAedW = expectedAed - w.aedAmount; var feeUsdtW = feeAedW / rate; conversions.push({ type: 'Withdrawal', typeIcon: '📤', date: w.date, ts: w.ts, aed: w.aedAmount, usdt: w.amount, impliedRate: w.amount>0 ? (w.aedAmount/w.amount) : 0, feeUsdt: feeUsdtW, }); } }); conversions.sort(function(a,b){ return (b.ts||0)-(a.ts||0); }); var depositFees = conversions.filter(function(c){return c.type==='Deposit';}).reduce(function(s,c){return s+(c.feeUsdt>0?c.feeUsdt:0);},0); var withdrawFees = conversions.filter(function(c){return c.type==='Withdrawal';}).reduce(function(s,c){return s+(c.feeUsdt>0?c.feeUsdt:0);},0); var totalFees = depositFees + withdrawFees; var depositCount = conversions.filter(function(c){return c.type==='Deposit';}).length; var withdrawCount = conversions.filter(function(c){return c.type==='Withdrawal';}).length; var tfEl = document.getElementById('cvTotalFees'); if(tfEl) tfEl.textContent = fmt(totalFees,2); var dfEl = document.getElementById('cvDepositFees'); if(dfEl) dfEl.textContent = fmt(depositFees,2); var dcEl = document.getElementById('cvDepositCount'); if(dcEl) dcEl.textContent = depositCount + ' conversion' + (depositCount!==1?'s':''); var wfEl = document.getElementById('cvWithdrawFees'); if(wfEl) wfEl.textContent = fmt(withdrawFees,2); var wcEl = document.getElementById('cvWithdrawCount');if(wcEl) wcEl.textContent = withdrawCount + ' conversion' + (withdrawCount!==1?'s':''); var tbody = document.getElementById('conversionBody'); var emptyEl = document.getElementById('conversionEmpty'); var tableEl = document.getElementById('conversionTable'); if(conversions.length === 0){ if(emptyEl) emptyEl.style.display = 'block'; if(tableEl) tableEl.style.display = 'none'; return; } if(emptyEl) emptyEl.style.display = 'none'; if(tableEl) tableEl.style.display = 'table'; if(tbody){ tbody.innerHTML = conversions.map(function(c, i){ var feeColor = c.feeUsdt > 0.01 ? 'var(--amber)' : c.feeUsdt < -0.01 ? 'var(--green)' : 'var(--muted)'; return '' + ''+(i+1)+'' + ''+c.date+'' + ''+c.typeIcon+' '+c.type+'' + ''+fmt(c.aed,2)+' AED' + ''+fmt(c.usdt,2)+' USDT' + ''+c.impliedRate.toFixed(2)+'' + ''+(c.feeUsdt>=0?'−':'+')+fmt(Math.abs(c.feeUsdt),2)+' USDT' + ''; }).join(''); } } // ════ FEE & EXCHANGE API HELPERS ════ function getFeeRates(){ var s = JSON.parse(localStorage.getItem('mg_settings') || '{}'); var maker = parseFloat(s.makerFee ?? 0.08) / 100; // 0.08% default KuCoin VIP1 var taker = parseFloat(s.takerFee ?? 0.1) / 100; // 0.10% default KuCoin VIP1 var kcs = s.kcsDiscount === true; if(kcs){ maker *= 0.8; taker *= 0.8; } // 20% KCS discount return { maker, taker }; } async function fetchFromBinance(coin){ const symbol = coin + 'USDT'; const [tickerRes, exInfoRes] = await Promise.all([ fetch('https://data-api.binance.vision/api/v3/ticker/24hr?symbol='+symbol, {cache:'no-store'}), fetch('https://data-api.binance.vision/api/v3/exchangeInfo?symbol='+symbol, {cache:'no-store'}).catch(()=>null), ]); if(!tickerRes.ok) throw new Error('Binance: no data'); const t = await tickerRes.json(); if(!t || !t.lastPrice) throw new Error('Binance: invalid data'); return { source: 'Binance', name: coin, symbol: coin, price: parseFloat(t.lastPrice), change24h: parseFloat(t.priceChangePercent), high24h: parseFloat(t.highPrice), low24h: parseFloat(t.lowPrice), volume24h: parseFloat(t.quoteVolume), marketCap: null, fdv: null, circulatingSupply: null, totalSupply: null, maxSupply: null, ath: null, athDate: null, athChangePct: null, atl: null, atlDate: null, atlChangePct: null, }; } async function fetchFromKuCoin(coin){ const symbol = coin + '-USDT'; const res = await fetch('https://api.kucoin.com/api/v1/market/stats?symbol='+symbol, {cache:'no-store'}); if(!res.ok) throw new Error('KuCoin: no data'); const json = await res.json(); const d = json.data; if(!d || !d.last) throw new Error('KuCoin: invalid data'); return { source: 'KuCoin', name: coin, symbol: coin, price: parseFloat(d.last), change24h: parseFloat(d.changeRate)*100, high24h: parseFloat(d.high), low24h: parseFloat(d.low), volume24h: parseFloat(d.volValue), marketCap: null, fdv: null, circulatingSupply: null, totalSupply: null, maxSupply: null, ath: null, athDate: null, athChangePct: null, atl: null, atlDate: null, atlChangePct: null, }; } async function fetchFromOKX(coin){ const symbol = coin + '-USDT'; const res = await fetch('https://www.okx.com/api/v5/market/ticker?instId='+symbol, {cache:'no-store'}); if(!res.ok) throw new Error('OKX: no data'); const json = await res.json(); const d = json.data && json.data[0]; if(!d || !d.last) throw new Error('OKX: invalid data'); const open24h = parseFloat(d.open24h); const last = parseFloat(d.last); const change24h = open24h > 0 ? ((last-open24h)/open24h*100) : 0; return { source: 'OKX', name: coin, symbol: coin, price: last, change24h: change24h, high24h: parseFloat(d.high24h), low24h: parseFloat(d.low24h), volume24h: parseFloat(d.volCcy24h), marketCap: null, fdv: null, circulatingSupply: null, totalSupply: null, maxSupply: null, ath: null, athDate: null, athChangePct: null, atl: null, atlDate: null, atlChangePct: null, }; } async function fetchFromCoinGecko(coin, cgId){ const urls=[ `https://api.coingecko.com/api/v3/coins/${cgId}?localization=false&tickers=false&community_data=false&developer_data=false`, `https://api.allorigins.win/raw?url=${encodeURIComponent('https://api.coingecko.com/api/v3/coins/'+cgId+'?localization=false&tickers=false&community_data=false&developer_data=false')}`, `https://corsproxy.io/?url=${encodeURIComponent('https://api.coingecko.com/api/v3/coins/'+cgId+'?localization=false&tickers=false&community_data=false&developer_data=false')}`, ]; for(const url of urls){ try{ const r=await fetch(url,{cache:'no-store'}); if(!r.ok)continue; const data=await r.json(); if(data&&data.market_data){ data._source = 'CoinGecko'; return data; } }catch(_){continue;} } throw new Error('CoinGecko: no data from any mirror'); } function renderFeesDashboard(){ var rates = getFeeRates(); // Update rate display var rd = document.getElementById('feeRateDisplay'); var s2 = JSON.parse(localStorage.getItem('mg_settings')||'{}'); if(rd){ var kcsLabel = s2.kcsDiscount ? ' (KCS -20%)' : ''; rd.textContent = 'Buy Fee '+(rates.maker*100).toFixed(3)+'% / Sell Fee '+(rates.taker*100).toFixed(3)+'%'+kcsLabel; } if(!tradeHistory || tradeHistory.length === 0){ ['fcTotalFees','fcBuyFees','fcSellFees','fcAvgFee'].forEach(function(id){ var el=document.getElementById(id); if(el) el.textContent='0.00'; }); ['fcGrossPnl','fcNetPnl','fcFeeRatio'].forEach(function(id){ var el=document.getElementById(id); if(el) el.textContent='—'; }); var fbe = document.getElementById('feeBreakdownEmpty'); var fbb = document.getElementById('feeBreakdownBody'); var fbt = document.getElementById('feeBreakdownTable'); if(fbe) fbe.style.display='block'; if(fbt) fbt.style.display='none'; return; } var fbt = document.getElementById('feeBreakdownTable'); var fbe = document.getElementById('feeBreakdownEmpty'); if(fbt) fbt.style.display='table'; if(fbe) fbe.style.display='none'; // Calculate per trade var totalFees=0, totalBuyFees=0, totalSellFees=0, totalGross=0, totalNet=0; var sorted = [...tradeHistory].sort(function(a,b){return a.ts-b.ts;}); var tableRows = ''; var cumFees = 0; var cumFeeData = []; sorted.forEach(function(t, i){ var bf = t.buyFees || (t.invested * rates.maker); var sf = t.sellFee || ((t.sellPrice||0) * (t.qty||0) * rates.taker); var tf = t.fees || (bf + sf); var grossPnl = (t.pnl||0) + tf; // gross = net + fees (fees already deducted from pnl) var netPnl = t.pnl || 0; var feeRatio = grossPnl !== 0 ? Math.abs(tf/grossPnl*100) : 0; totalFees += tf; totalBuyFees += bf; totalSellFees+= sf; totalGross += grossPnl; totalNet += netPnl; cumFees += tf; cumFeeData.push({ label: '#'+(i+1)+' '+t.pair, cumFees: parseFloat(cumFees.toFixed(2)) }); var feeColor = tf > 1 ? 'var(--red)' : 'var(--muted)'; var gpColor = grossPnl >= 0 ? 'var(--green)' : 'var(--red)'; var npColor = netPnl >= 0 ? 'var(--green)' : 'var(--red)'; tableRows += '' + ''+(i+1)+'' + ''+t.pair+'' + ''+fmt(t.invested)+'' + '−'+fmt(bf,2)+'' + '−'+fmt(sf,2)+'' + '−'+fmt(tf,2)+'' + ''+(grossPnl>=0?'+':'')+fmt(grossPnl)+'' + ''+(netPnl>=0?'+':'')+fmt(netPnl)+'' + ''+feeRatio.toFixed(1)+'%' + ''; }); // Update summary cards var fcTF = document.getElementById('fcTotalFees'); if(fcTF) fcTF.textContent = fmt(totalFees,2); var fcBF = document.getElementById('fcBuyFees'); if(fcBF) fcBF.textContent = fmt(totalBuyFees,2); var fcSF = document.getElementById('fcSellFees'); if(fcSF) fcSF.textContent = fmt(totalSellFees,2); var fcAF = document.getElementById('fcAvgFee'); if(fcAF) fcAF.textContent = fmt(totalFees/tradeHistory.length,2); var fcGP = document.getElementById('fcGrossPnl'); if(fcGP){ fcGP.textContent=(totalGross>=0?'+':'')+fmt(totalGross)+' USDT'; fcGP.style.color=totalGross>=0?'var(--green)':'var(--red)'; } var fcNP = document.getElementById('fcNetPnl'); if(fcNP){ fcNP.textContent=(totalNet>=0?'+':'')+fmt(totalNet)+' USDT'; fcNP.style.color=totalNet>=0?'var(--green)':'var(--red)'; } var fcFR = document.getElementById('fcFeeRatio'); if(fcFR){ var ratio = totalGross > 0 ? (totalFees/totalGross*100) : 0; fcFR.textContent = ratio.toFixed(2)+'%'; fcFR.style.color = ratio < 5 ? 'var(--green)' : ratio < 15 ? 'var(--amber)' : 'var(--red)'; } // Update breakdown table var fbb = document.getElementById('feeBreakdownBody'); if(fbb) fbb.innerHTML = tableRows; // Render cumulative fees chart var ctx = document.getElementById('feesChart'); if(ctx){ var c = ctx.getContext('2d'); if(feesChart && typeof feesChart.destroy==='function') feesChart.destroy(); feesChart = new Chart(c, { type: 'line', data: { labels: cumFeeData.map(function(d){return d.label;}), datasets: [{ label: 'Cumulative Fees (USDT)', data: cumFeeData.map(function(d){return d.cumFees;}), borderColor: '#ef4444', backgroundColor: 'rgba(239,68,68,0.08)', fill: true, tension: 0.3, borderWidth: 2, pointRadius: cumFeeData.length <= 20 ? 4 : 2, pointBackgroundColor: '#ef4444', }] }, options: { ...chartDefaults, responsive: true, maintainAspectRatio: false, plugins: { ...chartDefaults.plugins, tooltip: { callbacks: { label: function(ctx){ return ' Total fees paid: '+fmt(ctx.raw,2)+' USDT'; } } } }, scales: { x: { ticks:{color:'#94a3b8',font:{size:11},maxTicksLimit:10}, grid:{color:'#222b3a'} }, y: { ticks:{color:'#ef4444',font:{size:11},callback:function(v){return '−'+fmt(v)+' USDT';}}, grid:{color:'#222b3a'} } } } }); } } // ════════════════════════════════════════════════ // EXCHANGE FEE PRESETS (Binance / KuCoin Spot VIP tiers) // ════════════════════════════════════════════════ var EXCHANGE_FEE_TABLES = { binance: { label: 'Binance Spot — Maker/Taker', tiers: [ { name:'Regular (VIP0)', maker:0.1000, taker:0.1000 }, { name:'VIP1', maker:0.0900, taker:0.1000 }, { name:'VIP2', maker:0.0800, taker:0.1000 }, { name:'VIP3', maker:0.0420, taker:0.0600 }, { name:'VIP4', maker:0.0420, taker:0.0540 }, { name:'VIP5', maker:0.0360, taker:0.0480 }, { name:'VIP6', maker:0.0300, taker:0.0420 }, { name:'VIP7', maker:0.0240, taker:0.0360 }, { name:'VIP8', maker:0.0180, taker:0.0300 }, { name:'VIP9', maker:0.0120, taker:0.0240 }, { name:'With BNB 25% off (Regular)', maker:0.0750, taker:0.0750 }, ], hasDiscount: false, }, kucoin: { label: 'KuCoin Spot — Maker/Taker', tiers: [ { name:'Regular (Lv1)', maker:0.1000, taker:0.1000 }, { name:'VIP1', maker:0.0800, taker:0.1000 }, { name:'VIP2', maker:0.0600, taker:0.0900 }, { name:'VIP3', maker:0.0400, taker:0.0800 }, { name:'VIP4', maker:0.0200, taker:0.0700 }, { name:'VIP5', maker:0.0000, taker:0.0600 }, { name:'VIP6', maker:-0.0050, taker:0.0500 }, { name:'VIP7', maker:-0.0050, taker:0.0400 }, { name:'VIP8', maker:-0.0050, taker:0.0300 }, { name:'VIP9', maker:-0.0050, taker:0.0250 }, ], hasDiscount: true, // KCS 20% off }, }; function populateVipTiers(exchange){ var sel = document.getElementById('vipTierSelect'); var sub = document.getElementById('vipTierSub'); if(!sel) return; var table = EXCHANGE_FEE_TABLES[exchange]; if(!table){ sel.innerHTML = ''; if(sub) sub.textContent = 'Enter your own Buy/Sell Fee rates below'; return; } sel.innerHTML = table.tiers.map(function(t, i){ return ''; }).join(''); if(sub) sub.textContent = table.label; var kcsRow = document.getElementById('kcsDiscountRow'); if(kcsRow) kcsRow.style.display = table.hasDiscount ? 'flex' : 'none'; } function setExchangePreset(exchange){ saveSetting('exchange', exchange); populateVipTiers(exchange); if(exchange !== 'custom'){ applyVipPreset(0); // default to first tier (Regular) } renderFeesDashboard(); renderGoalsCard(); } function applyVipPreset(idx){ var exchange = document.getElementById('exchangeSelect').value; var table = EXCHANGE_FEE_TABLES[exchange]; if(!table) return; var tier = table.tiers[parseInt(idx)]; if(!tier) return; var mfEl = document.getElementById('makerFee'); var tfEl = document.getElementById('takerFee'); if(mfEl) mfEl.value = tier.maker; if(tfEl) tfEl.value = tier.taker; saveSetting('makerFee', tier.maker); saveSetting('takerFee', tier.taker); saveSetting('vipTierIdx', idx); var vipSel = document.getElementById('vipTierSelect'); if(vipSel) vipSel.value = idx; renderFeesDashboard(); renderGoalsCard(); showToast('✓ Fee rates set: ' + tier.name); } function markCustomTier(){ // If user manually edits maker/taker, switch VIP dropdown to indicate custom var vipSel = document.getElementById('vipTierSelect'); if(vipSel && vipSel.options.length){ // Add a "Custom (edited)" option if not present, select it var hasCustomOpt = Array.from(vipSel.options).some(function(o){ return o.value==='manual'; }); if(!hasCustomOpt){ var opt = document.createElement('option'); opt.value = 'manual'; opt.textContent = '✏️ Custom (manually edited)'; vipSel.insertBefore(opt, vipSel.firstChild); } vipSel.value = 'manual'; } renderFeesDashboard(); renderGoalsCard(); } // ════════════════════════════════════════════════ // CUSTOM COIN TAGS (internal/personal use only) // ════════════════════════════════════════════════ var coinFlags = JSON.parse(localStorage.getItem('mg_coin_flags') || '{}'); function saveCoinFlags(){ localStorage.setItem('mg_coin_flags', JSON.stringify(coinFlags)); } function toggleCoinFlag(coin, e){ if(e) e.stopPropagation(); if(coinFlags[coin]){ delete coinFlags[coin]; } else { coinFlags[coin] = true; } saveCoinFlags(); renderFavGrid(); } // ════════════════════════════════════════════════ // BACKUP & RESTORE // ════════════════════════════════════════════════ var BACKUP_KEYS = [ 'mg_history', // trade history 'mg_withdrawal', // withdrawal total 'mg_withdrawal_log', // withdrawal log 'mg_capital_log', // capital history 'mg_strategies', // saved strategies 'mg_active_strat', // active strategy id 'mg_favorites', // watchlist 'mg_settings', // app settings 'mg_profile', // profile info 'mg_avatar', // profile photo 'mg_notifications', // notification log 'mg_last_reports', // AI report schedule 'mg_zakat_payments', // zakat payment log 'mg_arcade', // points/levels/badges ]; function downloadBackup(){ var backup = { _meta: { app: 'Martingale Dashboard', version: '1.0', exportedAt: new Date().toISOString(), exportedAtLocal: new Date().toLocaleString('en-GB'), }, data: {} }; BACKUP_KEYS.forEach(function(key){ var val = localStorage.getItem(key); if(val !== null){ try { backup.data[key] = JSON.parse(val); } catch(e) { backup.data[key] = val; } // store raw string if not JSON } }); var json = JSON.stringify(backup, null, 2); var blob = new Blob([json], { type:'application/json' }); var url = URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = 'martingale_backup_' + new Date().toISOString().split('T')[0] + '.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); setTimeout(function(){ URL.revokeObjectURL(url); }, 2000); showToast('✓ Backup downloaded — ' + BACKUP_KEYS.length + ' data sets saved'); addNotification('system', '📦 Backup Created', 'Full backup downloaded with ' + (tradeHistory?tradeHistory.length:0) + ' trades and all settings.'); } function handleBackupUpload(e){ var file = e.target.files && e.target.files[0]; if(!file){ showToast('No file selected.'); return; } if(!file.name.endsWith('.json')){ showToast('Please select a .json backup file.'); return; } var reader = new FileReader(); reader.onerror = function(){ showToast('Could not read the file.'); }; reader.onload = function(ev){ try { var backup = JSON.parse(ev.target.result); if(!backup.data){ showToast('Invalid backup file format.'); return; } var keyCount = Object.keys(backup.data).length; var dateStr = backup._meta && backup._meta.exportedAtLocal ? backup._meta.exportedAtLocal : 'unknown date'; showConfirm( 'Restore backup from ' + dateStr + '? This will overwrite ' + keyCount + ' data sets including trades, capital, strategies, and settings. Current data will be replaced.', 'Restore', function(){ performRestore(backup); } ); } catch(err){ showToast('Failed to parse backup file — it may be corrupted.'); } }; reader.readAsText(file); e.target.value = ''; // reset input so same file can be re-selected } function performRestore(backup){ Object.keys(backup.data).forEach(function(key){ var val = backup.data[key]; var stored = typeof val === 'string' ? val : JSON.stringify(val); localStorage.setItem(key, stored); }); showToast('✓ Backup restored — reloading…'); setTimeout(function(){ window.location.reload(); }, 1200); } // ════════════════════════════════════════════════ // MY GOALS: Beat Inflation/Zakat & Real Estate // ════════════════════════════════════════════════ capFlowPeriod = 'all'; function setCapFlowPeriod(period, btn){ capFlowPeriod = period; document.querySelectorAll('.cf-period-btn').forEach(function(b){ b.classList.remove('active'); }); if(btn) btn.classList.add('active'); renderCapitalFlowChart(); } function renderCapitalFlowChart(){ var ctx = document.getElementById('capitalFlowChart'); if(!ctx) return; if(capitalFlowChart && typeof capitalFlowChart.destroy==='function') capitalFlowChart.destroy(); // Merge capital additions and withdrawals into one timeline var events = []; (capitalHistory||[]).forEach(function(c){ events.push({ ts: c.ts || 0, type:'add', amount: c.amount||0 }); }); (withdrawalHistory||[]).forEach(function(w){ events.push({ ts: w.ts || 0, type:'withdraw', amount: w.amount||0 }); }); if(events.length === 0){ var c2d = ctx.getContext('2d'); c2d.clearRect(0,0,ctx.width,ctx.height); c2d.fillStyle = '#64748b'; c2d.font = '13px Inter'; c2d.textAlign = 'center'; c2d.fillText('No capital or withdrawal entries yet', ctx.width/2, ctx.height/2); return; } events.sort(function(a,b){ return a.ts - b.ts; }); // Period filter var now = Date.now(); var cutoff = 0; if(capFlowPeriod === '30') cutoff = now - 30*86400000; if(capFlowPeriod === '90') cutoff = now - 90*86400000; if(capFlowPeriod === '365') cutoff = now - 365*86400000; var filtered = capFlowPeriod==='all' ? events : events.filter(function(e){ return e.ts >= cutoff; }); if(filtered.length === 0) filtered = events; // fallback if filter empties it var labels = []; var addedBars = []; var withdrawnBars = []; var netLine = []; var running = 0; // Include a starting point if filtering cut off earlier events if(capFlowPeriod !== 'all'){ var before = events.filter(function(e){ return e.ts < cutoff; }); running = before.reduce(function(s,e){ return s + (e.type==='add'?e.amount:-e.amount); }, 0); } filtered.forEach(function(e, i){ var d = new Date(e.ts); var label = d.toLocaleDateString('en-GB',{day:'2-digit',month:'short'}); labels.push(label); addedBars.push(e.type==='add' ? e.amount : 0); withdrawnBars.push(e.type==='withdraw' ? -e.amount : 0); running += (e.type==='add' ? e.amount : -e.amount); netLine.push(parseFloat(running.toFixed(2))); }); capitalFlowChart = new Chart(ctx.getContext('2d'), { type: 'bar', data: { labels: labels, datasets: [ { label: 'Capital Added', data: addedBars, backgroundColor: 'rgba(34,197,94,.6)', borderColor: '#22c55e', borderWidth: 1, borderRadius: 4, yAxisID: 'y', }, { label: 'Withdrawn', data: withdrawnBars, backgroundColor: 'rgba(239,68,68,.6)', borderColor: '#ef4444', borderWidth: 1, borderRadius: 4, yAxisID: 'y', }, { label: 'Net Capital Balance', data: netLine, type: 'line', borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,.08)', fill: true, tension: 0.3, borderWidth: 2.5, pointRadius: filtered.length <= 15 ? 4 : 2, pointBackgroundColor: '#3b82f6', yAxisID: 'y', } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode:'index', intersect:false }, plugins: { legend: { display:false }, tooltip: { callbacks: { label: function(ctx){ var v = ctx.raw; if(ctx.dataset.label === 'Net Capital Balance') return ' Net Balance: ' + fmt(v) + ' USDT'; return ' ' + ctx.dataset.label + ': ' + (v>=0?'+':'') + fmt(Math.abs(v)) + ' USDT'; } } } }, scales: { x: { ticks:{color:'#94a3b8',font:{size:10},maxTicksLimit:10}, grid:{color:'#222b3a'} }, y: { ticks:{color:'#64748b',font:{size:10},callback:function(v){return fmt(v,0);}}, grid:{color:'#222b3a'} } } } }); } function renderGoalsCard(){ var totalCapital = (capitalHistory||[]).reduce(function(s,c){return s+(c.amount||0);},0); var totalPnl = (tradeHistory||[]).reduce(function(s,t){return s+(t.pnl||0);},0); if(totalCapital <= 0 || tradeHistory.length === 0){ var ids = ['goalInflationStatus','goalRealEstateStatus']; ids.forEach(function(id){ var el=document.getElementById(id); if(el) el.textContent='—'; }); return; } // Time elapsed since first capital entry (in years) to annualize your return var sortedCap = capitalHistory.slice().sort(function(a,b){return (a.ts||0)-(b.ts||0);}); var firstTs = sortedCap.length ? sortedCap[0].ts : Date.now(); var daysElapsed = Math.max(1, (Date.now()-firstTs)/86400000); var yearsElapsed = daysElapsed/365; // Your annualized return % var totalReturnPct = (totalPnl/totalCapital)*100; var annualizedPct = yearsElapsed > 0 ? (totalReturnPct/yearsElapsed) : totalReturnPct; // ── Goal 1: Beat Inflation (3%) + Zakat (2.5%) = 5.5% combined hurdle ── var hurdle1 = 5.5; var beat1 = annualizedPct >= hurdle1; var gap1 = annualizedPct - hurdle1; var statusEl1 = document.getElementById('goalInflationStatus'); var subEl1 = document.getElementById('goalInflationSub'); var badgeEl1 = document.getElementById('goalInflationBadge'); var cardEl1 = document.getElementById('goalInflationCard'); if(statusEl1){ statusEl1.textContent = (annualizedPct>=0?'+':'') + annualizedPct.toFixed(2) + '%'; statusEl1.style.color = beat1 ? 'var(--green)' : 'var(--red)'; } if(subEl1){ subEl1.textContent = beat1 ? '✅ Beating by ' + (gap1>=0?'+':'') + gap1.toFixed(2) + 'pp annualized' : '⚠️ Behind by ' + Math.abs(gap1).toFixed(2) + 'pp — need ' + hurdle1 + '%/yr'; } if(badgeEl1){ badgeEl1.textContent = beat1 ? '✅ ON TRACK' : '⚠️ BEHIND'; badgeEl1.style.background = beat1 ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)'; badgeEl1.style.color = beat1 ? 'var(--green)' : 'var(--red)'; } if(cardEl1) cardEl1.style.borderColor = beat1 ? 'rgba(34,197,94,.35)' : 'rgba(239,68,68,.35)'; // ── Goal 2: Exceed Real Estate (6%) ── var hurdle2 = 6.0; var beat2 = annualizedPct >= hurdle2; var gap2 = annualizedPct - hurdle2; var statusEl2 = document.getElementById('goalRealEstateStatus'); var subEl2 = document.getElementById('goalRealEstateSub'); var badgeEl2 = document.getElementById('goalRealEstateBadge'); var cardEl2 = document.getElementById('goalRealEstateCard'); if(statusEl2){ statusEl2.textContent = (annualizedPct>=0?'+':'') + annualizedPct.toFixed(2) + '%'; statusEl2.style.color = beat2 ? 'var(--green)' : 'var(--red)'; } if(subEl2){ subEl2.textContent = beat2 ? '✅ Exceeding by ' + (gap2>=0?'+':'') + gap2.toFixed(2) + 'pp annualized' : '⚠️ Behind by ' + Math.abs(gap2).toFixed(2) + 'pp — need ' + hurdle2 + '%/yr'; } if(badgeEl2){ badgeEl2.textContent = beat2 ? '✅ ON TRACK' : '⚠️ BEHIND'; badgeEl2.style.background = beat2 ? 'rgba(34,197,94,.15)' : 'rgba(239,68,68,.15)'; badgeEl2.style.color = beat2 ? 'var(--green)' : 'var(--red)'; } if(cardEl2) cardEl2.style.borderColor = beat2 ? 'rgba(34,197,94,.35)' : 'rgba(239,68,68,.35)'; } // ════════════════════════════════════════════════ // PROFILE ARCADE SUMMARY // ════════════════════════════════════════════════ function toggleArcadeGuide(){ var g = document.getElementById('arcadeGuide'); var btn = document.getElementById('arcadeGuideToggle'); if(!g) return; var isOpen = g.style.display !== 'none'; g.style.display = isOpen ? 'none' : 'block'; if(btn) btn.textContent = isOpen ? 'ℹ️ How to earn' : '✕ Hide guide'; } function renderProfileArcadeSummary(){ if(typeof arcadeState === 'undefined') return; var pts = arcadeState.totalPoints || 0; var level = getCurrentLevel(pts); var nextLevel = getNextLevel(level.num); var badgeEl = document.getElementById('profArcBadge'); var numEl = document.getElementById('profArcLevelNum'); var nameEl = document.getElementById('profArcLevelName'); var ptsEl = document.getElementById('profArcPoints'); var barEl = document.getElementById('profArcBar'); var progEl = document.getElementById('profArcProgress'); if(badgeEl) badgeEl.textContent = level.icon; if(numEl) numEl.textContent = 'Level ' + level.num; if(nameEl) nameEl.textContent = level.name; if(ptsEl) ptsEl.textContent = pts.toLocaleString(); if(nextLevel){ var range = nextLevel.minPts - level.minPts; var progress = pts - level.minPts; var pct = Math.min(100, (progress/range)*100); if(barEl) barEl.style.width = pct + '%'; if(progEl) progEl.textContent = progress + ' / ' + range + ' pts to ' + nextLevel.name; } else { if(barEl) barEl.style.width = '100%'; if(progEl) progEl.textContent = '👑 Max level reached!'; } // Badge tier counts var gold=0, silver=0, bronze=0; Object.keys(arcadeState.records||{}).forEach(function(k){ var t = arcadeState.records[k].tier; if(t==='gold') gold++; else if(t==='silver') silver++; else if(t==='bronze') bronze++; }); var gEl = document.getElementById('profGoldCount'); if(gEl) gEl.textContent = gold; var sEl = document.getElementById('profSilverCount'); if(sEl) sEl.textContent = silver; var bEl = document.getElementById('profBronzeCount'); if(bEl) bEl.textContent = bronze; } // ════════════════════════════════════════════════ // TRADER ARCADE — GAMIFICATION ENGINE // ════════════════════════════════════════════════ var arcadeState = JSON.parse(localStorage.getItem('mg_arcade') || JSON.stringify({ totalPoints: 0, pointsLog: [], processedTradeIds: [], // trade ids already scored processedCapitalIds: [], // capital entries already scored records: { dailyProfit: { value: 0, tier: 'none', date: null }, dailyAllocation:{ value: 0, tier: 'none', date: null }, dailyGridMax: { value: 0, tier: 'none', date: null }, // most 3-trade days streak weeklyProfit: { value: 0, tier: 'none', date: null }, weeklyDiversity:{ value: 0, tier: 'none', date: null }, weeklyStreak: { value: 0, tier: 'none' }, // unbroken ledger - consecutive weeks logged monthlyProfit: { value: 0, tier: 'none', date: null }, monthlyDeepRecovery: { value: 0, tier: 'none', date: null }, // most all-orders-filled-then-profit in a month monthlyCapitalEscalator: { value: 0, tier: 'none', date: null }, // most mid-trade investment increases in a month } })); var LEVELS = [ { num:1, name:'Tester', icon:'🧪', minPts:0 }, { num:2, name:'Grid Builder', icon:'🏗️', minPts:100 }, { num:3, name:'Dip Manager', icon:'📉', minPts:500 }, { num:4, name:'Cycle Runner', icon:'🔄', minPts:1500 }, { num:5, name:'The Ultimate Averager', icon:'👑', minPts:5000 }, ]; function saveArcadeState(){ localStorage.setItem('mg_arcade', JSON.stringify(arcadeState)); } function getCurrentLevel(pts){ var lvl = LEVELS[0]; for(var i=0;i= LEVELS[i].minPts) lvl = LEVELS[i]; } return lvl; } function getNextLevel(currentLevelNum){ return LEVELS.find(function(l){ return l.num === currentLevelNum+1; }) || null; } function awardPoints(amount, label, dateStr){ arcadeState.totalPoints += amount; arcadeState.pointsLog.unshift({ amount: amount, label: label, date: dateStr || new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}), ts: Date.now() }); if(arcadeState.pointsLog.length > 200) arcadeState.pointsLog = arcadeState.pointsLog.slice(0,200); } function dayKey(ts){ return new Date(ts).toISOString().split('T')[0]; } function weekKey(ts){ var d = new Date(ts); var day = d.getDay(); var diffToMon = (day+6)%7; var monday = new Date(d); monday.setDate(d.getDate()-diffToMon); monday.setHours(0,0,0,0); return monday.toISOString().split('T')[0]; } function monthKey(ts){ var d = new Date(ts); return d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0'); } // ── Main engine: scan trade & capital history for new events ── function recalculatePoints(){ // Reset and rebuild from scratch for accuracy arcadeState.totalPoints = 0; arcadeState.pointsLog = []; arcadeState.processedTradeIds = []; arcadeState.processedCapitalIds = []; // Keep records but reset values to re-derive from history Object.keys(arcadeState.records).forEach(function(k){ if(arcadeState.records[k].value !== undefined) arcadeState.records[k].value = 0; arcadeState.records[k].tier = 'none'; }); processArcadeEvents(); saveArcadeState(); renderArcadeDashboard(); showToast('🎮 Points recalculated from full history'); } function processArcadeEvents(){ var trades = (tradeHistory||[]).slice().sort(function(a,b){return a.ts-b.ts;}); var capitals = (capitalHistory||[]).slice().sort(function(a,b){return (a.ts||0)-(b.ts||0);}); // ── Group trades by day/week/month for bonus calculations ── var byDay = {}, byWeek = {}, byMonth = {}; trades.forEach(function(t){ var dk=dayKey(t.ts), wk=weekKey(t.ts), mk=monthKey(t.ts); (byDay[dk]=byDay[dk]||[]).push(t); (byWeek[wk]=byWeek[wk]||[]).push(t); (byMonth[mk]=byMonth[mk]||[]).push(t); }); // ── Per-trade points ── trades.forEach(function(t){ var tid = t.id || t.ts; if(arcadeState.processedTradeIds.indexOf(tid) >= 0) return; // already scored var dateStr = new Date(t.ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short'}); // +10 completed Martingale cycle awardPoints(10, '✅ Completed cycle: '+t.pair, dateStr); // +5 full details logged (we always have entry/orders/exit in this system) if(t.avgBuy && t.sellPrice && t.ordersCount) awardPoints(5, '📝 Full trade details logged', dateStr); // +10 hit ALL configured safety orders/adds var configuredAdds = parseInt(document.getElementById('numOrders')&&document.getElementById('numOrders').value || 0); if(configuredAdds > 0 && t.ordersCount >= configuredAdds){ awardPoints(10, '🎯 Hit ALL safety orders ('+t.ordersCount+'/'+configuredAdds+')', dateStr); // Track Deep Recovery: all orders filled AND closed in profit if(t.pnl > 0){ var mk = monthKey(t.ts); arcadeState._drCount = arcadeState._drCount || {}; arcadeState._drCount[mk] = (arcadeState._drCount[mk]||0) + 1; } } arcadeState.processedTradeIds.push(tid); }); // ── Diversify coins: new pair not logged this week ── Object.keys(byWeek).forEach(function(wk){ var weekTrades = byWeek[wk].slice().sort(function(a,b){return a.ts-b.ts;}); var seenPairs = {}; weekTrades.forEach(function(t){ var tid = t.id||t.ts; var key = 'diversify_'+tid; if(arcadeState.processedTradeIds.indexOf('DIV_'+tid) >= 0) return; if(!seenPairs[t.pair]){ seenPairs[t.pair] = true; if(Object.keys(seenPairs).length > 1){ // not the first pair of the week awardPoints(25, '🌐 Diversified into '+t.pair, new Date(t.ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short'})); } } arcadeState.processedTradeIds.push('DIV_'+tid); }); }); // ── 3 trades in a day bonus ── Object.keys(byDay).forEach(function(dk){ var key = 'DAY3_'+dk; if(arcadeState.processedTradeIds.indexOf(key) >= 0) return; if(byDay[dk].length >= 3){ awardPoints(15, '🔥 3+ trades in one day ('+dk+')', dk); } arcadeState.processedTradeIds.push(key); }); // ── Capital increase events (manual mid-trade investment increase) ── capitals.forEach(function(c){ var cid = c.ts; if(arcadeState.processedCapitalIds.indexOf(cid) >= 0) return; awardPoints(20, '💰 Increased investment +'+fmt(c.amount)+' USDT', c.date); arcadeState.processedCapitalIds.push(cid); var mk = monthKey(c.ts); arcadeState._capEscCount = arcadeState._capEscCount || {}; arcadeState._capEscCount[mk] = (arcadeState._capEscCount[mk]||0) + 1; }); // ── Personal Records: scan all periods ── checkDailyRecords(byDay); checkWeeklyRecords(byWeek); checkMonthlyRecords(byMonth); } function tierForRecord(breakCount){ if(breakCount >= 5) return 'gold'; if(breakCount >= 2) return 'silver'; if(breakCount >= 1) return 'bronze'; return 'none'; } function checkDailyRecords(byDay){ var bestDayProfit = 0, bestDayProfitKey = null; var bestDayAlloc = 0, bestDayAllocKey = null; var max3DayStreak = 0, current3Streak = 0; var sortedDays = Object.keys(byDay).sort(); sortedDays.forEach(function(dk){ var trades = byDay[dk]; var dayPnl = trades.reduce(function(s,t){return s+(t.pnl||0);},0); var dayAlloc = trades.reduce(function(s,t){return s+(t.invested||0);},0); if(dayPnl > bestDayProfit){ bestDayProfit = dayPnl; bestDayProfitKey = dk; } if(dayAlloc > bestDayAlloc){ bestDayAlloc = dayAlloc; bestDayAllocKey = dk; } if(trades.length >= 3){ current3Streak++; if(current3Streak>max3DayStreak) max3DayStreak=current3Streak; } else { current3Streak = 0; } }); updateRecord('dailyProfit', bestDayProfit, bestDayProfitKey, '🏆 New Daily Profit Record!'); updateRecord('dailyAllocation', bestDayAlloc, bestDayAllocKey, '🏆 New Max Daily Allocation Record!'); updateRecord('dailyGridMax', max3DayStreak, null, '🏆 New 3-Trade-Day Streak Record!'); } function checkWeeklyRecords(byWeek){ var bestWeekProfit = 0, bestWeekKey = null; var bestDiversity = 0, bestDiversityKey = null; Object.keys(byWeek).forEach(function(wk){ var trades = byWeek[wk]; var weekPnl = trades.reduce(function(s,t){return s+(t.pnl||0);},0); var uniquePairs = {}; trades.forEach(function(t){ uniquePairs[t.pair]=true; }); var diversity = Object.keys(uniquePairs).length; if(weekPnl > bestWeekProfit){ bestWeekProfit = weekPnl; bestWeekKey = wk; } if(diversity > bestDiversity){ bestDiversity = diversity; bestDiversityKey = wk; } }); updateRecord('weeklyProfit', bestWeekProfit, bestWeekKey, '🏆 New Weekly Profit Peak!'); updateRecord('weeklyDiversity', bestDiversity, bestDiversityKey, '🏆 New Multi-Coiner Record!'); // Unbroken ledger: consecutive weeks with at least 1 trade var weekKeys = Object.keys(byWeek).sort(); var maxStreak = 0, curStreak = 0, prevWeekDate = null; weekKeys.forEach(function(wk){ var wDate = new Date(wk); if(prevWeekDate){ var diffWeeks = Math.round((wDate-prevWeekDate)/(7*86400000)); if(diffWeeks === 1) curStreak++; else curStreak = 1; } else curStreak = 1; if(curStreak > maxStreak) maxStreak = curStreak; prevWeekDate = wDate; }); updateRecord('weeklyStreak', maxStreak, null, '🏆 New Unbroken Ledger Streak!'); } function checkMonthlyRecords(byMonth){ var bestMonthProfit = 0, bestMonthKey = null; Object.keys(byMonth).forEach(function(mk){ var trades = byMonth[mk]; var monthPnl = trades.reduce(function(s,t){return s+(t.pnl||0);},0); if(monthPnl > bestMonthProfit){ bestMonthProfit = monthPnl; bestMonthKey = mk; } }); updateRecord('monthlyProfit', bestMonthProfit, bestMonthKey, '🏆 New Monthly Profit Peak!'); // Deep Recovery: most "all-orders-filled-then-profit" trades in a month var bestDR = 0, bestDRKey = null; Object.keys(arcadeState._drCount||{}).forEach(function(mk){ var c = arcadeState._drCount[mk]; if(c > bestDR){ bestDR = c; bestDRKey = mk; } }); updateRecord('monthlyDeepRecovery', bestDR, bestDRKey, '🏆 New Deep Recovery Master Record!'); // Capital Escalator: most mid-trade investment increases in a month var bestCE = 0, bestCEKey = null; Object.keys(arcadeState._capEscCount||{}).forEach(function(mk){ var c = arcadeState._capEscCount[mk]; if(c > bestCE){ bestCE = c; bestCEKey = mk; } }); updateRecord('monthlyCapitalEscalator', bestCE, bestCEKey, '🏆 New Capital Escalator Record!'); } function updateRecord(key, newVal, periodKey, announceMsg){ var rec = arcadeState.records[key]; if(!rec) return; if(newVal > rec.value){ var wasZero = rec.value === 0; rec.value = newVal; rec.date = periodKey; // Tier progression: bronze -> silver -> gold based on number of times broken rec._breaks = (rec._breaks||0) + 1; rec.tier = tierForRecord(rec._breaks); if(!wasZero){ // don't award bonus for the very first data point awardPoints(50, announceMsg, periodKey); var notifKey = 'RECORD_'+key+'_'+periodKey; if(arcadeState.processedTradeIds.indexOf(notifKey) < 0){ addNotification('alert', announceMsg, 'New record: '+fmt(newVal)+(key.indexOf('Profit')>=0?' USDT':'')); arcadeState.processedTradeIds.push(notifKey); } } } } // ── Render Dashboard ── function renderArcadeDashboard(){ var pts = arcadeState.totalPoints; var level = getCurrentLevel(pts); var nextLevel = getNextLevel(level.num); var badgeEl = document.getElementById('arcLevelBadge'); var numEl = document.getElementById('arcLevelNum'); var nameEl = document.getElementById('arcLevelName'); var barEl = document.getElementById('arcLevelBar'); var progEl = document.getElementById('arcLevelProgress'); var totalEl = document.getElementById('arcTotalPoints'); if(badgeEl) badgeEl.textContent = level.icon; if(numEl) numEl.textContent = 'Level ' + level.num; if(nameEl) nameEl.textContent = level.name; if(totalEl) totalEl.textContent = pts.toLocaleString(); if(nextLevel){ var range = nextLevel.minPts - level.minPts; var progress = pts - level.minPts; var pct = Math.min(100, (progress/range)*100); if(barEl) barEl.style.width = pct + '%'; if(progEl) progEl.textContent = progress + ' / ' + range + ' pts to ' + nextLevel.name; } else { if(barEl) barEl.style.width = '100%'; if(progEl) progEl.textContent = '👑 Max level reached!'; } // Level roadmap var roadmap = document.getElementById('arcLevelRoadmap'); if(roadmap){ roadmap.innerHTML = LEVELS.map(function(l){ var cls = l.num < level.num ? 'done' : l.num === level.num ? 'current' : ''; return '
' + '
'+l.icon+'
' + '
'+l.name+'
' + '
'+l.minPts.toLocaleString()+' pts
' + '
'; }).join(''); } // Points log var logEl = document.getElementById('arcPointsLog'); if(logEl){ if(arcadeState.pointsLog.length === 0){ logEl.innerHTML = '
No points yet — close some trades to start earning!
'; } else { logEl.innerHTML = arcadeState.pointsLog.slice(0,50).map(function(p){ return '
' + '
' + '
' + '
'+p.label+'
' + '
'+p.date+'
' + '
' + '
+'+p.amount+'
' + '
'; }).join(''); } } // Records renderRecordCards('arcDailyRecords', [ { key:'dailyProfit', icon:'💎', label:'Apex Trade (Highest Profit)', fmt:function(v){return fmt(v)+' USDT';} }, { key:'dailyAllocation', icon:'🏦', label:'Max Allocation', fmt:function(v){return fmt(v)+' USDT';} }, { key:'dailyGridMax', icon:'🔥', label:'Grid Max (3-Trade Day Streak)', fmt:function(v){return v+' days';} }, ]); renderRecordCards('arcWeeklyRecords', [ { key:'weeklyProfit', icon:'📈', label:'Weekly Profit Peak', fmt:function(v){return fmt(v)+' USDT';} }, { key:'weeklyDiversity', icon:'🌐', label:'The Multi-Coiner', fmt:function(v){return v+' coins';} }, { key:'weeklyStreak', icon:'🔗', label:'Unbroken Ledger', fmt:function(v){return v+' weeks';} }, ]); renderRecordCards('arcMonthlyRecords', [ { key:'monthlyProfit', icon:'🚀', label:'Monthly Profit Peak', fmt:function(v){return fmt(v)+' USDT';} }, { key:'monthlyDeepRecovery', icon:'🛟', label:'Deep Recovery Master', fmt:function(v){return v+' saves';} }, { key:'monthlyCapitalEscalator', icon:'⬆️', label:'Capital Escalator', fmt:function(v){return v+' boosts';} }, ]); } function renderRecordCards(containerId, configs){ var el = document.getElementById(containerId); if(!el) return; var TIER_ICONS = { none:'⬜', bronze:'🥉', silver:'🥈', gold:'🥇' }; el.innerHTML = configs.map(function(cfg){ var rec = arcadeState.records[cfg.key]; var tier = rec.tier || 'none'; return '
' + '
'+TIER_ICONS[tier]+'
' + '
'+cfg.icon+'
' + '
'+cfg.label+'
' + '
'+cfg.fmt(rec.value||0)+'
' + (rec.date ? '
'+rec.date+'
' : '
No data yet
') + '
'; }).join(''); } // Run on load and after every trade close / capital change function updateArcade(){ processArcadeEvents(); saveArcadeState(); renderArcadeDashboard(); } // ════════════════════════════════════════════════ // AI PERFORMANCE REPORTS (Daily/Weekly/Monthly/Yearly) // ════════════════════════════════════════════════ var lastReportCheck = JSON.parse(localStorage.getItem('mg_last_reports') || '{}'); function gatherPerformanceData(periodDays){ var now = Date.now(); var cutoff = now - periodDays*86400000; var trades = (tradeHistory||[]).filter(function(t){ return t.ts >= cutoff; }); var totalPnl = trades.reduce(function(s,t){return s+(t.pnl||0);},0); var totalInv = trades.reduce(function(s,t){return s+(t.invested||0);},0); var wins = trades.filter(function(t){return t.pnl>0;}).length; var losses = trades.filter(function(t){return t.pnl<0;}).length; var winRate = trades.length>0 ? (wins/trades.length*100) : 0; var avgPnl = trades.length>0 ? (totalPnl/trades.length) : 0; var bestTrade = trades.reduce(function(b,t){return (!b||t.pnl>b.pnl)?t:b;}, null); var worstTrade = trades.reduce(function(w,t){return (!w||t.pnl0 ? (totalPnl/totalCapital*100) : 0, }; } function buildReportPrompt(data, periodLabel){ var pairSummary = data.topPairs.map(function(p){ return p + ': ' + (data.pairMap[p].pnl>=0?'+':'') + data.pairMap[p].pnl.toFixed(2) + ' USDT (' + data.pairMap[p].count + ' trades)'; }).join(', '); var vibe = data.totalPnl > 0 && data.winRate >= 80 ? 'crushing it' : data.totalPnl > 0 ? 'solidly profitable' : data.totalPnl === 0 ? 'break-even' : 'in the red'; return 'Write an EXCITING, energetic trading performance report for the ' + periodLabel + ' period. ' + 'The trader is ' + vibe + ' right now. Use a confident, hype-but-honest tone like a trading coach celebrating wins or giving real talk on losses. ' + 'Format (max 90 words total):\n' + '1) An exciting one-line headline with an emoji that matches the mood (🔥🚀💪 for wins, 💡🎯 for learning moments)\n' + '2) One standout insight or pattern — make it feel like a discovery\n' + '3) One punchy, specific actionable tip tied directly to the numbers\n' + 'Use bold confident language. Avoid corporate/dry tone. No generic advice like "diversify" without tying to actual numbers.\n\n' + 'DATA:\n' + '- Trades closed: ' + data.tradeCount + '\n' + '- Total P&L: ' + (data.totalPnl>=0?'+':'') + data.totalPnl.toFixed(2) + ' USDT\n' + '- Win rate: ' + data.winRate.toFixed(1) + '% (' + data.wins + 'W / ' + data.losses + 'L)\n' + '- Avg P&L per trade: ' + data.avgPnl.toFixed(2) + ' USDT\n' + '- Best trade: ' + (data.bestTrade ? data.bestTrade.pair+' '+(data.bestTrade.pnl>=0?'+':'')+data.bestTrade.pnl.toFixed(2)+' USDT' : 'none') + '\n' + '- Worst trade: ' + (data.worstTrade ? data.worstTrade.pair+' '+data.worstTrade.pnl.toFixed(2)+' USDT' : 'none') + '\n' + '- Top pairs: ' + (pairSummary || 'none') + '\n' + '- Total fees paid: ' + data.totalFees.toFixed(2) + ' USDT\n' + '- Return on capital: ' + (data.pnlPctOnCapital>=0?'+':'') + data.pnlPctOnCapital.toFixed(2) + '%'; } function getReportVibe(data){ if(data.tradeCount === 0) return { emoji:'📊', headline:'No trades this period', color:'var(--muted)' }; if(data.winRate === 100 && data.tradeCount >= 3) return { emoji:'🔥', headline:'PERFECT STREAK!', color:'#f59e0b' }; if(data.totalPnl > 0 && data.winRate >= 80) return { emoji:'🚀', headline:'Crushing it!', color:'var(--green)' }; if(data.totalPnl > 0) return { emoji:'💪', headline:'Solid profit', color:'var(--green)' }; if(data.totalPnl === 0) return { emoji:'⚖️', headline:'Break-even', color:'var(--muted)' }; return { emoji:'💡', headline:'Learning moment', color:'var(--amber)' }; } async function generateAIReport(periodKey, periodDays, periodLabel, icon){ var data = gatherPerformanceData(periodDays); if(data.tradeCount === 0){ // No trades — skip silently, don't spam notifications return; } var prompt = buildReportPrompt(data, periodLabel); try { var response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 300, system: 'You are a sharp, no-nonsense crypto trading performance analyst. Be specific and data-driven. Never use generic advice like "diversify" or "manage risk" without tying it to the actual numbers given.', messages: [{ role:'user', content: prompt }] }) }); var json = await response.json(); var reportText = (json.content && json.content[0] && json.content[0].text) || null; if(reportText){ addNotification('report', icon+' '+periodLabel+' Performance Report', reportText); } else { // Fallback to a basic stats notification if AI call fails addNotification('report', icon+' '+periodLabel+' Performance Report', data.tradeCount+' trades · '+(data.totalPnl>=0?'+':'')+fmt(data.totalPnl)+' USDT · '+data.winRate.toFixed(0)+'% win rate'); } } catch(e){ addNotification('report', icon+' '+periodLabel+' Performance Report', data.tradeCount+' trades · '+(data.totalPnl>=0?'+':'')+fmt(data.totalPnl)+' USDT · '+data.winRate.toFixed(0)+'% win rate'); } lastReportCheck[periodKey] = new Date().toISOString(); localStorage.setItem('mg_last_reports', JSON.stringify(lastReportCheck)); } function checkScheduledReports(){ var now = new Date(); var today = now.toISOString().split('T')[0]; // Daily report — once per day, any time after 00:00 var lastDaily = lastReportCheck.daily ? lastReportCheck.daily.split('T')[0] : null; if(lastDaily !== today){ generateAIReport('daily', 1, 'Daily', '📅'); } // Weekly report — every Monday var lastWeekly = lastReportCheck.weekly ? new Date(lastReportCheck.weekly) : null; var daysSinceWeekly = lastWeekly ? Math.floor((now-lastWeekly)/86400000) : 999; if(now.getDay() === 1 && daysSinceWeekly >= 6){ generateAIReport('weekly', 7, 'Weekly', '📊'); } // Monthly report — 1st of each month var lastMonthly = lastReportCheck.monthly ? lastReportCheck.monthly.slice(0,7) : null; var thisMonth = today.slice(0,7); if(now.getDate() === 1 && lastMonthly !== thisMonth){ generateAIReport('monthly', 30, 'Monthly', '📈'); } // Yearly report — Jan 1st var lastYearly = lastReportCheck.yearly ? lastReportCheck.yearly.slice(0,4) : null; var thisYear = today.slice(0,4); if(now.getMonth()===0 && now.getDate()===1 && lastYearly !== thisYear){ generateAIReport('yearly', 365, 'Yearly', '🏆'); } } // Manual trigger — for testing or on-demand reports function generateReportNow(period){ var map = { daily: {days:1, label:'Daily', icon:'📅'}, weekly: {days:7, label:'Weekly', icon:'📊'}, monthly: {days:30, label:'Monthly', icon:'📈'}, yearly: {days:365, label:'Yearly', icon:'🏆'}, }; var cfg = map[period]; if(!cfg){ showToast('Invalid period.'); return; } showToast('Generating '+cfg.label+' AI report…'); generateAIReport(period+'_manual', cfg.days, cfg.label, cfg.icon); } // ════════════════════════════════════════════════ // CLOSE PREVIEW (simulate closing at any price) // ════════════════════════════════════════════════ function openClosePreview(){ var active = currentOrders.filter(function(o){ return o.activated; }); if(active.length === 0){ showToast('No activated orders to preview.'); return; } var last = active[active.length-1]; // Open the input modal first instead of jumping straight to report showClosePriceInput(last); } function showClosePriceInput(last){ var customInput = document.getElementById('customSellPrice'); var prefill = customInput && customInput.value ? customInput.value : last.sellPrice; var panel = document.getElementById('tdPanel'); panel.innerHTML = '
' + '' + '
Preview Close
' + '
' + '
' + '
' + '
Enter a price to simulate closing this trade
' + '
Selling Price (USDT)
' + '
' + '' + '
USDT
' + '
' + '
Current TP price: '+fmt(last.sellPrice,2)+' USDT
' + '' + '
'; document.getElementById('tdOverlay').style.display='flex'; document.body.style.overflow='hidden'; setTimeout(function(){ var inp = document.getElementById('previewPriceInput'); if(inp){ inp.focus(); inp.select(); } updateClosePreviewLive(); }, 100); } function updateClosePreviewLive(){ var active = currentOrders.filter(function(o){ return o.activated; }); if(active.length === 0) return; var last = active[active.length-1]; var inp = document.getElementById('previewPriceInput'); var out = document.getElementById('previewLivePnl'); if(!inp || !out) return; var p = parseFloat(inp.value); if(!p || isNaN(p)){ out.textContent='Current TP price: '+fmt(last.sellPrice,2)+' USDT'; out.style.color='var(--muted)'; return; } var pnl = (p - last.avgPrice) * last.execQty; var pct = ((p - last.avgPrice) / last.avgPrice) * 100; var isPos = pnl >= 0; out.innerHTML = (isPos?'+':'')+fmt(pnl)+' USDT ('+(isPos?'+':'')+fmt(pct,2)+'%)'; out.style.color = isPos ? 'var(--green)' : 'var(--red)'; } function runClosePreview(){ var active = currentOrders.filter(function(o){ return o.activated; }); if(active.length === 0){ showToast('No activated orders.'); return; } var last = active[active.length-1]; var inp = document.getElementById('previewPriceInput'); var simPrice = inp ? parseFloat(inp.value) : NaN; if(!simPrice || simPrice <= 0){ showToast('Enter a valid price.'); return; } var base = document.getElementById('base') ? document.getElementById('base').value : 'BTC'; var pair = base + '/USDT'; var pnl = (simPrice - last.avgPrice) * last.execQty; var pnlPct = ((simPrice - last.avgPrice) / last.avgPrice) * 100; // Fees var s = JSON.parse(localStorage.getItem('mg_settings')||'{}'); var makerR = parseFloat(s.makerFee||0.08)/100 * (s.kcsDiscount?0.8:1); var takerR = parseFloat(s.takerFee||0.10)/100 * (s.kcsDiscount?0.8:1); var buyFee = last.execAmount * makerR; var sellFee = (simPrice * last.execQty) * takerR; var totalFee= buyFee + sellFee; var netPnl = pnl - totalFee; var netPnlPct = (netPnl / last.execAmount) * 100; var isWin = netPnl >= 0; var pnlColor = isWin ? '#2DD4BF' : '#f85149'; // APR estimate (annualized based on a 1-day hold assumption — preview only) var apr = (netPnl / last.execAmount) * 365 * 100; var priceLow = fmt(Math.min(last.avgPrice, simPrice), 4); var priceHigh = fmt(Math.max(last.avgPrice, simPrice), 4); var coinSymbol = base; var returnCoin = fmt(last.execQty, 4) + ' ' + coinSymbol; var returnUSDT = fmt(simPrice * last.execQty, 4) + ' USDT'; var panel = document.getElementById('tdPanel'); panel.innerHTML = '
' + '' + '
Close Preview
' + '
' + '
' + '
' + '🔍 SIMULATION — This trade has not been closed yet' + '
' + '
' + '
' + '
📈
' + '
' + '
Spot Martingale
' + '
'+pair+'
' + '
' + '
' + '
⏳ Active (Preview)
' + '
' + '
' + 'Would close at: ' + new Date().toLocaleString('en-GB') + '
' + active.length + ' orders filled · ' + active.length + ' arbitrages' + '
' + '
' + '
' + '
' + '
Investment
' + '
'+fmt(last.execAmount)+' USDT
' + '
' + '
' + '
' + '
Total Profit
' + '
'+(netPnl>=0?'+':'')+fmt(netPnl,2)+' USDT
' + '
'+(netPnlPct>=0?'+':'')+fmt(netPnlPct,2)+'%
' + '
' + '
' + '
' + 'Funds would return: '+returnUSDT + '
' + '
' + '
' + tdGridItem('Gross P&L', (pnl>=0?'+':'')+fmt(pnl,2), pnl>=0?'#2DD4BF':'#f85149') + tdGridItem('Net P&L', (netPnl>=0?'+':'')+fmt(netPnl,2), pnlColor) + tdGridItem('Avg. Buy Price', fmt(last.avgPrice,2)) + tdGridItem('Price Range', priceLow+' ~ '+priceHigh) + tdGridItem('Close Price', fmt(simPrice,2), 'var(--accent)') + tdGridItem('Buy Fee', '−'+fmt(buyFee,2), '#f85149') + tdGridItem('Sell Fee', '−'+fmt(sellFee,2), '#f85149') + tdGridItem('Total Fees', '−'+fmt(totalFee,2), '#f85149') + '
' + '
' + '' + '' + '
'; document.getElementById('tdOverlay').style.display='flex'; document.body.style.overflow='hidden'; } // ════════════════════════════════════════════════ // ZAKAT DASHBOARD // ════════════════════════════════════════════════ var zakatPayments = JSON.parse(localStorage.getItem('mg_zakat_payments') || '[]'); function renderZakatDashboard(){ var nisab = parseFloat(document.getElementById('nisabAmount')&&document.getElementById('nisabAmount').value||5500); // Total capital deployed = all invested capital entries var totalCapital = capitalHistory.reduce(function(s,c){return s+(c.amount||0);},0); // Total P&L var totalPnl = (tradeHistory||[]).reduce(function(s,t){return s+(t.pnl||0);},0); // Total zakatable portfolio var totalPortfolio = totalCapital + totalPnl; // Zakat amount var zakatDue = totalPortfolio >= nisab ? totalPortfolio * 0.025 : 0; var aboveNisab = totalPortfolio >= nisab; // Next 1 Jan var now = new Date(); var nextJan = new Date(now.getFullYear() + (now.getMonth()===0&&now.getDate()===1?0:1), 0, 1); var daysUntil = Math.ceil((nextJan-now)/86400000); // Update UI var zkD = document.getElementById('zk-deployed'); if(zkD) zkD.textContent = fmt(totalCapital); var zkP = document.getElementById('zk-pnl'); if(zkP){ zkP.textContent=(totalPnl>=0?'+':'')+fmt(totalPnl); zkP.style.color=totalPnl>=0?'var(--green)':'var(--red)'; } var zkT = document.getElementById('zk-total'); if(zkT) zkT.textContent = fmt(totalPortfolio); var zkA = document.getElementById('zk-amount'); if(zkA) zkA.textContent = fmt(zakatDue,2)+' USDT'; var zkF = document.getElementById('zk-formula'); if(zkF) zkF.textContent = aboveNisab ? ('= '+fmt(totalPortfolio)+' USDT × 2.5% = '+fmt(zakatDue,2)+' USDT') : 'Portfolio below Nisab threshold — no Zakat due'; var zkN = document.getElementById('zk-nisab-status'); if(zkN){ zkN.textContent = aboveNisab ? '✅ Above Nisab' : '❌ Below Nisab'; zkN.style.color = aboveNisab?'var(--green)':'var(--red)'; } var zkNd = document.getElementById('zk-next-due'); if(zkNd) zkNd.textContent = '1 Jan '+nextJan.getFullYear()+' ('+daysUntil+'d)'; // Zakat payment log renderZakatPaymentLog(zakatDue); // Zakat chart renderZakatDueChart(totalPortfolio); } function renderZakatPaymentLog(zakatDue){ var log = document.getElementById('zakatPaymentLog'); if(!log) return; if(zakatPayments.length===0){ log.innerHTML='
No Zakat payments recorded. Click + Record Payment when you pay.
'; return; } var totalPaid = zakatPayments.reduce(function(s,p){return s+(p.amount||0);},0); log.innerHTML = '
'+ 'Total Paid: '+fmt(totalPaid,2)+' USDT'+ (zakatDue>0?(''+ (totalPaid>=zakatDue?'\u2705 Fully Paid':'\u26a0\ufe0f Remaining: '+fmt(zakatDue-totalPaid,2))+''):'')+ '
'+ zakatPayments.map(function(p,i){ return '
'+ '
'+(zakatPayments.length-i)+'
'+ '
'+p.date+'
'+ '
'+fmt(p.amount,2)+' USDT
'+ ''+ '
'; }).join(''); } function addZakatPayment(){ var nisab = parseFloat(document.getElementById('nisabAmount')&&document.getElementById('nisabAmount').value||5500); var totalCapital = capitalHistory.reduce(function(s,c){return s+(c.amount||0);},0); var totalPnl = (tradeHistory||[]).reduce(function(s,t){return s+(t.pnl||0);},0); var suggested = ((totalCapital+totalPnl)*0.025).toFixed(2); var val = prompt('Enter Zakat amount paid (USDT):\nSuggested: '+suggested+' USDT', suggested); if(!val || isNaN(parseFloat(val))) return; var amount = parseFloat(val); var now = new Date(); zakatPayments.unshift({ amount:amount, date:now.toLocaleString('en-GB'), ts:now.getTime(), year:now.getFullYear() }); localStorage.setItem('mg_zakat_payments', JSON.stringify(zakatPayments)); renderZakatDashboard(); showToast('☪️ Zakat payment of '+fmt(amount,2)+' USDT recorded'); // Add notification addNotification('system','☪️ Zakat Payment Recorded','Paid '+fmt(amount,2)+' USDT on '+now.toLocaleDateString('en-GB')); } function deleteZakatPayment(idx){ idx=parseInt(idx); if(isNaN(idx)||idx<0||idx>=zakatPayments.length) return; var p=zakatPayments[idx]; showConfirm('Delete Zakat payment of '+fmt(p.amount,2)+' USDT?','Delete',function(){ zakatPayments.splice(idx,1); localStorage.setItem('mg_zakat_payments',JSON.stringify(zakatPayments)); renderZakatDashboard(); showToast('Zakat payment deleted.'); }); } function renderZakatDueChart(currentPortfolio){ var ctx = document.getElementById('zakatDueChart'); if(!ctx) return; if(zakatDueChart && typeof zakatDueChart.destroy==='function') zakatDueChart.destroy(); // Build yearly data from trade history + capital var now = new Date(); var years = []; for(var y=now.getFullYear()-3; y<=now.getFullYear()+1; y++) years.push(y); var dueBars = years.map(function(y){ if(y===now.getFullYear()) return parseFloat((currentPortfolio*0.025).toFixed(2)); return 0; // future years: 0; past years would need snapshots }); var paidBars = years.map(function(y){ return zakatPayments.filter(function(p){return p.year===y;}).reduce(function(s,p){return s+(p.amount||0);},0); }); zakatDueChart = new Chart(ctx.getContext('2d'),{ type:'bar', data:{ labels:years.map(function(y){return 'Zakat '+y;}), datasets:[ {label:'Due (2.5%)',data:dueBars,backgroundColor:'rgba(245,158,11,.4)',borderColor:'#f59e0b',borderWidth:2,borderRadius:6}, {label:'Paid',data:paidBars,backgroundColor:'rgba(34,197,94,.4)',borderColor:'#22c55e',borderWidth:2,borderRadius:6}, ] }, options:{ responsive:true,maintainAspectRatio:false, plugins:{legend:{display:true,labels:{color:'#94a3b8',font:{size:11}}}, tooltip:{callbacks:{label:function(c){return c.dataset.label+': '+fmt(c.raw,2)+' USDT';}}}}, scales:{ x:{ticks:{color:'#94a3b8',font:{size:11}},grid:{color:'#222b3a'}}, y:{ticks:{color:'#94a3b8',font:{size:10},callback:function(v){return fmt(v,2);}},grid:{color:'#222b3a'}} } } }); } // Schedule Zakat notification on 1 Jan function checkZakatNotification(){ var now = new Date(); var lastCheck = localStorage.getItem('mg_zakat_notif_year'); if(now.getMonth()===0 && now.getDate()===1 && lastCheck!==String(now.getFullYear())){ var totalCapital = capitalHistory.reduce(function(s,c){return s+(c.amount||0);},0); var totalPnl = (tradeHistory||[]).reduce(function(s,t){return s+(t.pnl||0);},0); var zakatDue = (totalCapital+totalPnl)*0.025; addNotification('alert','☪️ Zakat Due Today — 1 January', 'Portfolio value: '+fmt(totalCapital+totalPnl)+' USDT | Zakat due: '+fmt(zakatDue,2)+' USDT (2.5%)'); localStorage.setItem('mg_zakat_notif_year', String(now.getFullYear())); } } // ════════════════════════════════════════════════ // CAPITAL MANAGEMENT (log-based, like withdrawals) // ════════════════════════════════════════════════ var capitalHistory = JSON.parse(localStorage.getItem('mg_capital_log') || '[]'); var capitalLogOpen = false; function saveCapitalHistory(){ localStorage.setItem('mg_capital_log', JSON.stringify(capitalHistory)); } function addCapital(){ var inp = document.getElementById('initialCapital'); if(!inp) return; var val = parseFloat(inp.value); if(isNaN(val) || val <= 0){ showToast('Enter a valid amount.'); return; } var aedInp = document.getElementById('capAedAmount'); var aedVal = aedInp ? parseFloat(aedInp.value) : NaN; var now = new Date(); var entry = { amount: val, date: now.toLocaleString('en-GB'), ts: now.getTime() }; if(!isNaN(aedVal) && aedVal > 0) entry.aedAmount = aedVal; capitalHistory.unshift(entry); saveCapitalHistory(); inp.value = ''; if(aedInp) aedInp.value = ''; var prevEl = document.getElementById('capConvPreview'); if(prevEl){ prevEl.textContent='No fee data'; prevEl.style.color='var(--muted)'; } renderCapitalDashboard(); renderZakatDashboard(); checkZakatNotification(); renderConversionDashboard(); checkScheduledReports(); updateArcade(); renderGoalsCard(); showToast('+' + fmt(val) + ' USDT added to capital'); } function deleteCapital(idx){ idx = parseInt(idx); if(isNaN(idx) || idx < 0 || idx >= capitalHistory.length){ showToast('Entry not found.'); return; } var c = capitalHistory[idx]; showConfirm('Delete capital entry of +' + fmt(c.amount) + ' USDT?', 'Delete', function(){ capitalHistory.splice(idx, 1); saveCapitalHistory(); renderCapitalDashboard(); showToast('Capital entry deleted.'); }); } function toggleCapitalLog(){ capitalLogOpen = !capitalLogOpen; var log = document.getElementById('capitalLog'); var chev = document.getElementById('capLogChevron'); if(log) log.style.display = capitalLogOpen ? 'block' : 'none'; if(chev) chev.style.transform = capitalLogOpen ? 'rotate(180deg)' : ''; } function renderCapitalDashboard(){ // Total initial capital = sum of all capital entries var totalCapital = capitalHistory.reduce(function(s,c){ return s + (c.amount||0); }, 0); // Total P&L from all closed trades var totalPnl = (tradeHistory||[]).reduce(function(s,t){ return s + (t.pnl||0); }, 0); // Withdrawals var withdrawn = withdrawalTotal || 0; // Current capital var current = totalCapital + totalPnl - withdrawn; // P&L rate on total capital invested var pnlPct = totalCapital > 0 ? (totalPnl / totalCapital * 100) : 0; // Update summary cards var capInitEl = document.getElementById('capInitial'); var capDateEl = document.getElementById('capInitialDate'); var capCurEl = document.getElementById('capCurrent'); var capCurPctEl = document.getElementById('capCurrentPct'); var capPnlEl = document.getElementById('capTotalPnl'); var capPctEl = document.getElementById('capPnlPct'); if(capInitEl){ capInitEl.textContent = fmt(totalCapital); } if(capDateEl){ capDateEl.textContent = capitalHistory.length + ' entr' + (capitalHistory.length===1?'y':'ies') + ' · tap to view'; } if(capCurEl){ capCurEl.textContent = fmt(current); capCurEl.style.color = current >= totalCapital ? 'var(--green)' : 'var(--red)'; } if(capCurPctEl){ var curPct = totalCapital > 0 ? ((current - totalCapital) / totalCapital * 100) : 0; capCurPctEl.textContent = (curPct>=0?'+':'') + curPct.toFixed(2) + '% vs capital added'; capCurPctEl.style.color = curPct >= 0 ? 'var(--green)' : 'var(--red)'; } if(capPnlEl){ capPnlEl.textContent = (totalPnl>=0?'+':'') + fmt(totalPnl) + ' USDT'; capPnlEl.style.color = totalPnl >= 0 ? 'var(--green)' : 'var(--red)'; } if(capPctEl){ capPctEl.textContent = (pnlPct>=0?'+':'') + pnlPct.toFixed(2) + '% on initial capital'; capPctEl.style.color = pnlPct >= 0 ? 'var(--green)' : 'var(--red)'; } // Render capital log var log = document.getElementById('capitalLog'); if(log){ if(capitalHistory.length === 0){ log.innerHTML = '
No capital entries yet. Add your initial investment above.
'; } else { var total = capitalHistory.reduce(function(s,c){return s+c.amount;},0); log.innerHTML = '
' + 'Capital History · ' + fmt(total) + ' USDT total' + '
' + capitalHistory.map(function(c, i){ return '
' + '
' + (capitalHistory.length - i) + '
' + '
' + c.date + '
' + '
+' + fmt(c.amount) + ' USDT
' + '' + '
'; }).join(''); } } } renderCapitalFlowChart(); var _AR_LEGACY = [{"id":"ar_000","name":"Salah Credit Cards","amount":2500.0,"expected":2500.0,"date":"2015-10-05","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_001","name":"Khalid Abdulaziz","amount":10500.0,"expected":10500.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_002","name":"Abu Taha","amount":1000.0,"expected":1000.0,"date":"2015-05-17","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_003","name":"Awad","amount":3500.0,"expected":3500.0,"date":"2015-06-27","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_004","name":"Mohammad Atta","amount":1000.0,"expected":1000.0,"date":"2016-10-04","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_005","name":"Sameer Albelooshi","amount":6000.0,"expected":6000.0,"date":"2016-11-01","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_006","name":"Walid Al Ali","amount":30000.0,"expected":30000.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_007","name":"Farhang","amount":10000.0,"expected":10000.0,"date":"2020-05-17","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_008","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-07-05","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_009","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-08-11","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_010","name":"Ahmed Nagib","amount":3130.0,"expected":3130.0,"date":"2020-08-26","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_011","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-09-05","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_012","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-10-03","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_013","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-10-31","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_014","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2020-12-02","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_015","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-01-01","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_016","name":"Farhang","amount":4950.0,"expected":4950.0,"date":"2021-01-10","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_017","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-01-26","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_018","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-02-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_019","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-03-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_020","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-04-29","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_021","name":"Farhang","amount":3000.0,"expected":3000.0,"date":"2021-07-02","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_022","name":"Farhang","amount":3000.0,"expected":3000.0,"date":"2021-07-31","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_023","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2021-08-17","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_024","name":"Abdulla Albelooshi","amount":185.0,"expected":185.0,"date":"","dueDate":"","reason":"$185 USD","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_025","name":"Farhang","amount":40000.0,"expected":40000.0,"date":"2021-08-17","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_026","name":"Farhang","amount":10000.0,"expected":10000.0,"date":"2022-01-20","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_027","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2022-04-27","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_028","name":"Eva","amount":780.0,"expected":780.0,"date":"2022-05-19","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_029","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2022-08-27","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_030","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2022-07-29","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_031","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2022-10-18","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_032","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2022-11-19","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_033","name":"Farhang","amount":20000.0,"expected":20000.0,"date":"2023-01-23","dueDate":"","reason":"Zakah","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_034","name":"Farhang","amount":1680.0,"expected":1680.0,"date":"2023-11-29","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_035","name":"Omar Saleh","amount":11000.0,"expected":11000.0,"date":"2024-01-28","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_036","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2024-01-11","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_037","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-04-09","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_038","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-04-24","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_039","name":"Farhang","amount":3000.0,"expected":3000.0,"date":"2024-05-14","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_040","name":"Farhang","amount":2000.0,"expected":2000.0,"date":"2024-08-21","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_041","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-05-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_042","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-06-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_043","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-07-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_044","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-08-22","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_045","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-09-22","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_046","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2024-10-25","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_047","name":"Farhang","amount":5000.0,"expected":5000.0,"date":"2024-10-10","dueDate":"","reason":"Zakah","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_048","name":"Eva","amount":300.0,"expected":300.0,"date":"2024-11-09","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_049","name":"Rashed Essa","amount":6000.0,"expected":6000.0,"date":"2024-11-11","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_050","name":"Howa","amount":700.0,"expected":700.0,"date":"2024-12-15","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_051","name":"Rashed Essa","amount":2000.0,"expected":2000.0,"date":"2025-01-08","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_052","name":"Farhang","amount":1500.0,"expected":1500.0,"date":"2025-01-26","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_053","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2025-01-28","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_054","name":"Rashed Essa","amount":2000.0,"expected":2000.0,"date":"2025-02-12","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_055","name":"Zakaria","amount":13800.0,"expected":13800.0,"date":"2025-05-23","dueDate":"","reason":"from Sinwan","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_056","name":"Farhang","amount":860.0,"expected":860.0,"date":"2025-06-19","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_057","name":"Rashed Essa","amount":3000.0,"expected":3000.0,"date":"2025-06-21","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_058","name":"Rashed Essa","amount":1000.0,"expected":1000.0,"date":"2025-07-23","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_059","name":"Mawla Bakesh","amount":29000.0,"expected":29000.0,"date":"2025-09-02","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_060","name":"Rashed Essa","amount":1000.0,"expected":1000.0,"date":"2025-09-23","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_061","name":"Sayed Ali","amount":2000.0,"expected":2000.0,"date":"2025-10-06","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_062","name":"Rashed Essa","amount":1000.0,"expected":1000.0,"date":"2025-12-10","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_063","name":"Farhang","amount":600.0,"expected":600.0,"date":"2026-02-05","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_064","name":"Fahad Essa","amount":20000.0,"expected":20000.0,"date":"2026-02-24","dueDate":"","reason":"From Sinwan","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_065","name":"Rashed Essa","amount":2000.0,"expected":2000.0,"date":"2026-03-31","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_066","name":"Fahad Essa","amount":10000.0,"expected":10000.0,"date":"2026-03-24","dueDate":"","reason":"Salary","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_067","name":"Fahad Essa","amount":10000.0,"expected":10000.0,"date":"2026-04-24","dueDate":"","reason":"Salary","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_068","name":"Fahad Essa","amount":10000.0,"expected":10000.0,"date":"2026-05-24","dueDate":"","reason":"Salary","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_069","name":"FADHL ABDULLAH","amount":4000.0,"expected":4000.0,"date":"2026-05-26","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_070","name":"Ahmed Nagib","amount":3000.0,"expected":3000.0,"date":"2026-06-26","dueDate":"","reason":"","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_071","name":"Fahad Essa","amount":10000.0,"expected":10000.0,"date":"2026-06-26","dueDate":"","reason":"Salary","bank":"","iban":"","paid":0,"payments":[],"reminders":[],"attachments":[]},{"id":"ar_paid_000","name":"Sharifah","amount":5612.0,"expected":5612.0,"date":"","dueDate":"","reason":"Electricity","bank":"","iban":"","paid":5612.0,"payments":[{"amount":5612.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_001","name":"Rashed","amount":3104.0,"expected":3104.0,"date":"","dueDate":"","reason":"Du","bank":"","iban":"","paid":3104.0,"payments":[{"amount":3104.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_002","name":"Salah Alshamsi","amount":2000.0,"expected":2000.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":2000.0,"payments":[{"amount":2000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_003","name":"Menteqa Hamdan Hamriyah","amount":2453.0,"expected":2453.0,"date":"","dueDate":"","reason":"Maintenance","bank":"","iban":"","paid":2453.0,"payments":[{"amount":2453.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_004","name":"Naif","amount":9750.0,"expected":9750.0,"date":"","dueDate":"","reason":"DEWA","bank":"","iban":"","paid":9750.0,"payments":[{"amount":9750.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_005","name":"Mubarak","amount":211.0,"expected":211.0,"date":"2016-03-27","dueDate":"","reason":"LG","bank":"","iban":"","paid":211.0,"payments":[{"amount":211.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_006","name":"Obaid","amount":2700.0,"expected":2700.0,"date":"","dueDate":"","reason":"Du","bank":"","iban":"","paid":2700.0,"payments":[{"amount":2700.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_007","name":"Mayed Sultan","amount":500.0,"expected":500.0,"date":"2019-01-25","dueDate":"","reason":"","bank":"","iban":"","paid":500.0,"payments":[{"amount":500.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_008","name":"Mohammad Ali","amount":2000.0,"expected":2000.0,"date":"2019-01-25","dueDate":"","reason":"","bank":"","iban":"","paid":2000.0,"payments":[{"amount":2000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_009","name":"Suhail","amount":869.0,"expected":869.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":869.0,"payments":[{"amount":869.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_010","name":"Joy","amount":700.0,"expected":700.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":700.0,"payments":[{"amount":700.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_011","name":"Mamatha","amount":170.0,"expected":170.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":170.0,"payments":[{"amount":170.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_012","name":"Mubarak","amount":168.0,"expected":168.0,"date":"2020-07-12","dueDate":"","reason":"","bank":"","iban":"","paid":168.0,"payments":[{"amount":168.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_013","name":"Rafael Ken Aguilar","amount":350.0,"expected":350.0,"date":"2021-08-20","dueDate":"","reason":"$350 USD","bank":"","iban":"","paid":350.0,"payments":[{"amount":350.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_014","name":"Abdulrahman","amount":800.0,"expected":800.0,"date":"2021-11-21","dueDate":"","reason":"POCO","bank":"","iban":"","paid":800.0,"payments":[{"amount":800.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_015","name":"Abdulmalik","amount":300.0,"expected":300.0,"date":"2021-11-21","dueDate":"","reason":"POCO","bank":"","iban":"","paid":300.0,"payments":[{"amount":300.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_016","name":"Abdulmalik","amount":300.0,"expected":300.0,"date":"2022-04-09","dueDate":"","reason":"","bank":"","iban":"","paid":300.0,"payments":[{"amount":300.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_017","name":"Hassan","amount":300.0,"expected":300.0,"date":"2022-07-27","dueDate":"","reason":"","bank":"","iban":"","paid":300.0,"payments":[{"amount":300.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_018","name":"Sandia","amount":700.0,"expected":700.0,"date":"2022-09-26","dueDate":"","reason":"","bank":"","iban":"","paid":700.0,"payments":[{"amount":700.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_019","name":"Lashmih","amount":686.0,"expected":686.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":686.0,"payments":[{"amount":686.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_020","name":"Mareya","amount":918.0,"expected":918.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":918.0,"payments":[{"amount":918.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_021","name":"Menzel","amount":1117.0,"expected":1117.0,"date":"2020-04-17","dueDate":"","reason":"","bank":"","iban":"","paid":1117.0,"payments":[{"amount":1117.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_022","name":"Hassan","amount":395.0,"expected":395.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":395.0,"payments":[{"amount":395.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_023","name":"Abdulrahman","amount":338.0,"expected":338.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":338.0,"payments":[{"amount":338.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_024","name":"Mubarak","amount":288.0,"expected":288.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":288.0,"payments":[{"amount":288.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_025","name":"Abdulhamid","amount":10000.0,"expected":10000.0,"date":"","dueDate":"","reason":"building","bank":"","iban":"","paid":10000.0,"payments":[{"amount":10000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_026","name":"Eva","amount":500.0,"expected":500.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":500.0,"payments":[{"amount":500.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_027","name":"Mubarak","amount":470.0,"expected":470.0,"date":"2018-02-21","dueDate":"","reason":"","bank":"","iban":"","paid":470.0,"payments":[{"amount":470.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_028","name":"Mubarak","amount":250.0,"expected":250.0,"date":"2018-08-04","dueDate":"","reason":"","bank":"","iban":"","paid":250.0,"payments":[{"amount":250.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_029","name":"Omar Saleh","amount":50000.0,"expected":50000.0,"date":"2023-12-08","dueDate":"","reason":"","bank":"","iban":"","paid":50000.0,"payments":[{"amount":50000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_030","name":"Rashed Essa","amount":10000.0,"expected":10000.0,"date":"2023-12-14","dueDate":"","reason":"","bank":"","iban":"","paid":10000.0,"payments":[{"amount":10000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_031","name":"Howa","amount":600.0,"expected":600.0,"date":"2024-02-16","dueDate":"","reason":"","bank":"","iban":"","paid":600.0,"payments":[{"amount":600.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_032","name":"Eva","amount":100.0,"expected":100.0,"date":"2024-03-16","dueDate":"","reason":"","bank":"","iban":"","paid":100.0,"payments":[{"amount":100.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_033","name":"Muhi Udin","amount":1025.0,"expected":1025.0,"date":"2025-01-22","dueDate":"","reason":"","bank":"","iban":"","paid":1025.0,"payments":[{"amount":1025.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_034","name":"Muhi Udin","amount":1397.0,"expected":1397.0,"date":"2025-02-17","dueDate":"","reason":"","bank":"","iban":"","paid":1397.0,"payments":[{"amount":1397.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_035","name":"Shani","amount":897.0,"expected":897.0,"date":"2025-03-31","dueDate":"","reason":"","bank":"","iban":"","paid":897.0,"payments":[{"amount":897.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_036","name":"Abdulrahman","amount":400.0,"expected":400.0,"date":"2025-05-22","dueDate":"","reason":"","bank":"","iban":"","paid":400.0,"payments":[{"amount":400.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_037","name":"Amir","amount":3700.0,"expected":3700.0,"date":"2025-05-22","dueDate":"","reason":"","bank":"","iban":"","paid":3700.0,"payments":[{"amount":3700.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_038","name":"Shani","amount":1400.0,"expected":1400.0,"date":"2025-09-18","dueDate":"","reason":"","bank":"","iban":"","paid":1400.0,"payments":[{"amount":1400.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_039","name":"Amina Jagair","amount":581.0,"expected":581.0,"date":"2025-10-15","dueDate":"","reason":"","bank":"","iban":"","paid":581.0,"payments":[{"amount":581.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_040","name":"Mostafa","amount":1000.0,"expected":1000.0,"date":"2025-10-04","dueDate":"","reason":"","bank":"","iban":"","paid":1000.0,"payments":[{"amount":1000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_041","name":"Walid Ali","amount":2000.0,"expected":2000.0,"date":"2026-01-28","dueDate":"","reason":"","bank":"","iban":"","paid":2000.0,"payments":[{"amount":2000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_042","name":"Jhotti","amount":300.0,"expected":300.0,"date":"2026-02-26","dueDate":"","reason":"","bank":"","iban":"","paid":300.0,"payments":[{"amount":300.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_043","name":"FADHL ABDULLAH","amount":3000.0,"expected":3000.0,"date":"2026-03-31","dueDate":"","reason":"","bank":"","iban":"","paid":3000.0,"payments":[{"amount":3000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_044","name":"Rashed Essa","amount":1000.0,"expected":1000.0,"date":"2025-09-23","dueDate":"","reason":"","bank":"","iban":"","paid":1000.0,"payments":[{"amount":1000.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_045","name":"Amir","amount":2549.0,"expected":2549.0,"date":"2025-04-20","dueDate":"","reason":"","bank":"","iban":"","paid":2549.0,"payments":[{"amount":2549.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_046","name":"Jutti","amount":890.0,"expected":890.0,"date":"","dueDate":"","reason":"","bank":"","iban":"","paid":890.0,"payments":[{"amount":890.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_047","name":"Fahad","amount":6877.0,"expected":6877.0,"date":"2024-03-26","dueDate":"","reason":"Charger","bank":"","iban":"","paid":6877.0,"payments":[{"amount":6877.0,"ts":1000000000000}],"reminders":[],"attachments":[]},{"id":"ar_paid_048","name":"Ahmed Alwari","amount":6877.0,"expected":6877.0,"date":"2024-03-26","dueDate":"","reason":"Charger","bank":"","iban":"","paid":6877.0,"payments":[{"amount":6877.0,"ts":1000000000000}],"reminders":[],"attachments":[]}]; // ══════════════════════════════════════════════════════════════════════════ // BUMELHA MARTINGALE — A/R (Accounts Receivable) Module // Professional rewrite · All features preserved · Zero-error target // // External dependencies (provided by main dashboard): // fmt(n[,d]) — number formatter // showToast(msg) — toast notification // addNotification(...) — notification center (optional) // Chart — Chart.js global // // Public globals exposed for inline HTML handlers (unchanged names): // openArModal, closeArModal, saveArLoan, deleteArLoan, // openArPayModal, closeArPayModal, arFillFull, saveArPayment, // openArRemindModal, closeArRemindModal, saveArReminder, // openArLegalModal, closeArLegalModal, generateArLegal, printArLegal, copyArLegal, // exportArExcel, exportArPDF, setArFilter, renderArTab, // arForceReload, arClearAll, arAddAttachments, openArDetail // ══════════════════════════════════════════════════════════════════════════ (function (global) { 'use strict'; // ────────────────────────────────────────────────────────────────────── // CONSTANTS & STATE // ────────────────────────────────────────────────────────────────────── var STORAGE_KEY = 'mg_ar_loans'; var EMBEDDED = (typeof _AR_LEGACY !== 'undefined' && _AR_LEGACY.length) ? _AR_LEGACY : (typeof LEGACY_AR_LOANS !== 'undefined' && LEGACY_AR_LOANS.length) ? LEGACY_AR_LOANS : []; var state = { loans: [], filter: 'all', chart: null, pendingAttachments: [], legalText: '', legalMeta: null }; // Sender bank details used in legal documents (single source of truth) var SENDER = { name: 'محمد إبراهيم بوملحه', bank: 'بنك الإمارات الإسلامي', account: '3577269090902', iban: 'AE170340003577269090902', swift: 'MEBLAEAD' }; // ────────────────────────────────────────────────────────────────────── // SMALL HELPERS // ────────────────────────────────────────────────────────────────────── function $(id) { return document.getElementById(id); } function setText(id, v) { var e = $(id); if (e) e.textContent = v; } function setHTML(id, v) { var e = $(id); if (e) e.innerHTML = v; } function setVal(id, v) { var e = $(id); if (e) e.value = v; } function getVal(id) { var e = $(id); return e ? String(e.value).trim() : ''; } function esc(s) { return String(s == null ? '' : s).replace(/&/g,'&').replace(//g,'>'); } function toast(m) { if (typeof showToast === 'function') showToast(m); } function money(n, d) { return (typeof fmt === 'function') ? fmt(n, d) : Number(n || 0).toFixed(d == null ? 2 : d); } function today() { return new Date().toISOString().split('T')[0]; } function deepCopy(o) { return JSON.parse(JSON.stringify(o)); } function findLoan(id) { for (var i = 0; i < state.loans.length; i++) if (state.loans[i].id === id) return state.loans[i]; return null; } function lockScroll(on) { document.body.style.overflow = on ? 'hidden' : ''; } function showOverlay(id) { var o = $(id); if (o) { o.style.display = 'flex'; lockScroll(true); } } function hideOverlay(id) { var o = $(id); if (o) { o.style.display = 'none'; lockScroll(false); } } // Loan financial summary function summarize(l) { var amount = l.amount || 0; var paid = l.paid || 0; var remaining = amount - paid; var isPaid = remaining <= 0.01; var pct = amount > 0 ? Math.min(100, paid / amount * 100) : 0; var overdue = !isPaid && l.dueDate && new Date(l.dueDate) < new Date(); return { amount: amount, paid: paid, remaining: remaining < 0 ? 0 : remaining, isPaid: isPaid, pct: pct, overdue: overdue }; } // ────────────────────────────────────────────────────────────────────── // PERSISTENCE // ────────────────────────────────────────────────────────────────────── function load() { try { // If user explicitly cleared, keep it empty — don't re-seed from embedded data var wasCleared = localStorage.getItem('mg_ar_loans_cleared') === '1'; var stored = localStorage.getItem(STORAGE_KEY); if (stored) { var parsed = JSON.parse(stored); if (Array.isArray(parsed) && parsed.length) { state.loans = parsed; return; } } if (wasCleared) { state.loans = []; return; } // respect intentional clear } catch (e) { /* ignore corrupt storage */ } // Fall back to embedded data (WebView-safe: never depends on storage) state.loans = EMBEDDED.length ? deepCopy(EMBEDDED) : []; save(); } function save() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state.loans)); } catch (e) { /* storage disabled */ } } function ensureLoaded() { if (!state.loans || !state.loans.length) load(); } // ────────────────────────────────────────────────────────────────────── // REMINDERS — overdue notifications on load // ────────────────────────────────────────────────────────────────────── function checkReminders() { if (typeof addNotification !== 'function') return; var t = today(); state.loans.forEach(function (l) { var s = summarize(l); if (s.remaining <= 0.01 || !l.dueDate) return; if (l.dueDate <= t && !l._notified) { l._notified = true; addNotification('alert', 'Loan Due: ' + l.name, money(s.remaining) + ' AED expected back' + (l.reason ? ' (' + l.reason + ')' : '')); } }); save(); } // ────────────────────────────────────────────────────────────────────── // FILTERING // ────────────────────────────────────────────────────────────────────── function applyFilter(loans) { return loans.filter(function (l) { var s = summarize(l); switch (state.filter) { case 'paid': return s.isPaid; case 'active': return !s.isPaid && s.paid === 0; case 'partial': return !s.isPaid && s.paid > 0; case 'overdue': return s.overdue; default: return true; } }); } function setFilter(f, btn) { state.filter = f; var btns = document.querySelectorAll('[id^="arFilter-"]'); for (var i = 0; i < btns.length; i++) btns[i].classList.remove('active'); if (btn) btn.classList.add('active'); render(); } // ────────────────────────────────────────────────────────────────────── // RENDERING — summary cards, loan list, chart // ────────────────────────────────────────────────────────────────────── function renderSummary() { var totalLent = 0, totalPaid = 0; state.loans.forEach(function (l) { totalLent += (l.amount || 0); totalPaid += (l.paid || 0); }); var outstanding = totalLent - totalPaid; setText('arTotalLent', money(totalLent)); setText('arLoanCount', state.loans.length + ' loan' + (state.loans.length !== 1 ? 's' : '')); setText('arOutstanding', money(outstanding < 0 ? 0 : outstanding)); setText('arPaidInfo', money(totalPaid) + ' AED repaid'); } function dueLabel(l, s) { if (!l.dueDate || s.isPaid) return ''; var days = Math.ceil((new Date(l.dueDate + 'T00:00:00') - new Date()) / 86400000); if (days < 0) return 'Overdue ' + Math.abs(days) + 'd'; if (days === 0) return 'Due today'; return 'Due in ' + days + 'd'; } function loanCard(l) { var s = summarize(l); var atts = l.attachments || []; var rem = (l.reminders || []).length; var border = s.isPaid ? 'rgba(34,197,94,.3)' : 'var(--border)'; var due = dueLabel(l, s); var btns = (s.isPaid ? '' : '') + '' + '' + '' + '' + ''; var previews = atts.length ? '
' + atts.map(function (a, ai) { return attachThumb(a, l.id, ai); }).join('') + '
' : ''; return '
' + '
' + '
' + esc(l.name) + (s.isPaid ? ' PAID' : '') + '
' + '
' + esc(l.date || '') + (l.reason ? ' - ' + esc(l.reason) : '') + (rem ? ' (' + rem + ' reminders)' : '') + '
' + '
' + money(l.amount) + ' AED
' + '
' + '
' + '
' + '' + (s.isPaid ? 'Fully repaid' : money(s.paid) + '/' + money(s.amount) + ' AED') + '' + '' + s.pct.toFixed(0) + '%' + '
' + (due ? '
' + due + '
' : '') + '
' + btns + '
' + previews + '
'; } function renderList() { var list = $('arList'); if (!list) return; var filtered = applyFilter(state.loans); // Apply person filter from chart click if (state.personFilter) { filtered = filtered.filter(function (l) { return l.name === state.personFilter; }); } if (!filtered.length) { var msg = state.personFilter ? 'No loans found for ' + state.personFilter + '.' : 'No loans match this filter.'; list.innerHTML = '
' + msg + '
'; return; } var sorted = filtered.slice().sort(function (a, b) { return (b.date || '').localeCompare(a.date || ''); }); list.innerHTML = sorted.map(loanCard).join(''); list.onclick = function (e) { var btn = e.target.closest ? e.target.closest('[data-fn]') : null; var card = e.target.closest ? e.target.closest('.ar-card') : null; if (btn) { e.stopPropagation(); dispatch(btn); } else if (card) { openDetail(card.getAttribute('data-ar-id'), e); } }; } // Track person filter (null = show all) state.personFilter = null; function renderChart() { var canvas = $('arChart'); if (!canvas || typeof Chart === 'undefined') return; if (state.chart && typeof state.chart.destroy === 'function') state.chart.destroy(); var byPerson = {}; state.loans.forEach(function (l) { var s = summarize(l); if (s.remaining > 0.01) byPerson[l.name] = (byPerson[l.name] || 0) + s.remaining; }); var names = Object.keys(byPerson).sort(function (a, b) { return byPerson[b] - byPerson[a]; }).slice(0, 10); var c2d = canvas.getContext('2d'); if (!names.length) { c2d.clearRect(0, 0, canvas.width, canvas.height); c2d.fillStyle = '#64748b'; c2d.font = '13px Inter'; c2d.textAlign = 'center'; c2d.fillText('No outstanding loans', canvas.width / 2, canvas.height / 2); return; } // Highlight selected bar var bgColors = names.map(function (n) { return (state.personFilter && n === state.personFilter) ? '#f59e0b' : 'rgba(245,158,11,.5)'; }); var borderColors = names.map(function (n) { return (state.personFilter && n === state.personFilter) ? '#fff' : '#f59e0b'; }); state.chart = new Chart(c2d, { type: 'bar', data: { labels: names, datasets: [{ label: 'Outstanding (AED)', data: names.map(function (n) { return byPerson[n]; }), backgroundColor: bgColors, borderColor: borderColors, borderWidth: 2, borderRadius: 6 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { callbacks: { label: function (ctx) { return ' ' + money(ctx.raw) + ' AED outstanding'; } } } }, scales: { x: { ticks: { color: '#94a3b8', font: { size: 11 } }, grid: { display: false } }, y: { ticks: { color: '#64748b', font: { size: 10 }, callback: function (v) { return money(v, 0); } }, grid: { color: '#222b3a' } } }, onClick: function (evt, elements) { if (elements && elements.length) { var idx = elements[0].index; var name = names[idx]; if (state.personFilter === name) { // Tap same person again → clear filter state.personFilter = null; toast('Showing all loans'); } else { state.personFilter = name; toast('Filtered: ' + name); } renderList(); renderChart(); updatePersonFilterBadge(); } } } }); } function updatePersonFilterBadge() { var badge = $('arPersonFilterBadge'); if (!badge) { badge = document.createElement('div'); badge.id = 'arPersonFilterBadge'; badge.style.cssText = 'display:none;align-items:center;gap:8px;padding:6px 12px;background:rgba(245,158,11,.12);border:1px solid rgba(245,158,11,.4);border-radius:20px;font-size:12px;color:var(--amber);font-weight:600;margin-bottom:8px;'; badge.innerHTML = ''; var list = $('arList'); if (list && list.parentNode) list.parentNode.insertBefore(badge, list); } if (state.personFilter) { badge.style.display = 'flex'; var lbl = $('arPersonFilterLabel'); if (lbl) lbl.textContent = '👤 ' + state.personFilter; } else { badge.style.display = 'none'; } } function render() { ensureLoaded(); renderSummary(); renderList(); renderChart(); updatePersonFilterBadge(); } // ────────────────────────────────────────────────────────────────────── // DETAIL VIEW // ────────────────────────────────────────────────────────────────────── function openDetail(id, event) { if (event) event.stopPropagation(); var l = findLoan(id); if (!l) return; var s = summarize(l); var overlay = $('arDetailOverlay'); if (!overlay) { overlay = document.createElement('div'); overlay.className = 'confirm-overlay'; overlay.id = 'arDetailOverlay'; overlay.style.cssText = 'display:none;align-items:flex-start;overflow-y:auto;'; overlay.onclick = function (e) { if (e.target === overlay) hideOverlay('arDetailOverlay'); }; document.body.appendChild(overlay); } var payments = (l.payments && l.payments.length) ? '
Payments (' + l.payments.length + ')
' + l.payments.slice().reverse().map(function (p) { return '
' + new Date(p.ts).toLocaleDateString('en-GB') + '+' + money(p.amount) + ' AED
'; }).join('') + '
' : ''; var reminders = (l.reminders && l.reminders.length) ? '
Reminders (' + l.reminders.length + ')
' + l.reminders.slice().reverse().map(function (r) { return '
' + esc(r.date) + '
' + (r.note ? '
' + esc(r.note) + '
' : '') + '
'; }).join('') + '
' : '
No reminders yet
'; function statBox(label, val, color) { return '
' + label + '
' + val + '
AED
'; } overlay.innerHTML = '
' + '
' + '
' + esc(l.name) + '
' + '' + '
' + '
' + statBox('LENT', money(l.amount), 'var(--amber)') + statBox('PAID', money(s.paid), 'var(--green)') + statBox('LEFT', money(s.remaining), s.remaining <= 0 ? 'var(--green)' : 'var(--red)') + '
' + '
' + '
' + s.pct.toFixed(0) + '% repaid  |  Date: ' + esc(l.date || '—') + '
' + payments + reminders + '
' + (s.remaining > 0.01 ? '' : '') + '' + '
' + '
'; showOverlay('arDetailOverlay'); var pid = l.id; $('arDetClose').onclick = function () { hideOverlay('arDetailOverlay'); }; var payBtn = $('arDetPay'); if (payBtn) payBtn.onclick = function () { hideOverlay('arDetailOverlay'); openPayModal(pid); }; var remBtn = $('arDetRemind'); if (remBtn) remBtn.onclick = function () { hideOverlay('arDetailOverlay'); openRemindModal(pid); }; } // ────────────────────────────────────────────────────────────────────── // ADD / EDIT LOAN // ────────────────────────────────────────────────────────────────────── function openModal(editId) { setVal('arEditId', editId || ''); setText('arModalTitle', editId ? 'Edit Loan' : 'Add Loan'); if (editId) { var l = findLoan(editId); if (l) { setVal('arName', l.name || ''); setVal('arAmount', l.amount || ''); setVal('arDate', l.date || ''); setVal('arDueDate', l.dueDate || ''); setVal('arReason', l.reason || ''); setVal('arBank', l.bank || ''); setVal('arIban', l.iban || ''); state.pendingAttachments = (l.attachments || []).map(function (a) { return { name: a.name, data: a.data, type: a.type }; }); } } else { ['arName', 'arReason', 'arBank', 'arIban', 'arAmount', 'arDueDate'].forEach(function (id) { setVal(id, ''); }); setVal('arDate', today()); state.pendingAttachments = []; } renderAttachList(); showOverlay('arModalOverlay'); } function closeModal() { hideOverlay('arModalOverlay'); } function saveLoan() { try { ensureLoaded(); var name = getVal('arName'); var amount = parseFloat(getVal('arAmount')) || 0; if (!name) { toast("Enter the person's name"); return; } if (amount <= 0) { toast('Enter a valid amount'); return; } var editId = getVal('arEditId'); var attachments = (state.pendingAttachments || []).slice(); var fields = { name: name, amount: amount, date: getVal('arDate'), dueDate: getVal('arDueDate'), reason: getVal('arReason'), bank: getVal('arBank'), iban: getVal('arIban'), attachments: attachments }; if (editId) { var l = findLoan(editId); if (l) for (var k in fields) if (fields.hasOwnProperty(k)) l[k] = fields[k]; } else { state.loans.push({ id: 'ar_' + Date.now(), name: name, amount: amount, expected: amount, date: fields.date, dueDate: fields.dueDate, reason: fields.reason, bank: fields.bank, iban: fields.iban, paid: 0, payments: [], reminders: [], attachments: attachments }); } save(); closeModal(); render(); toast('Loan saved'); } catch (err) { toast('Error: ' + (err && err.message ? err.message : 'unknown')); } } function deleteLoan(id) { state.loans = state.loans.filter(function (x) { return x.id !== id; }); save(); render(); toast('Deleted'); } // ────────────────────────────────────────────────────────────────────── // PAYMENTS // ────────────────────────────────────────────────────────────────────── function openPayModal(id) { var l = findLoan(id); if (!l) return; var s = summarize(l); setVal('arPayId', id); setText('arPayTitle', 'Payment from ' + l.name); setText('arPaySub', 'Remaining: AED ' + money(s.remaining) + ' of ' + money(l.amount)); setVal('arPayAmount', ''); showOverlay('arPayOverlay'); } function closePayModal() { hideOverlay('arPayOverlay'); } function fillFull() { var l = findLoan(getVal('arPayId')); if (!l) return; setVal('arPayAmount', summarize(l).remaining.toFixed(2)); } function savePayment() { var l = findLoan(getVal('arPayId')); if (!l) return; var amt = parseFloat(getVal('arPayAmount')) || 0; if (amt <= 0) { toast('Enter a valid amount'); return; } l.paid = (l.paid || 0) + amt; l.payments = l.payments || []; l.payments.push({ amount: amt, ts: Date.now() }); save(); closePayModal(); render(); var rem = (l.amount || 0) - l.paid; toast(rem <= 0.01 ? 'Fully repaid!' : 'Payment recorded - ' + money(rem) + ' AED left'); } // ────────────────────────────────────────────────────────────────────── // REMINDER LOG // ────────────────────────────────────────────────────────────────────── function buildRemindOverlay() { var overlay = document.createElement('div'); overlay.className = 'confirm-overlay'; overlay.id = 'arRemindOverlay'; overlay.style.display = 'none'; overlay.onclick = function (e) { if (e.target === overlay) closeRemindModal(); }; overlay.innerHTML = '
' + '
' + '
Log Reminder
' + '' + '
' + '' + '
Date I Reminded Him
' + '' + '
What He Said / Note
' + '' + '
' + '' + '' + '
' + '
'; document.body.appendChild(overlay); return overlay; } function openRemindModal(id) { var l = findLoan(id); if (!l) return; if (!$('arRemindOverlay')) buildRemindOverlay(); setVal('arRemindId', id); setText('arRemindTitle', 'Reminder - ' + l.name); setVal('arRemindDate', today()); setVal('arRemindNote', ''); showOverlay('arRemindOverlay'); } function closeRemindModal() { hideOverlay('arRemindOverlay'); } function saveReminder() { var l = findLoan(getVal('arRemindId')); if (!l) return; var date = getVal('arRemindDate'); var note = getVal('arRemindNote'); if (!date) { toast('Select a date'); return; } l.reminders = l.reminders || []; l.reminders.push({ date: date, note: note, ts: Date.now() }); save(); closeRemindModal(); render(); toast('Reminder logged for ' + l.name); } // ────────────────────────────────────────────────────────────────────── // EXPORTS — Excel (CSV) & PDF (print window) // ────────────────────────────────────────────────────────────────────── function exportExcel() { var rows = [['Name', 'Amount (AED)', 'Paid (AED)', 'Remaining (AED)', '% Paid', 'Date', 'Due Date', 'Reason', 'Status']]; state.loans.forEach(function (l) { var s = summarize(l); var pct = l.amount > 0 ? (s.paid / l.amount * 100).toFixed(0) + '%' : '0%'; var status = s.isPaid ? 'PAID' : (s.overdue ? 'OVERDUE' : 'ACTIVE'); rows.push([l.name, l.amount, s.paid, s.remaining, pct, l.date || '', l.dueDate || '', l.reason || '', status]); }); var csv = rows.map(function (r) { return r.map(function (v) { return '"' + String(v).replace(/"/g, '""') + '"'; }).join(','); }).join('\n'); var a = document.createElement('a'); a.href = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv); a.download = 'AR_Loans_' + today() + '.csv'; a.click(); toast('Excel (CSV) exported'); } function exportPDF() { var totalLent = 0, totalPaid = 0; state.loans.forEach(function (l) { totalLent += (l.amount || 0); totalPaid += (l.paid || 0); }); var body = state.loans.map(function (l, i) { var s = summarize(l); var status = s.isPaid ? '' : (s.overdue ? 'OVERDUE' : 'Active'); return '' + (i + 1) + '' + esc(l.name) + '' + (l.amount || 0).toLocaleString() + '' + s.paid.toLocaleString() + '' + s.remaining.toLocaleString() + '' + esc(l.date || '-') + '' + status + '' + esc(l.reason || '-') + ''; }).join(''); var html = 'A/R Report' + '' + '

Money Lent - A/R Report

' + '

Generated: ' + new Date().toLocaleString() + ' | Total Lent: ' + totalLent.toLocaleString() + ' AED | Repaid: ' + totalPaid.toLocaleString() + ' AED

' + '' + body + '
#NameLentPaidRemainingDateStatusReason
'; var w = window.open('', '_blank'); if (w) { w.document.write(html); w.document.close(); setTimeout(function () { w.print(); }, 500); } else toast('Allow popups to print'); } // ────────────────────────────────────────────────────────────────────── // ARABIC NUMBER → WORDS (for legal documents) // ────────────────────────────────────────────────────────────────────── function numberToWords(n) { var ones = ['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة', 'عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر', 'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر']; var tens = ['', '', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون']; var hundreds = ['', 'مئة', 'مئتان', 'ثلاثمئة', 'أربعمئة', 'خمسمئة', 'ستمئة', 'سبعمئة', 'ثمانمئة', 'تسعمئة']; if (n === 0) return 'صفر'; var parts = []; if (n >= 1000000) { parts.push(numberToWords(Math.floor(n / 1000000)) + ' مليون'); n %= 1000000; } if (n >= 1000) { var th = Math.floor(n / 1000); if (th === 1) parts.push('ألف'); else if (th === 2) parts.push('ألفان'); else if (th < 11) parts.push(ones[th] + ' آلاف'); else parts.push(numberToWords(th) + ' ألف'); n %= 1000; } if (n >= 100) { parts.push(hundreds[Math.floor(n / 100)]); n %= 100; } if (n > 0) { if (n < 20) parts.push(ones[n]); else { var t = Math.floor(n / 10), o = n % 10; if (o > 0) parts.push(ones[o] + ' و' + tens[t]); else parts.push(tens[t]); } } return parts.join(' و'); } // ────────────────────────────────────────────────────────────────────── // LEGAL DOCUMENT // ────────────────────────────────────────────────────────────────────── function buildLegalOverlay() { var overlay = document.createElement('div'); overlay.className = 'confirm-overlay'; overlay.id = 'arLegalOverlay'; overlay.style.display = 'none'; overlay.style.alignItems = 'flex-start'; overlay.style.overflowY = 'auto'; overlay.onclick = function (e) { if (e.target === overlay) closeLegalModal(); }; overlay.innerHTML = '
' + '
' + '
Legal Document
' + '' + '
' + '' + '
Transfer Reference (رقم الحوالة)
' + '' + '' + '' + '' + '
'; document.body.appendChild(overlay); return overlay; } function openLegalModal(id) { var l = findLoan(id); if (!l) { toast('Not found'); return; } if (!$('arLegalOverlay')) buildLegalOverlay(); setVal('arLegalId', id); setVal('arLegalRef', l.legalRef || ''); var pv = $('arLegalPreview'); if (pv) pv.style.display = 'none'; var ac = $('arLegalActions'); if (ac) ac.style.display = 'none'; showOverlay('arLegalOverlay'); } function closeLegalModal() { hideOverlay('arLegalOverlay'); } function fmtDate(iso) { if (!iso) return '......'; var p = iso.split('-'); return p[2] + '/' + p[1] + '/' + p[0]; } function generateLegal() { var l = findLoan(getVal('arLegalId')); if (!l) return; var ref = getVal('arLegalRef'); if (!ref) { toast('Enter the transfer reference number'); return; } l.legalRef = ref; save(); var amt = Math.round(l.amount); var words = numberToWords(amt); var dateStr = fmtDate(l.date || today()); var dueStr = fmtDate(l.dueDate); var bank = l.bank || '[اسم البنك]'; var iban = l.iban || '[رقم الحساب]'; var text = 'أُقر أنا ' + SENDER.name + ' بأنني قد قمت بتحويل مبلغ وقدره ' + amt.toLocaleString('ar-AE') + ' درهم إماراتي (' + words + ' درهم إماراتي فقط لا غير) بتاريخ ' + dateStr + ' إلى حساب السيد/ ' + l.name + (l.bank ? ' لدى ' + bank : '') + (l.iban ? '، رقم الحساب ' + iban : '') + '.\n\nويُعتبر هذا المبلغ سلفة شخصية، على أن تتم تسويته وسداده بتاريخ ' + dueStr + '، وذلك بموجب التفاهم القائم بين الطرفين.' + (l.reason ? '\n\nالغرض: ' + l.reason : '') + '\n\nالمرسل:\n' + SENDER.name + (ref ? '\nالمرجع: ' + ref : '') + '\n\nبياناتي البنكية:\n' + SENDER.bank + '\n' + SENDER.name + '\nرقم الحساب: ' + SENDER.account + '\nIBAN: ' + SENDER.iban + '\nSwift Code: ' + SENDER.swift; var pv = $('arLegalPreview'); if (pv) { pv.style.display = 'block'; pv.textContent = text; } var ac = $('arLegalActions'); if (ac) ac.style.display = 'flex'; state.legalText = text; state.legalMeta = { l: l, ref: ref, amt: amt, words: words, dateStr: dateStr, dueStr: dueStr, bank: bank, iban: iban }; } function printLegal() { var d = state.legalMeta; if (!d) return; var html = 'Legal' + '' + '

إقرار بتحويل مبلغ مالي

' + '

أُقر أنا ' + SENDER.name + ' بأنني قد قمت بتحويل مبلغ وقدره ' + d.amt.toLocaleString('ar-AE') + ' درهم إماراتي (' + d.words + ' درهم إماراتي فقط لا غير) بتاريخ ' + d.dateStr + ' إلى حساب السيد/ ' + esc(d.l.name) + '' + (d.l.bank ? ' لدى ' + esc(d.bank) + '' : '') + (d.l.iban ? '، رقم الحساب ' + esc(d.iban) + '' : '') + '.

' + '

ويُعتبر هذا المبلغ سلفة شخصية، على أن تتم تسويته وسداده بتاريخ ' + d.dueStr + '، وذلك بموجب التفاهم القائم بين الطرفين.

' + (d.l.reason ? '

الغرض: ' + esc(d.l.reason) + '

' : '') + '
البيانات البنكية للمُحيل
' + '
' + SENDER.bank + '
' + SENDER.name + '
رقم الحساب: ' + SENDER.account + '
IBAN: ' + SENDER.iban + '
Swift Code: ' + SENDER.swift + '
' + '
التوقيع:
 
التاريخ: ' + d.dateStr + '
المرسل: ' + SENDER.name + '
' + (d.ref ? '
REF: ' + esc(d.ref) + '
' : '') + ''; var w = window.open('', '_blank'); if (w) { w.document.write(html); w.document.close(); setTimeout(function () { w.print(); }, 500); } else toast('Allow popups to print'); } function copyLegal() { var t = state.legalText || ''; if (navigator.clipboard) { navigator.clipboard.writeText(t).then(function () { toast('Copied'); }); } else { var ta = document.createElement('textarea'); ta.value = t; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); } catch (e) {} document.body.removeChild(ta); toast('Copied'); } } // ────────────────────────────────────────────────────────────────────── // ATTACHMENTS // ────────────────────────────────────────────────────────────────────── function isImage(a) { return a.type && a.type.indexOf('image/') === 0; } function fileExt(name) { return String(name).split('.').pop().toUpperCase().slice(0, 4); } function attachThumb(a, loanId, ai) { var thumb = isImage(a) ? '' : '
' + fileExt(a.name) + '
'; return '
' + thumb + '
' + '
'; } function renderAttachList() { var list = $('arAttachList'); if (!list) return; var atts = state.pendingAttachments || []; if (!atts.length) { list.innerHTML = ''; return; } list.innerHTML = atts.map(function (a, i) { var preview = isImage(a) ? '' : '
' + fileExt(a.name) + '
'; return '
' + preview + '
' + esc(a.name) + '
' + '' + '
'; }).join(''); // Wire up "remove pending" buttons list.onclick = function (e) { var btn = e.target.closest ? e.target.closest('[data-fn="rmpending"]') : null; if (btn) { var ai = parseInt(btn.getAttribute('data-ai'), 10); state.pendingAttachments.splice(ai, 1); renderAttachList(); } }; } function addAttachments(files) { if (!files || !files.length) return; var pending = state.pendingAttachments || []; Array.prototype.slice.call(files).forEach(function (file) { var reader = new FileReader(); reader.onload = function (e) { pending.push({ name: file.name, data: e.target.result, type: file.type, size: file.size }); renderAttachList(); }; reader.readAsDataURL(file); }); state.pendingAttachments = pending; var input = $('arAttachInput'); if (input) input.value = ''; } function deleteAttach(loanId, ai) { var l = findLoan(loanId); if (!l || !l.attachments) return; if (!confirm('Delete this attachment?')) return; l.attachments.splice(ai, 1); save(); render(); } function viewAttach(loanId, ai) { var l = findLoan(loanId); if (!l || !l.attachments || !l.attachments[ai]) return; var a = l.attachments[ai]; var overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.85);z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:20px;'; overlay.onclick = function (e) { if (e.target === overlay) document.body.removeChild(overlay); }; var closeWrap = document.createElement('div'); closeWrap.style.cssText = 'position:absolute;top:16px;right:16px;'; var closeBtn = document.createElement('button'); closeBtn.textContent = '\u2715'; closeBtn.style.cssText = 'background:rgba(255,255,255,.15);border:none;color:#fff;font-size:20px;width:36px;height:36px;border-radius:50%;cursor:pointer;'; closeBtn.onclick = function () { document.body.removeChild(overlay); }; closeWrap.appendChild(closeBtn); overlay.appendChild(closeWrap); var content = document.createElement('div'); if (isImage(a)) { var img = document.createElement('img'); img.src = a.data; img.style.cssText = 'max-width:100%;max-height:80vh;border-radius:10px;object-fit:contain;'; content.appendChild(img); } else { content.innerHTML = '
\uD83D\uDCC4
' + esc(a.name) + '
'; var dl = document.createElement('a'); dl.href = a.data; dl.download = a.name; dl.textContent = '\u2B07 Download'; dl.style.cssText = 'padding:10px 20px;background:var(--amber);color:#000;border-radius:8px;font-weight:700;text-decoration:none;display:inline-block;margin-top:10px;'; content.appendChild(dl); } overlay.appendChild(content); var label = document.createElement('div'); label.textContent = a.name; label.style.cssText = 'margin-top:12px;font-size:13px;color:rgba(255,255,255,.7);'; overlay.appendChild(label); if (isImage(a)) { var dlLink = document.createElement('a'); dlLink.href = a.data; dlLink.download = a.name; dlLink.textContent = '\u2B07 Download'; dlLink.style.cssText = 'margin-top:8px;padding:8px 16px;background:rgba(255,255,255,.15);color:#fff;border-radius:8px;font-size:13px;text-decoration:none;font-weight:600;'; overlay.appendChild(dlLink); } document.body.appendChild(overlay); } // ────────────────────────────────────────────────────────────────────── // MAINTENANCE — reload / clear // ────────────────────────────────────────────────────────────────────── function forceReload() { try { localStorage.removeItem(STORAGE_KEY); } catch (e) {} try { localStorage.removeItem('mg_ar_loans_cleared'); } catch (e) {} // allow re-seeding state.loans = EMBEDDED.length ? deepCopy(EMBEDDED) : []; save(); render(); toast('Loaded ' + state.loans.length + ' loans'); } function clearAll() { if (!state.loans || !state.loans.length) { toast('Already empty'); return; } if (!confirm('Delete ALL ' + state.loans.length + ' loans? This cannot be undone.')) return; state.loans = []; // Write sentinel so the early IIFE doesn't re-seed on next load try { localStorage.setItem('mg_ar_loans_cleared', '1'); } catch(e) {} save(); render(); toast('All loans cleared'); } // ────────────────────────────────────────────────────────────────────── // CARD-BUTTON DISPATCHER // ────────────────────────────────────────────────────────────────────── function dispatch(btn) { var fn = btn.getAttribute('data-fn'); var id = btn.getAttribute('data-id'); var ai = btn.getAttribute('data-ai'); switch (fn) { case 'pay': openPayModal(id); break; case 'edit': openModal(id); break; case 'del': deleteLoan(id); break; case 'remind': openRemindModal(id); break; case 'legal': openLegalModal(id); break; case 'attach': openModal(id); break; case 'viewattach': viewAttach(id, parseInt(ai, 10)); break; case 'delattach': deleteAttach(id, parseInt(ai, 10)); break; } } // ────────────────────────────────────────────────────────────────────── // BOOTSTRAP — load data immediately (WebView-safe) // ────────────────────────────────────────────────────────────────────── load(); // ────────────────────────────────────────────────────────────────────── // PUBLIC API — expose globals for inline HTML onclick handlers // ────────────────────────────────────────────────────────────────────── global.openArModal = openModal; global.closeArModal = closeModal; global.saveArLoan = saveLoan; global.deleteArLoan = deleteLoan; global.openArPayModal = openPayModal; global.closeArPayModal = closePayModal; global.arFillFull = fillFull; global.saveArPayment = savePayment; global.openArRemindModal = openRemindModal; global.closeArRemindModal = closeRemindModal; global.saveArReminder = saveReminder; global.openArLegalModal = openLegalModal; global.closeArLegalModal = closeLegalModal; global.generateArLegal = generateLegal; global.printArLegal = printLegal; global.copyArLegal = copyLegal; global.exportArExcel = exportExcel; global.exportArPDF = exportPDF; global.setArFilter = setFilter; global.arClearPersonFilter = function () { state.personFilter = null; render(); }; global.renderArTab = render; global.openArDetail = openDetail; global.arForceReload = forceReload; global.arClearAll = clearAll; global.arAddAttachments = addAttachments; global.checkArReminders = checkReminders; // Internal helpers some inline code may still reference global._arAct = dispatch; // Backwards-compat: keep arLoans accessible if other modules read it Object.defineProperty(global, 'arLoans', { get: function () { return state.loans; }, set: function (v) { if (Array.isArray(v)) state.loans = v; }, configurable: true }); })(typeof window !== 'undefined' ? window : this); // ════ CAPITAL MANAGEMENT ════ // ════ INJECTED MISSING FUNCTIONS ════ function exportSinglePDF(idx){ var t = tradeHistory[idx]; if(!t) return; var trades = [t]; triggerPrint(buildPDFHTML(trades, 'Trade Detail — '+t.pair)); } // ════ KUCOIN DASHBOARD + TRADE DETAIL ════ kcPeriodDays = 7; kcProfitChart, kcAllocChart, kcWinRateChart; kcPhTab = 'total'; kcProfitChart, kcAllocChart, kcWinRateChart; kcPhTab = 'total'; kcPhTab = 'total'; function setPhTab(tab, btn){ kcPhTab = tab; // Reset all tab buttons var allTabs = document.querySelectorAll('[id^="ph-tab-"]'); allTabs.forEach(function(b){ b.style.borderBottomColor = 'transparent'; b.style.color = 'var(--muted)'; b.style.fontWeight = '600'; }); // Activate clicked tab if(btn){ btn.style.borderBottomColor = 'var(--accent)'; btn.style.color = 'var(--text)'; btn.style.fontWeight = '700'; } // Show/hide panes var pTotal = document.getElementById('ph-pane-total'); var pHistory = document.getElementById('ph-pane-history'); if(pTotal) pTotal.style.display = (tab === 'total') ? 'block' : 'none'; if(pHistory) pHistory.style.display = (tab === 'history') ? 'block' : 'none'; // Render data for total profits pane if(tab === 'total') renderKcDashboard(); } function setKcPeriod(days, btn){ kcPeriodDays = days; document.querySelectorAll('[id^="kcp-"]').forEach(function(b){ b.style.borderBottomColor='transparent'; b.style.color='var(--muted)'; b.style.fontWeight='500'; }); btn.style.borderBottomColor='var(--accent)'; btn.style.color='var(--text)'; btn.style.fontWeight='700'; renderKcDashboard(); } function renderKcDashboard(){ var sorted = (tradeHistory||[]).slice().sort(function(a,b){return a.ts-b.ts;}); var now = Date.now(); var cutoff = now - kcPeriodDays*86400000; var todayCut = new Date(); todayCut.setHours(0,0,0,0); // Today's profit var todayTrades = sorted.filter(function(t){return t.ts >= todayCut.getTime();}); var todayPnl = todayTrades.reduce(function(s,t){return s+(t.pnl||0);},0); var el = document.getElementById('kc-today-profit'); if(el){ el.textContent=(todayPnl>=0?'+':'')+fmt(todayPnl,2); el.style.color=todayPnl>=0?'var(--green)':'var(--red)'; } // Operating Assets = total currently invested (from current strategy) var operating = currentOrders.reduce(function(s,o){return s+(o.activated?o.execAmount:0);},0); var opEl = document.getElementById('kc-operating'); if(opEl) opEl.textContent = fmt(operating,2); // Unrealized PNL = live P&L on open orders var unrealized = 0; if(currentOrders.length>0){ var lastActive = currentOrders.filter(function(o){return o.activated;}); if(lastActive.length>0){ var la = lastActive[lastActive.length-1]; var livePrice = parseFloat(document.getElementById('entryPriceMin')&&document.getElementById('entryPriceMin').value||0); if(livePrice>0) unrealized = (livePrice - la.avgPrice)*la.execQty; } } var unrEl = document.getElementById('kc-unrealized'); if(unrEl){ unrEl.textContent=(unrealized>=0?'+':'')+fmt(unrealized,2); unrEl.style.color=unrealized>=0?'var(--green)':'var(--red)'; } // Profit History (all closed trades total) var profitHistory = sorted.reduce(function(s,t){return s+(t.pnl||0);},0); var phEl = document.getElementById('kc-profit-history'); if(phEl){ phEl.textContent=(profitHistory>=0?'+':'')+fmt(profitHistory,2); phEl.style.color=profitHistory>=0?'var(--green)':'var(--red)'; } // Total Profits = Profit History + Unrealized var totalProfits = profitHistory + unrealized; var tpEl = document.getElementById('kc-total-profits'); if(tpEl){ tpEl.textContent=(totalProfits>=0?'+':'')+fmt(totalProfits,2); tpEl.style.color=totalProfits>=0?'var(--green)':'var(--red)'; } // Period filter for chart var periodTrades = sorted.filter(function(t){return t.ts>=cutoff;}); // Build daily P&L data for chart var dayMap = {}; periodTrades.forEach(function(t){ var d = new Date(t.ts).toISOString().split('T')[0]; dayMap[d] = (dayMap[d]||0) + (t.pnl||0); }); // Fill in all days in range var labels=[], vals=[], cumPnl=0, runningPnl=[]; for(var d=0; d=0?'+':'')+fmt(periodPnl,2)+' USDT'; cpEl.style.color=periodPnl>=0?'var(--green)':'var(--red)'; } // Profit line chart var ctx = document.getElementById('kcProfitChart'); if(ctx){ if(kcProfitChart) kcProfitChart.destroy(); var lineColor = periodPnl>=0?'#2DD4BF':'#f85149'; kcProfitChart = new Chart(ctx.getContext('2d'), { type:'line', data:{ labels: labels, datasets:[{ label:'Total Profits', data: runningPnl, borderColor: lineColor, backgroundColor: 'transparent', tension: 0.4, borderWidth: 2.5, pointRadius: 4, pointBackgroundColor: lineColor, fill: false, }] }, options:{ responsive:true, maintainAspectRatio:false, plugins:{legend:{display:false},tooltip:{callbacks:{label:function(c){return(c.raw>=0?'+':'')+fmt(c.raw,2)+' USDT';}}}}, scales:{ x:{ticks:{color:'#64748b',font:{size:10},maxTicksLimit:5},grid:{color:'rgba(255,255,255,.04)'}}, y:{ticks:{color:'#64748b',font:{size:10},callback:function(v){return fmt(v,0);}},grid:{color:'rgba(255,255,255,.04)'}} } } }); } // ── Asset Allocation Doughnut ── // Closed trades return their proceeds (invested + P&L) to USDT. // Only currently-open (unclosed) strategy positions count as coin holdings. var pairMap = {}; periodTrades.forEach(function(t){ var proceeds = (t.invested||0) + (t.pnl||0); pairMap['USDT'] = (pairMap['USDT']||0) + proceeds; }); (strategies||[]).forEach(function(s){ var orders = s.orders || []; var filled = orders.filter(function(o){ return o.activated; }); if(filled.length){ var p = s.base || 'Unknown'; pairMap[p] = (pairMap[p]||0) + filled[filled.length-1].execAmount; } }); var pairs = Object.keys(pairMap).sort(function(a,b){return pairMap[b]-pairMap[a];}).slice(0,6); var total = pairs.reduce(function(s,p){return s+pairMap[p];},0); var COLORS = ['#2DD4BF','#3b82f6','#a78bfa','#22c55e','#67e8f9','#f59e0b']; var allocCtx = document.getElementById('kcAllocChart'); if(allocCtx){ if(kcAllocChart) kcAllocChart.destroy(); if(pairs.length>0){ kcAllocChart = new Chart(allocCtx.getContext('2d'),{ type:'doughnut', data:{ labels: pairs, datasets:[{data:pairs.map(function(p){return pairMap[p];}),backgroundColor:COLORS,borderWidth:0,hoverOffset:4}] }, options:{responsive:true,maintainAspectRatio:false,cutout:'65%',plugins:{legend:{display:false},tooltip:{callbacks:{label:function(c){return c.label+': '+(c.parsed/total*100).toFixed(1)+'%';}}}}} }); var centerEl = document.getElementById('kcAllocCenter'); if(centerEl) centerEl.textContent = pairs.length+' pairs'; } var legEl = document.getElementById('kcAllocLegend'); if(legEl){ legEl.innerHTML = pairs.map(function(p,i){ return '
' + '
' + '
'+p+'
' + '
' + ''+(total>0?(pairMap[p]/total*100).toFixed(2):'0')+'%' + '' + '
'; }).join('') || '
No trades in this period
'; } } window._allocPairMap = pairMap; window._allocPeriodTrades = periodTrades; // ── Win Rate Doughnut ── var wins = periodTrades.filter(function(t){return t.pnl>0;}).length; var losses = periodTrades.filter(function(t){return t.pnl<0;}).length; var even = periodTrades.length - wins - losses; var total3 = periodTrades.length; var winPct = total3>0 ? Math.round(wins/total3*100) : 0; var wpEl = document.getElementById('kcWinRatePct'); if(wpEl) wpEl.textContent = winPct+'%'; var wrCtx = document.getElementById('kcWinRateChart'); if(wrCtx){ if(kcWinRateChart) kcWinRateChart.destroy(); kcWinRateChart = new Chart(wrCtx.getContext('2d'),{ type:'doughnut', data:{ labels:['Profits','Losses','Break-Even'], datasets:[{ data: total3>0 ? [wins,losses,even] : [0,0,1], backgroundColor:['#22c55e','#b91c1c','#e2e8f0'], borderWidth:0 }] }, options:{responsive:true,maintainAspectRatio:false,cutout:'70%',plugins:{legend:{display:false},tooltip:{callbacks:{label:function(c){return c.label+': '+c.raw;}}}} } }); var wrLeg = document.getElementById('kcWinRateLegend'); if(wrLeg){ wrLeg.innerHTML = [ {label:'Profits', color:'#22c55e', val:wins, filter:'win'}, {label:'Losses', color:'#b91c1c', val:losses, filter:'loss'}, {label:'Break-Even',color:'#94a3b8', val:even, filter:'even'}, ].map(function(r){ return '
' + '
' + '
'+r.label+'
' + '
'+r.val+'
'; }).join(''); } } } function showWinRateDetail(filterType, label){ var trades = window._allocPeriodTrades || []; var matched = trades.filter(function(t){ if(filterType==='win') return t.pnl > 0; if(filterType==='loss') return t.pnl < 0; return t.pnl === 0; }); var overlay = document.getElementById('winRateDetailOverlay'); var titleEl = document.getElementById('wrdTitle'); var listEl = document.getElementById('wrdList'); if(!overlay||!titleEl||!listEl) return; titleEl.textContent = label + ' (' + matched.length + ')'; listEl.innerHTML = matched.length === 0 ? '
No trades in this category.
' : matched.map(function(t){ var idx = tradeHistory.findIndex(function(x){ return x.id === t.id; }); var pnlColor = t.pnl>=0 ? 'var(--green)' : 'var(--red)'; var dateStr = new Date(t.ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); return '
' + '
'+t.pair+'
'+dateStr+'
' + '
'+(t.pnl>=0?'+':'')+fmt(t.pnl,2)+' USDT
' + '
'; }).join(''); overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function closeWinRateDetail(){ var overlay = document.getElementById('winRateDetailOverlay'); if(overlay) overlay.style.display = 'none'; document.body.style.overflow = ''; } function showInfoModal(text){ var overlay = document.getElementById('infoModalOverlay'); var textEl = document.getElementById('infoModalText'); if(!overlay||!textEl) return; textEl.textContent = text; overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function closeInfoModal(){ var overlay = document.getElementById('infoModalOverlay'); if(overlay) overlay.style.display = 'none'; document.body.style.overflow = ''; } // ── Full Asset Allocation breakdown (all pairs, not just top 6) ── function openAllocFullDetail(){ var pairMap = window._allocPairMap || {}; var trades = window._allocPeriodTrades || []; var pairs = Object.keys(pairMap).sort(function(a,b){ return pairMap[b]-pairMap[a]; }); var total = pairs.reduce(function(s,p){ return s+pairMap[p]; }, 0); var COLORS = ['#2DD4BF','#3b82f6','#a78bfa','#22c55e','#67e8f9','#f59e0b','#f87171','#eab308','#fb923c','#94a3b8']; var overlay = document.getElementById('winRateDetailOverlay'); var titleEl = document.getElementById('wrdTitle'); var listEl = document.getElementById('wrdList'); if(!overlay||!titleEl||!listEl) return; titleEl.textContent = 'Asset Allocation (' + pairs.length + ' pairs)'; listEl.innerHTML = pairs.length === 0 ? '
No trades yet.
' : pairs.map(function(p,i){ var pct = total>0 ? (pairMap[p]/total*100) : 0; var pairTrades = trades.filter(function(t){ return (t.pair||'').replace('/USDT','') === p; }); var color = COLORS[i % COLORS.length]; return '
' + '
' + '
' + '
'+p+'
'+pairTrades.length+' trade'+(pairTrades.length!==1?'s':'')+' · '+fmt(pairMap[p],2)+' USDT invested
' + '
' + '
' + ''+pct.toFixed(2)+'%' + '' + '
' + '
'; }).join(''); overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function openTdModal(idx){ var t = tradeHistory[idx]; if(!t){ showToast('Trade not found'); return; } var isWin = t.pnl >= 0; var pnlColor = isWin ? '#2DD4BF' : '#f85149'; var date = new Date(t.ts); var dateStr = date.toLocaleString('en-GB', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}); // APR = (pnl/invested) * 365 * 100 var apr = (t.invested && t.invested>0) ? ((t.pnl/t.invested)*365*100) : 0; // Fees var s2 = JSON.parse(localStorage.getItem('mg_settings')||'{}'); var makerR = parseFloat(s2.makerFee||0.08)/100 * (s2.kcsDiscount?0.8:1); var takerR = parseFloat(s2.takerFee||0.10)/100 * (s2.kcsDiscount?0.8:1); var buyFee = t.buyFees || (t.invested||0)*makerR; var sellFee = t.sellFee || ((t.sellPrice||0)*(t.qty||0))*takerR; var totalFee= t.fees || (buyFee+sellFee); var grossPnl= (t.pnl||0)+totalFee; // Coin qty var coinSymbol = (t.pair||'').replace('/USDT',''); var returnCoin = fmt(t.qty||0,2)+' '+coinSymbol; var returnUSDT = fmt((t.sellPrice||0)*(t.qty||0),4)+' USDT'; // Price range: low = avgBuy * (1 - priceDrop%), high = sellPrice var pdrop = parseFloat(document.getElementById('priceDrop')&&document.getElementById('priceDrop').value||1.7); var priceLow = fmt((t.avgBuy||0)*(1-pdrop/100*(t.ordersCount||1)),4); var priceHigh = fmt(t.sellPrice||0,2); var priceRange = priceLow+' ~ '+priceHigh; var panel = document.getElementById('tdPanel'); panel.innerHTML = '
' + '' + '
History
' + '
' + '
' + '
' + '
' + '
📈
' + '
' + '
Spot Martingale
' + '
'+(t.pair||'—')+'
' + '
' + '
' + '
Stopped
' + '
' + '
' + 'Stopped at '+dateStr+'
' + '0d · '+t.ordersCount+' orders filled · '+(t.ordersCount||1)+' arbitrages'+ '
' + '
' + '
' + '
' + '
💵
Investment
' + '
'+fmt(t.invested||0)+'USDT
' + '
' + '
' + '
'+(isWin?'📈':'📉')+'
Total Profit
' + '
'+(t.pnl>=0?'+':'')+fmt(t.pnl,2)+'USDT
' + '
'+(t.pnl>=0?'+':'')+fmt(t.pnlPct,2)+'%
' + '
' + '
' + '
' + '
↩ Funds returned
' + '
' + '
'+returnCoin+'
' + '
'+returnUSDT+'
' + '
' + '
' + '
' + '
'+ tdGridItem('Price Range', priceRange) + tdGridItem('Avg. Price', fmt(t.avgBuy,2)) + tdGridItem('Sell Price', fmt(t.sellPrice,2)) + tdGridItem('Buy Fee', '−'+fmt(buyFee,2), '#f85149') + tdGridItem('Sell Fee','−'+fmt(sellFee,2), '#f85149') + tdGridItem('Total Fees', '−'+fmt(totalFee,2), '#f85149') + '
'+ '
'+ ''+ ''+ '
'; document.getElementById('tdOverlay').style.display='flex'; document.body.style.overflow='hidden'; } function tdGridItem(label, val, color){ return '
' + '
'+label+'
' + '
'+val+'
' + '
'; } // ════════════════════════════════════════════════ // ASSET ALLOCATION — HOLDING DETAIL // ════════════════════════════════════════════════ function openHoldingDetail(pair){ if(pair === 'USDT'){ showToast('USDT — cash from closed trades, no coin detail to show'); return; } var pairMap = window._allocPairMap || {}; var periodTrades = window._allocPeriodTrades || []; var trades = periodTrades.filter(function(t){ return (t.pair||'').replace('/USDT','') === pair; }); if(trades.length === 0){ showToast('No data for ' + pair); return; } // Aggregate holding stats var totalInvested = trades.reduce(function(s,t){ return s + (t.invested||0); }, 0); var totalQty = trades.reduce(function(s,t){ return s + (t.qty||0); }, 0); var totalPnl = trades.reduce(function(s,t){ return s + (t.pnl||0); }, 0); var lastTrade = trades.slice().sort(function(a,b){ return b.ts - a.ts; })[0]; var lastPrice = lastTrade ? lastTrade.sellPrice : 0; var marketValue = lastPrice * (totalQty / Math.max(trades.length,1)); // approx avg-qty-per-trade * last price // Better: market value = sum of (qty * sellPrice) per trade, since each trade's qty already sold at its own price marketValue = trades.reduce(function(s,t){ return s + ((t.qty||0) * (t.sellPrice||0)); }, 0); var avgBuy = totalQty > 0 ? (totalInvested / totalQty) : 0; var pnlPct = totalInvested > 0 ? (totalPnl / totalInvested * 100) : 0; var wins = trades.filter(function(t){ return t.pnl > 0; }).length; var panel = document.getElementById('tdPanel'); panel.innerHTML = '
' + '' + '
' + pair + ' Holdings
' + '
' + '
' + '
' + '
' + '
' + pair.slice(0,2) + '
' + '
' + '
' + pair + '/USDT
' + '
' + trades.length + ' trade' + (trades.length>1?'s':'') + ' in this period
' + '
' + '
' + '
' + '
' + '
' + '
' + '
Invested
' + '
' + fmt(totalInvested) + ' USDT
' + '
' + '
' + '
' + '
Total P&L
' + '
' + (totalPnl>=0?'+':'') + fmt(totalPnl,2) + ' USDT
' + '
' + (pnlPct>=0?'+':'') + fmt(pnlPct,2) + '%
' + '
' + '
' + '
' + '
' + tdGridItem('Number of Coins', fmt(totalQty,2)) + tdGridItem('Market Value', fmt(marketValue,2) + ' USDT') + tdGridItem('Avg Buy Price', fmt(avgBuy,2) + ' USDT') + tdGridItem('Last Sell Price', fmt(lastPrice,2) + ' USDT') + tdGridItem('Win Rate', Math.round(wins/trades.length*100) + '%', wins/trades.length>=0.5?'#2DD4BF':'#f85149') + tdGridItem('Trades Closed', trades.length) + '
' + '
' + '' + '
'; document.getElementById('tdOverlay').style.display='flex'; document.body.style.overflow='hidden'; } function closeTdModal(){ document.getElementById('tdOverlay').style.display='none'; document.body.style.overflow=''; } function shareTrade(idx){ var t = tradeHistory[idx]; if(!t) return; var text = '📊 Martingale Trade Report\n' + 'Pair: '+t.pair+'\n' + 'Invested: '+fmt(t.invested)+' USDT\n' + 'P&L: '+(t.pnl>=0?'+':'')+fmt(t.pnl,2)+' USDT ('+fmt(t.pnlPct,2)+'%)\n' + 'Orders: '+t.ordersCount+'\n' + 'Date: '+new Date(t.ts).toLocaleDateString('en-GB'); if(navigator.share){ navigator.share({ title:'Martingale Trade', text:text }).catch(function(){}); } else { copyVal(text); } } // ════ NOTIFICATION CENTER ════ notifications = JSON.parse(localStorage.getItem('mg_notifications') || '[]'); notifTab = 'all'; function saveNotifications(){ localStorage.setItem('mg_notifications', JSON.stringify(notifications.slice(0,100))); } NOTIF_ICONS = { trade: '✅', loss: '❌', alert: '⚠️', report: '', system: '🔔', news: '📰', }; function addNotification(type, title, message, data){ // type: 'trade' | 'alert' | 'report' | 'system' | 'loss' | 'news' var n = { id: Date.now() + '_' + Math.random().toString(36).slice(2,6), type: type, title: title, message: message, data: data || null, ts: Date.now(), read: false, }; notifications.unshift(n); saveNotifications(); updateNotifBadge(); renderNotifList(); return n; } // ── Coin News Fetcher (CryptoCompare News API — free, no key) ── async function fetchCoinNews(){ var btn = document.getElementById('newsFetchBtn'); var holdingCoin = (document.getElementById('base') && document.getElementById('base').value || '').toUpperCase(); var coinSet = {}; if(holdingCoin) coinSet[holdingCoin] = true; (favorites || []).forEach(function(c){ coinSet[(c||'').toUpperCase()] = true; }); var coins = Object.keys(coinSet).filter(Boolean); if(coins.length === 0){ alert('No holding or favorite coins to fetch news for yet.'); return; } if(btn){ btn.disabled = true; btn.textContent = '📰 Fetching news…'; } var added = 0; for(var i=0;i1?'s':'') + ' added to notifications'); } } function updateNotifBadge(){ var unread = notifications.filter(function(n){ return !n.read; }).length; var badge = document.getElementById('notifBadge'); if(!badge) return; badge.textContent = unread > 99 ? '99+' : String(unread); badge.style.display = unread > 0 ? 'flex' : 'none'; } function toggleNotifPanel(){ var panel = document.getElementById('notifPanel'); var overlay = document.getElementById('notifOverlay'); if(!panel) return; var isOpen = panel.style.display !== 'none'; if(isOpen){ panel.style.display = 'none'; overlay.style.display = 'none'; } else { panel.style.display = 'flex'; overlay.style.display = 'block'; panel.style.transform = ''; renderNotifList(); initNotifPanelSwipe(); } } function initNotifPanelSwipe(){ var handle = document.getElementById('notifSwipeHandle'); var panel = document.getElementById('notifPanel'); if(!handle || !panel || handle._swipeInit) return; handle._swipeInit = true; var startY = 0, curY = 0, dragging = false; function start(y){ startY = y; curY = y; dragging = true; panel.style.transition = 'none'; } function move(y){ if(!dragging) return; curY = y; panel.style.transform = 'translateY(' + (curY - startY) + 'px)'; } function end(){ if(!dragging) return; dragging = false; var dy = curY - startY; panel.style.transition = 'transform .25s ease'; if(Math.abs(dy) > 70){ panel.style.transform = 'translateY(' + (dy<0?'-100%':'100%') + ')'; setTimeout(function(){ closeNotifPanel(); panel.style.transform=''; }, 220); } else { panel.style.transform = ''; } } handle.addEventListener('touchstart', function(e){ start(e.touches[0].clientY); }, {passive:true}); handle.addEventListener('touchmove', function(e){ move(e.touches[0].clientY); }, {passive:true}); handle.addEventListener('touchend', end); handle.addEventListener('mousedown', function(e){ start(e.clientY); function mm(ev){ move(ev.clientY); } function mu(){ end(); document.removeEventListener('mousemove',mm); document.removeEventListener('mouseup',mu); } document.addEventListener('mousemove',mm); document.addEventListener('mouseup',mu); }); } function closeNotifPanel(){ var panel = document.getElementById('notifPanel'); var overlay = document.getElementById('notifOverlay'); if(panel) panel.style.display = 'none'; if(overlay) overlay.style.display = 'none'; } function setNotifTab(tab){ notifTab = tab; document.querySelectorAll('.notif-tab').forEach(function(b){ b.classList.remove('active'); }); var el = document.getElementById('ntab-'+tab); if(el) el.classList.add('active'); renderNotifList(); } function markAllRead(){ notifications.forEach(function(n){ n.read = true; }); saveNotifications(); updateNotifBadge(); renderNotifList(); } function clearAllNotifs(){ var filtered = notifTab === 'all' ? [] : notifications.filter(function(n){ return n.type !== (notifTab==='trades'?'trade':notifTab==='alerts'?'alert':notifTab==='reports'?'report':'system'); }); notifications = filtered; saveNotifications(); updateNotifBadge(); renderNotifList(); } function deleteNotif(id){ notifications = notifications.filter(function(n){ return n.id !== id; }); saveNotifications(); updateNotifBadge(); renderNotifList(); } function markRead(id){ var n = notifications.find(function(n){ return n.id===id; }); if(n){ n.read=true; saveNotifications(); updateNotifBadge(); renderNotifList(); } } function fmtRelTime(ts){ var diff = Date.now() - ts; var s = Math.floor(diff/1000); if(s < 60) return 'Just now'; if(s < 3600) return Math.floor(s/60)+'m ago'; if(s < 86400) return Math.floor(s/3600)+'h ago'; return Math.floor(s/86400)+'d ago'; } function renderNotifList(){ var list = document.getElementById('notifList'); if(!list) return; var typeFilter = { all: null, trades: ['trade','loss'], alerts: ['alert'], reports: ['report'], system: ['system'], }[notifTab]; var filtered = typeFilter ? notifications.filter(function(n){ return typeFilter.indexOf(n.type) >= 0; }) : notifications; if(filtered.length === 0){ list.innerHTML = '
🔔
No notifications yet
Events like trade closes and alerts appear here
'; return; } list.innerHTML = filtered.map(function(n){ var clickAction = (n.type === 'news' && n.data && n.data.url) ? "markRead('"+n.id+"');window.open('"+n.data.url+"','_blank')" : "markRead('"+n.id+"')"; return '
' + '
🗑 Remove
' + '
' + '
'+(NOTIF_ICONS[n.type]||'🔔')+'
' + '
' + '
'+n.title+'
'+n.message+(n.data&&n.data.source?' — '+n.data.source+'':'')+'
' + '
'+fmtRelTime(n.ts)+'
' + '
' + '' + '
' + '
'; }).join(''); initNotifSwipe(); } // ── Swipe-to-dismiss for notification items ────────── function initNotifSwipe(){ var wraps = document.querySelectorAll('.notif-swipe-wrap'); wraps.forEach(function(wrap){ var item = wrap.querySelector('.notif-item'); var startX = 0, currentX = 0, dragging = false; function onStart(x){ startX = x; currentX = x; dragging = true; item.style.transition = 'none'; } function onMove(x){ if(!dragging) return; currentX = x; var dx = currentX - startX; item.style.transform = 'translateX(' + dx + 'px)'; wrap.querySelector('.notif-swipe-bg').style.opacity = Math.min(1, Math.abs(dx) / 80); } function onEnd(){ if(!dragging) return; dragging = false; var dx = currentX - startX; item.style.transition = 'transform .25s ease, opacity .25s ease'; if(Math.abs(dx) > 90){ // Swiped far enough — dismiss item.style.transform = 'translateX(' + (dx > 0 ? '120%' : '-120%') + ')'; item.style.opacity = '0'; var id = wrap.getAttribute('data-notif-id'); setTimeout(function(){ deleteNotif(id); }, 220); } else { // Snap back item.style.transform = 'translateX(0)'; wrap.querySelector('.notif-swipe-bg').style.opacity = '0'; } } item.addEventListener('touchstart', function(e){ onStart(e.touches[0].clientX); }, {passive:true}); item.addEventListener('touchmove', function(e){ onMove(e.touches[0].clientX); }, {passive:true}); item.addEventListener('touchend', onEnd); // Mouse support for desktop testing item.addEventListener('mousedown', function(e){ onStart(e.clientX); }); document.addEventListener('mousemove', function(e){ if(dragging) onMove(e.clientX); }); document.addEventListener('mouseup', onEnd); }); } function notifyTradeClosed(trade){ var isWin = trade.pnl >= 0; addNotification( isWin ? 'trade' : 'loss', isWin ? '✅ Trade Closed — WIN' : '❌ Trade Closed — LOSS', trade.pair + ' | P&L: ' + (trade.pnl>=0?'+':'') + fmt(trade.pnl) + ' USDT (' + (trade.pnlPct>=0?'+':'') + fmt(trade.pnlPct,2) + '%)' ); } function notifyPriceAlert(pair, price, type){ addNotification( 'alert', '⚠️ Price Alert — ' + pair, type + ' reached: ' + fmt(price,2) + ' USDT' ); } function notifyExport(format, count){ addNotification( 'report', '📊 Export Complete', count + ' trades exported as ' + format ); } function applyLang(l){ var isAr=(l==='ar'); document.documentElement.dir=isAr?'rtl':'ltr'; document.documentElement.lang=l; document.body.style.fontFamily=isAr?"'Dubai','Segoe UI',Tahoma,Arial,sans-serif":''; var T=isAr?{ tab_strategy:'الاستراتيجية',tab_history:'سجل الأرباح', tab_chart:'الرسم البياني',tab_pairs:'إحصائيات الأزواج', create:'إنشاء',badge:'محاكي الاستراتيجية',parameters:'المعاملات', strategy_parameters:'معاملات الاستراتيجية',investment:'الاستثمار', martingale_strategy:'استراتيجية مارتينغال',clear_all:'🗑 مسح الكل' }:{ tab_strategy:'Strategy',tab_history:'Profit History', tab_chart:'Chart',tab_pairs:'Pair Stats', create:'Create',badge:'Strategy Simulator',parameters:'Parameters', strategy_parameters:'Strategy Parameters',investment:'Investment', martingale_strategy:'Martingale Strategy',clear_all:'🗑 Clear All' }; document.querySelectorAll('[data-i18n]').forEach(function(el){ var k=el.getAttribute('data-i18n'); if(T[k]!==undefined)el.textContent=T[k]; }); var thMap=isAr?{'Date':'التاريخ','Pair':'الزوج','Orders Filled':'الأوامر المنفذة','Avg Buy':'متوسط الشراء','Sell Price':'سعر البيع','Qty':'الكمية','Invested':'المستثمر','P&L (USDT)':'الربح/الخسارة','P&L %':'نسبة الربح','Result':'النتيجة','No.':'رقم'}:{'التاريخ':'Date','الزوج':'Pair','الأوامر المنفذة':'Orders Filled','متوسط الشراء':'Avg Buy','سعر البيع':'Sell Price','الكمية':'Qty','المستثمر':'Invested','الربح/الخسارة':'P&L (USDT)','نسبة الربح':'P&L %','النتيجة':'Result','رقم':'No.'}; document.querySelectorAll('#historyTable thead th').forEach(function(th){var k=th.textContent.trim();if(thMap[k])th.textContent=thMap[k];}); var cardAR={'MAX NO. OF ADDS':'الحد الأقصى للإضافات','TOTAL DEPLOYED':'إجمالي المنشور','TOTAL QUANTITY':'الكمية الإجمالية','AVG ENTRY PRICE':'متوسط سعر الدخول','TAKE PROFIT TARGET':'هدف جني الأرباح','TOTAL WITHDRAWN':'إجمالي السحب','NET INVESTMENT':'صافي الاستثمار'}; var cardEN={'الحد الأقصى للإضافات':'MAX NO. OF ADDS','إجمالي المنشور':'TOTAL DEPLOYED','الكمية الإجمالية':'TOTAL QUANTITY','متوسط سعر الدخول':'AVG ENTRY PRICE','هدف جني الأرباح':'TAKE PROFIT TARGET','إجمالي السحب':'TOTAL WITHDRAWN','صافي الاستثمار':'NET INVESTMENT'}; document.querySelectorAll('.card-label').forEach(function(el){var k=el.textContent.trim().toUpperCase();var map=isAr?cardAR:cardEN;if(map[k])el.textContent=map[k];}); } // ── Safe element helpers ───────────────────────── function gel(id){ return document.getElementById(id); } function setText(id,val){ const e=document.getElementById(id); if(e) e.textContent=val; } function setAnim(id,val){ const e=document.getElementById(id); if(e) e.style.animation=val; } function setBorder(id,val){ const e=document.getElementById(id); if(e) e.style.borderColor=val; } function setPlaceholder(id,val){ const e=document.getElementById(id); if(e) e.placeholder=val; } function setVal(id,val){ const e=document.getElementById(id); if(e) e.value=val; } // ── Tab navigation: move focus to next/prev focusable input ── function focusNext(el){ const all=[...document.querySelectorAll( 'input:not([type=hidden]):not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled])' )].filter(e=>e.offsetParent!==null && !e.closest('.cg-overlay')); const idx=all.indexOf(el); if(idx>-1 && idxe.offsetParent!==null && !e.closest('.cg-overlay')); const idx=all.indexOf(el); if(idx>0) all[idx-1].focus(); } // Global Tab handler — ensures Tab always moves to next input, Shift+Tab to previous // Attach to all stepper inputs and special fields that override default Tab document.addEventListener('keydown', function(e){ if(e.key !== 'Tab') return; const tag = e.target.tagName; const type = e.target.type; // Only intercept on inputs that might have broken Tab behavior if(tag === 'INPUT' || tag === 'TEXTAREA'){ // Close any open dropdown const dd = document.getElementById('coinDropdown'); if(dd) dd.style.display = 'none'; // Let natural Tab work UNLESS the field is a stepper-val (number inside a flex container) // For stepper inputs, natural Tab should work — do nothing special } }); // ════════════════════════════════════════════════ // STATE // ════════════════════════════════════════════════ currentOrders = []; tradeHistory = JSON.parse(localStorage.getItem('mg_history') || '[]'); // ════════════════════════════════════════════════ // UTILS // ════════════════════════════════════════════════ function fmt(n, d=2){ return Number(n).toLocaleString('en-US',{minimumFractionDigits:0,maximumFractionDigits:d}); } function fmtDate(ts){ return new Date(ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'2-digit'}); } function stepVal(id,delta){ const el=document.getElementById(id); el.value=Math.max(parseFloat(el.min)||0,Math.round((parseFloat(el.value)||0+delta)*100)/100); } function toggleAdv(){ const b=document.getElementById('advBody'),c=document.getElementById('advChevron'); if(!b||!c) return; const open=b.style.display!=='none'; b.style.display=open?'none':'block'; c.textContent=open?'∨':'∧'; } function switchTab(name,btn){ document.querySelectorAll('.tab-pane').forEach(p=>p.classList.remove('active')); document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); document.getElementById('tab-'+name).classList.add('active'); btn.classList.add('active'); if(name==='chart') renderCharts(); if(name==='pairs') renderPairStats(); if(name==='strategies') renderStrategiesList(); if(name==='fund') renderCapitalFlowChart(); if(name==='ar') renderArTab(); if(name==='budget'){ bdgLoad(); renderBudget(); } if(name==='charity'){ chtLoad(); renderCharityTab(); } } // ════════════════════════════════════════════════ // COIN SEARCH // ════════════════════════════════════════════════ // (duplicate removed) const COINS=["1000SHIB","1INCH","AAVE","ACT","ADA","AEVO","ALGO","ALICE","ALT","ANKR","API3","APT","ARB","ARKM","ATOM","AUDIO","AVAX","AXS","BAL","BANANA","BAND","BAT","BB","BADGER","BAKE","BCH","BEAM","BLUR","BNB","BONK","BOME","BTC","BURGER","CAKE","CATI","CELO","CELR","CFX","CHZ","CHILLGUY","COMBO","COMP","CRV","CYBER","DASH","DGB","DOGS","DOT","DOGE","DUSK","DYDX","DYM","EGLD","EIGEN","ENJ","EOS","ETC","ETH","ETHFI","FIL","FLR","FLOKI","FTM","G","GALA","GOAT","GRT","HBAR","HMSTR","HOOK","ICP","IMX","INJ","IO","IOTA","JUP","KEY","KSM","LINK","LISTA","LRC","LSK","LTC","LUNA","LUNC","MAJOR","MANA","MANTA","MAV","MATIC","METIS","MKR","MOG","MOODENG","NANO","NEAR","NEIRO","NEO","NMR","NOT","NTRN","OCEAN","OMNI","OMG","ONDO","ONE","OP","ORDI","PENDLE","PEPE","PERP","PIXEL","PNUT","POL","PORTAL","PYTH","QTUM","REEF","RENDER","REN","REZ","ROSE","RUNE","SAGA","SAND","SATS","SC","SCRT","SEI","SKL","SNX","SOL","SPX","STEEM","STORJ","STRK","STX","SUI","SUSHI","TAIKO","THETA","TIA","TLM","TNSR","TRX","UNI","USTC","VET","VITE","VIRTUAL","W","WAVES","WIF","WLD","XEM","XLM","XMR","XRP","YFI","ZEC","ZETA","ZIL","ZK","ZKJ","ZRO","ZRX"].sort(); function renderDropdown(list){ const dd=document.getElementById('coinDropdown'); dd.innerHTML=list.map(c=>{ var flagged = (typeof coinFlags!=='undefined' && coinFlags[c]) ? true : false; return `
${c} / USDT${flagged?'🔖':''}
`; }).join(''); dd.style.display=list.length?'block':'none'; } function openDropdown(){ filterCoins(); } function filterCoins(){ const q=document.getElementById('coinSearch').value.trim().toUpperCase(); renderDropdown((q?COINS.filter(c=>c.startsWith(q)||c.includes(q)):COINS).slice(0,60)); } function selectCoin(coin){ document.getElementById('base').value=coin; document.getElementById('coinSearch').value=coin; document.getElementById('coinDropdown').style.display='none'; fetchPrice(coin); renderFavChips(); } document.addEventListener('click',e=>{ if(!e.target.closest('#coinSearch')&&!e.target.closest('#coinDropdown')) document.getElementById('coinDropdown').style.display='none'; }); // ── Coin search keyboard navigation (Tab fix) ────────────── document.addEventListener('DOMContentLoaded',function(){ const cs=document.getElementById('coinSearch'); if(!cs)return; cs.addEventListener('keydown',function(e){ if(e.key==='Tab'){ e.preventDefault(); e.stopPropagation(); document.getElementById('coinDropdown').style.display='none'; e.shiftKey ? focusPrev(cs) : focusNext(cs); } if(e.key==='Escape'){ document.getElementById('coinDropdown').style.display='none'; } }); // ── Global event delegation for dynamic delete buttons ────── document.addEventListener('click', function(e){ // Delete single trade row (data-delete-trade="index") var dtBtn = e.target.closest('[data-delete-trade]'); if(dtBtn){ e.stopPropagation(); var idx = parseInt(dtBtn.getAttribute('data-delete-trade')); if(!isNaN(idx)) deleteSingleTrade(idx); return; } // Delete withdrawal entry (data-delete-withdrawal="index") var dwBtn = e.target.closest('[data-delete-withdrawal]'); if(dwBtn){ e.stopPropagation(); deleteWithdrawal(parseInt(dwBtn.getAttribute('data-delete-withdrawal'))); return; } // Delete capital entry var dcBtn = e.target.closest('[data-delete-capital]'); if(dcBtn){ e.stopPropagation(); deleteCapital(parseInt(dcBtn.getAttribute('data-delete-capital'))); return; } // Delete Zakat payment var dzBtn = e.target.closest('[data-delete-zakat]'); if(dzBtn){ e.stopPropagation(); deleteZakatPayment(parseInt(dzBtn.getAttribute('data-delete-zakat'))); return; } }); }); // ════════════════════════════════════════════════ // LIVE PRICE // ════════════════════════════════════════════════ function formatPrice(p){ if(p>=1000)return parseFloat(p.toFixed(2)); if(p>=1)return parseFloat(p.toFixed(2)); if(p>=0.01)return parseFloat(p.toFixed(2)); return parseFloat(p.toFixed(8)); } // ── Entry Price: fetched from TradingView/Binance/KuCoin ────── function syncEntryPrice(val){ document.getElementById('startPrice').value = val; } function setEntrySuccess(price, source){ const p = formatPrice(price); setVal('entryPriceMin', p); setVal('startPrice', p); setText('epStatus', '● Live'); const _es=gel('epStatus'); if(_es) _es.style.color='#22c55e'; setText('epSource', 'Source: ' + source); setAnim('epRefreshBtn', ''); const inp = document.getElementById('entryPriceMin'); if(inp){ inp.style.transition='border-color .4s'; inp.style.borderColor='#22c55e'; setTimeout(()=>{ inp.style.borderColor=''; },2000); } } function setEntryFailed(){ setPlaceholder('entryPriceMin', 'Enter price manually'); setText('epStatus', ''); setText('epSource', ''); document.getElementById('epRefreshBtn').style.animation = ''; } _activeWS = null; function fetchEntryPrice(coinOverride){ // Manual entry only - network disabled on this device setPlaceholder('entryPriceMin', 'Enter price manually'); setText('epStatus',''); } async function tryRestSources(symbol, resolve){ const endpoints = [ [`https://data-api.binance.vision/api/v3/ticker/price?symbol=${symbol}USDT`, d=>d.price, 'Binance'], [`https://api.binance.com/api/v3/ticker/price?symbol=${symbol}USDT`, d=>d.price, 'Binance'], [`https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=${symbol}-USDT`, d=>d.data&&d.data.price, 'KuCoin'], [`https://api.coinbase.com/v2/prices/${symbol}-USD/spot`, d=>d.data&&d.data.amount, 'Coinbase'], [`https://api.allorigins.win/raw?url=${encodeURIComponent('https://api.binance.com/api/v3/ticker/price?symbol='+symbol+'USDT')}`, d=>d.price, 'Binance'], [`https://corsproxy.io/?url=${encodeURIComponent('https://api.binance.com/api/v3/ticker/price?symbol='+symbol+'USDT')}`, d=>d.price, 'Binance'], [`https://api.allorigins.win/raw?url=${encodeURIComponent('https://query1.finance.yahoo.com/v8/finance/chart/'+symbol+'-USD?interval=1m&range=1d')}`, d=>d.chart&&d.chart.result&&d.chart.result[0]&&d.chart.result[0].meta&&d.chart.result[0].meta.regularMarketPrice, 'TradingView'], ]; const promises = endpoints.map(([url, xform, label]) => fetch(url, { cache:'no-store' }) .then(r=>{ if(!r.ok) throw 0; return r.json(); }) .then(d=>{ const p=parseFloat(xform(d)); if(p>0&&!isNaN(p)) return {price:p,source:label}; throw 0; }) ); // JSONP source (works when fetch is blocked by WebView) const jsonpProm = jsonpFetch('https://min-api.cryptocompare.com/data/price?fsym='+symbol+'&tsyms=USDT,USD', 6000) .then(d=>{ const p=parseFloat(d.USDT||d.USD); if(p>0&&!isNaN(p)) return {price:p,source:'CryptoCompare'}; throw 0; }); try { const result = await Promise.any(promises.concat([jsonpProm])); resolve(result.price, result.source); } catch(_){ // total failure — clear spinner, let user type setEntryFailed(); } } // alias function fetchPrice(coin){ fetchEntryPrice(coin); } // ════════════════════════════════════════════════ // CALCULATE / BUILD TABLE // ════════════════════════════════════════════════ // ── Investment input comma formatter ───────────── function formatInvestInput(el){ // Remove anything that isn't a digit or dot let raw = el.value.replace(/,/g,'').replace(/[^0-9.]/g,''); // Only one decimal point const parts = raw.split('.'); if(parts.length > 2) raw = parts[0]+'.'+parts.slice(1).join(''); // Format integer part with commas const intPart = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,','); el.value = parts.length === 2 ? intPart+'.'+parts[1] : intPart; // Store raw numeric for calculations el.dataset.raw = raw; } function investKeydown(e, el){ // Allow: backspace, delete, arrows, tab, enter, decimal point const allowed = ['Backspace','Delete','ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Tab','Enter','.','Home','End']; if(allowed.includes(e.key)) return; // Block non-numeric keys if(!/^\d$/.test(e.key)){ e.preventDefault(); } } function getInvestValue(){ const el = document.getElementById('initAmount'); return parseFloat((el.dataset.raw || el.value.replace(/,/g,'')) || '0'); } function setInvestValue(val){ const el = document.getElementById('initAmount'); el.dataset.raw = String(val); const s = String(val); const parts = s.split('.'); el.value = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g,',') + (parts[1] ? '.'+parts[1] : ''); } function calculate(){ saveCurrentStrategy(); const base =document.getElementById('base').value||'BTC'; // Entry price comes from the single entry price field const startPrice=parseFloat(document.getElementById('entryPriceMin').value)||parseFloat(document.getElementById('startPrice').value); const initAmount=getInvestValue(); const priceDrop =parseFloat(document.getElementById('priceDrop').value)/100; const takeProfit=parseFloat(document.getElementById('takeProfit').value)/100; const multiplier=parseFloat(document.getElementById('multiplier').value); const numOrders =parseInt(document.getElementById('numOrders').value); if([startPrice,initAmount,priceDrop,takeProfit,multiplier,numOrders].some(isNaN)||initAmount<=0){ alert('Please fill in all required fields.'); return; } // Calculate order weights so amounts sum to total investment currentOrders=[]; let weightSum=0; for(let i=0;i `
${p.label} ${p.value}
`).join(''); } document.getElementById('tableSubtitle').textContent=`Entry at ${fmt(startPrice)} USDT · tick filled orders`; document.getElementById('sumOrders').textContent=numOrders; document.getElementById('sumDeployed').textContent=fmt(last.execAmount,0); document.getElementById('sumQty').textContent=fmt(last.execQty,0); document.getElementById('sumAvgPrice').textContent=fmt(last.avgPrice); document.getElementById('sumTP').textContent=fmt(last.tpPrice); renderWithdrawalSummary(); renderTable(); document.getElementById('emptyState').style.display='none'; document.getElementById('results').style.display='block'; // Auto-switch to Strategy tab and scroll dashboard into view const stratBtn = document.querySelector('.tab-btn'); switchTab('strategy', stratBtn); const dashboard = document.querySelector('.dashboard'); if(dashboard) dashboard.scrollTo({top:0, behavior:'smooth'}); } function renderTable(){ const last=currentOrders[currentOrders.length-1]; const tbody=document.getElementById('tableBody'); tbody.innerHTML=''; currentOrders.forEach((o,i)=>{ const activated=o.activated; // P&L for this order set if activated let pnlHtml='—'; if(activated){ const pnl=(o.sellPrice-o.avgPrice)*o.execQty; const pnlPct=((o.sellPrice-o.avgPrice)/o.avgPrice)*100; const cls=pnl>=0?'pnl-cell-pos':'pnl-cell-neg'; pnlHtml=`${pnl>=0?'+':''}${fmt(pnl)}
${pnl>=0?'+':''}${fmt(pnlPct,2)}%
`; } // TP Profit: if TP hit at this add level, profit = (tpPrice - avgPrice) * execQty const tpProfit = (o.tpPrice - o.avgPrice) * o.execQty; const tpProfitPct = ((o.tpPrice - o.avgPrice) / o.avgPrice) * 100; const tr=document.createElement('tr'); if(activated) tr.classList.add('activated'); tr.innerHTML=`
${i+1} ${activated?'● Filled':'○ Pending'} ${fmt(o.price)} ${fmt(o.amount)} ${fmt(o.avgPrice)} ${fmt(o.execQty,0)} ${fmt(o.execAmount,0)} ${activated?``:'—'} ${pnlHtml} ${tpProfit>=0?'+':''}${fmt(tpProfit)}
+${fmt(tpProfitPct,2)}% · TP: ${fmt(o.tpPrice,2)} `; tbody.appendChild(tr); tbody.appendChild(tr); }); updatePnlBar(); } function toggleOrder(i){ const current = currentOrders[i].activated; if(!current){ // TICKING ON: all previous orders must be activated first for(let j = 0; j < i; j++){ if(!currentOrders[j].activated){ // Flash the missing order to show which must be ticked first const rows = document.getElementById('tableBody').querySelectorAll('tr'); if(rows[j]){ rows[j].style.transition = 'background .15s'; rows[j].style.background = 'rgba(239,68,68,0.18)'; setTimeout(()=>{ rows[j].style.background = ''; }, 600); } return; // block — order j must be ticked first } } currentOrders[i].activated = true; } else { // UNTICKING: all subsequent orders must be unticked first for(let j = currentOrders.length - 1; j > i; j--){ if(currentOrders[j].activated){ const rows = document.getElementById('tableBody').querySelectorAll('tr'); if(rows[j]){ rows[j].style.transition = 'background .15s'; rows[j].style.background = 'rgba(239,68,68,0.18)'; setTimeout(()=>{ rows[j].style.background = ''; }, 600); } return; // block — must untick order j first } } currentOrders[i].activated = false; } renderTable(); checkAllFilled(); } function updateSellPrice(i,val){ const p=parseFloat(val); if(!isNaN(p)&&p>0){ currentOrders[i].sellPrice=p; updatePnlBar(); } } function updatePnlBar(){ const active=currentOrders.filter(o=>o.activated); const bar=document.getElementById('pnlBar'); const closePanel=document.getElementById('closeTradePanel'); const badge=document.getElementById('activeTradeBadge'); const badgeText=document.getElementById('activeTradeBadgeText'); if(active.length===0){ bar.style.display='none'; if(closePanel) closePanel.style.display='none'; if(badge) badge.style.display='none'; return; } if(badge){ badge.style.display='flex'; } if(badgeText){ const pair=document.getElementById('base').value||'BTC'; badgeText.textContent=`${active.length} Order${active.length>1?'s':''} Active · ${pair}/USDT`; } bar.style.display='flex'; if(closePanel) closePanel.style.display='flex'; // Use last activated order's cumulative data const last=active[active.length-1]; const sellP=last.sellPrice; const pnl=(sellP-last.avgPrice)*last.execQty; const pnlPct=((sellP-last.avgPrice)/last.avgPrice)*100; document.getElementById('pnlActiveOrders').textContent=active.length; document.getElementById('pnlAvgBuy').textContent=fmt(last.avgPrice); document.getElementById('pnlSellPrice').textContent=fmt(sellP); const uEl=document.getElementById('pnlUnrealized'); uEl.textContent=(pnl>=0?'+':'')+fmt(pnl)+' USDT'; uEl.style.color=pnl>=0?'var(--green)':'var(--red)'; const pEl=document.getElementById('pnlPct'); pEl.textContent=(pnlPct>=0?'+':'')+fmt(pnlPct,2)+'%'; pEl.style.color=pnlPct>=0?'var(--green)':'var(--red)'; } // ════════════════════════════════════════════════ // CLOSE TRADE → HISTORY // ════════════════════════════════════════════════ function previewCustomSell(val){ const p=parseFloat(val); const preview=document.getElementById('customSellPreview'); if(!preview)return; if(!p||isNaN(p)){preview.textContent='';return;} const active=currentOrders.filter(o=>o.activated); if(active.length===0){preview.textContent='';return;} const last=active[active.length-1]; const pnl=(p-last.avgPrice)*last.execQty; const pnlPct=((p-last.avgPrice)/last.avgPrice)*100; const isPos=pnl>=0; preview.innerHTML=`${isPos?'+':''}${fmt(pnl)}
${isPos?'+':''}${fmt(pnlPct,2)}%
`; } function closeTrade(){ const active=currentOrders.filter(o=>o.activated); if(active.length===0){alert('No activated orders.');return;} const last=active[active.length-1]; const base=document.getElementById('base').value||'BTC'; const customInput=document.getElementById('customSellPrice'); const customVal=customInput?parseFloat(customInput.value):NaN; const sellPrice=(!isNaN(customVal)&&customVal>0)?customVal:last.sellPrice; const usedCustom=(!isNaN(customVal)&&customVal>0&&customVal!==last.sellPrice); const pnl=(sellPrice-last.avgPrice)*last.execQty; const pnlPct=((sellPrice-last.avgPrice)/last.avgPrice)*100; const trade={ id:Date.now(),ts:Date.now(), date:new Date().toISOString().split('T')[0], pair:base+'/USDT', ordersCount:active.length, avgBuy:last.avgPrice, sellPrice,qty:last.execQty, invested:last.execAmount, pnl,pnlPct,win:pnl>=0,customSell:usedCustom }; tradeHistory.unshift(trade); localStorage.setItem('mg_history',JSON.stringify(tradeHistory)); currentOrders.forEach(o=>{o.activated=false;}); if(customInput)customInput.value=''; const preview=document.getElementById('customSellPreview'); if(preview)preview.textContent=''; renderTable(); const badge=document.getElementById('activeTradeBadge'); if(badge)badge.style.display='none'; renderHistory(); playTradeCloseSound(); alert(`Trade closed${usedCustom?' (custom price)':''}!\nSell: ${fmt(sellPrice)} USDT\nP&L: ${pnl>=0?'+':''}${fmt(pnl)} USDT (${fmt(pnlPct,2)}%)`); } // ════════════════════════════════════════════════ // HISTORY RENDER // ════════════════════════════════════════════════ function renderHistory(){ const wins=tradeHistory.filter(t=>t.win).length; const losses=tradeHistory.filter(t=>!t.win).length; const total=tradeHistory.length; const totalProfit=tradeHistory.reduce((s,t)=>s+t.pnl,0); const winRate=total?((wins/total)*100).toFixed(1):0; const avg=total?totalProfit/total:0; document.getElementById('histTotalProfit').textContent=(totalProfit>=0?'+':'')+fmt(totalProfit)+' USDT'; document.getElementById('histTotalProfit').className='hist-card-value '+(totalProfit>=0?'green':'red'); document.getElementById('histTotalTrades').textContent=total; document.getElementById('histWins').textContent=wins; document.getElementById('histLosses').textContent=losses; document.getElementById('histWinRate').textContent=winRate+'%'; document.getElementById('histAvgProfit').textContent=(avg>=0?'+':'')+fmt(avg)+' USDT'; const tbody=document.getElementById('historyBody'); tbody.innerHTML=''; if(total===0){ document.getElementById('historyEmpty').style.display='block'; document.getElementById('historyTable').style.display='none'; return; } document.getElementById('historyEmpty').style.display='none'; document.getElementById('historyTable').style.display='table'; const totalTrades = tradeHistory.length; selectedTrades.clear(); updateSelectionUI(); tradeHistory.forEach((t,idx)=>{ const tr=document.createElement('tr'); tr.dataset.idx = idx; tr.style.cursor = 'pointer'; tr.onclick = function(e){ if(!e.target.closest('button') && !e.target.closest('.hist-cb')){ openTdModal(idx); } }; const pnlCls=t.pnl>=0?'pnl-cell-pos':'pnl-cell-neg'; const tradeNum = totalTrades - idx; // newest = highest number tr.innerHTML=`
${tradeNum} ${fmtDate(t.ts)} ${t.pair} ${t.ordersCount} ${fmt(t.avgBuy)} ${fmt(t.sellPrice)} ${fmt(t.qty,2)} ${fmt(t.invested)} ${t.pnl>=0?'+':''}${fmt(t.pnl)} ${t.pnlPct>=0?'+':''}${fmt(t.pnlPct,2)}% ${t.win?'WIN':'LOSS'} `; tbody.appendChild(tr); }); } function deleteSingleTrade(idx){ idx = parseInt(idx); if(isNaN(idx) || idx < 0 || idx >= tradeHistory.length){ showToast('Trade not found.'); return; } var t = tradeHistory[idx]; showConfirm('Delete trade: ' + t.pair + ' | P&L: ' + (t.pnl>=0?'+':'') + fmt(t.pnl) + ' USDT?', 'Delete', function(){ tradeHistory.splice(idx, 1); localStorage.setItem('mg_history', JSON.stringify(tradeHistory)); selectedTrades.clear(); renderHistory(); renderPnlPeriods(); showToast('Trade deleted.'); }); } function clearHistory(){ if(tradeHistory.length===0)return; if(!confirm('Delete ALL ' + tradeHistory.length + ' trade(s)? This cannot be undone.')) return; tradeHistory=[]; localStorage.setItem('mg_history','[]'); renderHistory(); renderPnlPeriods(); showToast('All history cleared.'); } // ════════════════════════════════════════════════ // CHARTS // ════════════════════════════════════════════════ zakatPeriod = 'daily'; realEstatePeriod = 'daily'; function setBenchPeriod(chart, period, btn){ if(chart === 'zakat'){ zakatPeriod = period; document.querySelectorAll('[id^="bz-"]').forEach(b=>b.classList.remove('active')); document.getElementById('bz-'+period).classList.add('active'); renderZakatChart(); } else { realEstatePeriod = period; document.querySelectorAll('[id^="re-"]').forEach(b=>b.classList.remove('active')); document.getElementById('re-'+period).classList.add('active'); renderRealEstateChart(); } } function renderCharts(){ renderProfitChart(); renderWinLossChart(); renderDailyChart(); renderInvestedChart(); renderPairProfitChart(); renderZakatChart(); renderRealEstateChart(); if(typeof renderFeesDashboard === 'function') renderFeesDashboard(); if(typeof renderZakatDashboard === 'function') renderZakatDashboard(); if(typeof renderConversionDashboard === 'function') renderConversionDashboard(); } // (duplicate removed) const chartDefaults={ // (dup-body) color:'#94a3b8', // (dup-body) plugins:{legend:{labels:{color:'#94a3b8',font:{family:"'Inter',sans-serif",size:12}}}}, // (dup-body) scales:{x:{ticks:{color:'#64748b',font:{size:11}},grid:{color:'#222b3a'}},y:{ticks:{color:'#64748b',font:{size:11}},grid:{color:'#222b3a'}}} // (dup-body) }; function renderProfitChart(){ const ctx=document.getElementById('profitChart').getContext('2d'); if(profitChart && typeof profitChart.destroy === 'function') profitChart.destroy(); if(tradeHistory.length===0){ profitChart=null; ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); ctx.fillStyle='#64748b'; ctx.font='14px Inter'; ctx.textAlign='center'; ctx.fillText('No trade data yet',ctx.canvas.width/2,ctx.canvas.height/2); return; } const sorted=[...tradeHistory].sort((a,b)=>a.ts-b.ts); let cum=0; const labels=sorted.map(t=>fmtDate(t.ts)); const data=sorted.map(t=>{ cum+=t.pnl; return parseFloat(cum.toFixed(2)); }); profitChart=new Chart(ctx,{ type:'line', data:{labels,datasets:[{label:'Cumulative P&L (USDT)',data,borderColor:'#3b82f6',backgroundColor:'rgba(59,130,246,.1)',fill:true,tension:.4,pointRadius:4,pointBackgroundColor:'#3b82f6'}]}, options:{...chartDefaults,responsive:true,maintainAspectRatio:false,plugins:{...chartDefaults.plugins,tooltip:{callbacks:{label:c=>`${c.raw>=0?'+':''}${c.raw} USDT`}}}} }); } function renderWinLossChart(){ const ctx=document.getElementById('winLossChart').getContext('2d'); if(winLossChart && typeof winLossChart.destroy==='function') winLossChart.destroy(); const wins=tradeHistory.filter(t=>t.win).length; const losses=tradeHistory.length-wins; if(tradeHistory.length===0){ winLossChart=null; ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); ctx.fillStyle='#64748b'; ctx.font='13px Inter'; ctx.textAlign='center'; ctx.fillText('No data',ctx.canvas.width/2,ctx.canvas.height/2); return; } const wlTotal = wins + losses; winLossChart=new Chart(ctx,{ type:'doughnut', data:{labels:['Wins','Losses'],datasets:[{data:[wins,losses],backgroundColor:['rgba(34,197,94,.8)','rgba(239,68,68,.8)'],borderColor:['#22c55e','#ef4444'],borderWidth:2}]}, options:{responsive:true,maintainAspectRatio:false,plugins:{ legend:{position:'bottom',labels:{color:'#e2e8f0',font:{size:13,weight:'700'},generateLabels:function(chart){ const data = chart.data; return data.labels.map(function(label,i){ const val = data.datasets[0].data[i]; const pct = wlTotal>0 ? (val/wlTotal*100).toFixed(1) : '0.0'; return { text: label+': '+val+' ('+pct+'%)', fillStyle: data.datasets[0].backgroundColor[i], strokeStyle: data.datasets[0].borderColor[i], lineWidth: 2, index: i }; }); }}}, tooltip:{callbacks:{label:function(c){ const pct = wlTotal>0 ? (c.raw/wlTotal*100).toFixed(1) : '0.0'; return c.label+': '+c.raw+' ('+pct+'%)'; }}} }} }); } function renderDailyChart(){ const el=document.getElementById('dailyChart'); if(!el) return; const ctx=el.getContext('2d'); if(dailyChart && typeof dailyChart.destroy==='function') dailyChart.destroy(); if(tradeHistory.length===0){ dailyChart=null; ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); ctx.fillStyle='#64748b'; ctx.font='13px Inter'; ctx.textAlign='center'; ctx.fillText('No data',ctx.canvas.width/2,ctx.canvas.height/2); return; } const byDay={}; tradeHistory.forEach(t=>{ byDay[t.date]=(byDay[t.date]||0)+t.pnl; }); const days=Object.keys(byDay).sort(); const vals=days.map(d=>parseFloat(byDay[d].toFixed(2))); dailyChart=new Chart(ctx,{ type:'bar', data:{labels:days.map(d=>fmtDate(new Date(d).getTime()+43200000)),datasets:[{label:'Daily P&L',data:vals,backgroundColor:vals.map(v=>v>=0?'rgba(34,197,94,.7)':'rgba(239,68,68,.7)'),borderColor:vals.map(v=>v>=0?'#22c55e':'#ef4444'),borderWidth:1,borderRadius:4}]}, options:{...chartDefaults,responsive:true,maintainAspectRatio:false,plugins:{...chartDefaults.plugins,tooltip:{enabled:false}}} }); } function renderInvestedChart(){ const ctx = document.getElementById('investedChart'); if(!ctx) return; const c = ctx.getContext('2d'); if(investedChart && typeof investedChart.destroy==='function') investedChart.destroy(); if(tradeHistory.length === 0){ investedChart = null; c.clearRect(0,0,ctx.width,ctx.height); c.fillStyle='#64748b'; c.font='13px Inter'; c.textAlign='center'; c.fillText('No trade data yet', ctx.width/2, ctx.height/2); return; } const sorted = [...tradeHistory].sort((a,b) => a.ts - b.ts); // Cumulative investment and cumulative P&L across all trades let cumInvest = 0, cumPnl = 0; const labels = sorted.map((t,i) => `#${i+1} ${t.pair}`); const cumInvests = sorted.map(t => { cumInvest += t.invested; return parseFloat(cumInvest.toFixed(2)); }); const cumPnls = sorted.map(t => { cumPnl += t.pnl; return parseFloat(cumPnl.toFixed(2)); }); const perInvests = sorted.map(t => parseFloat(t.invested.toFixed(2))); investedChart = new Chart(c, { type: 'bar', data: { labels, datasets: [ { label: 'Per Trade Invested (USDT)', data: perInvests, backgroundColor: 'rgba(59,130,246,.45)', borderColor: '#3b82f6', borderWidth: 1, borderRadius: 5, yAxisID: 'y', }, { label: 'Cumulative Invested (USDT)', data: cumInvests, type: 'line', borderColor: '#3b82f6', backgroundColor: 'rgba(59,130,246,.08)', fill: true, borderWidth: 2, pointRadius: 4, pointBackgroundColor: '#3b82f6', tension: 0.3, yAxisID: 'y', }, { label: 'Cumulative P&L (USDT)', data: cumPnls, type: 'line', borderColor: '#22c55e', backgroundColor: 'transparent', borderWidth: 2.5, pointRadius: 4, pointBackgroundColor: cumPnls.map(v => v >= 0 ? '#22c55e' : '#ef4444'), tension: 0.3, yAxisID: 'y2', } ] }, options: { ...chartDefaults, responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { ...chartDefaults.plugins, tooltip: { enabled: false } }, scales: { x: { ticks:{color:'#94a3b8',font:{size:11},maxRotation:30}, grid:{color:'#222b3a'} }, y: { position:'left', ticks:{color:'#3b82f6',font:{size:10},maxTicksLimit:5,callback:v=>fmt(v,0)}, grid:{color:'#222b3a'}, title:{display:true,text:'USDT',color:'#3b82f6',font:{size:10}} }, y2: { position:'right', ticks:{color:'#22c55e',font:{size:10},maxTicksLimit:5,callback:v=>(v>=0?'+':'')+fmt(v,0)}, grid:{drawOnChartArea:false}, title:{display:true,text:'P&L',color:'#22c55e',font:{size:10}} } } } }); } function renderPairProfitChart(){ const ctx = document.getElementById('pairProfitChart'); if(!ctx) return; const c = ctx.getContext('2d'); if(pairProfitChart && typeof pairProfitChart.destroy==='function') pairProfitChart.destroy(); if(tradeHistory.length === 0){ pairProfitChart = null; c.clearRect(0,0,ctx.width,ctx.height); c.fillStyle='#64748b'; c.font='13px Inter'; c.textAlign='center'; c.fillText('No trade data yet', ctx.width/2, ctx.height/2); return; } // Aggregate P&L per pair const byPair = {}; const winsByPair = {}; const tradesByPair = {}; tradeHistory.forEach(t => { byPair[t.pair] = (byPair[t.pair] || 0) + t.pnl; tradesByPair[t.pair]= (tradesByPair[t.pair] || 0) + 1; if(t.win) winsByPair[t.pair] = (winsByPair[t.pair] || 0) + 1; }); // Sort by P&L descending const pairs = Object.keys(byPair).sort((a,b) => byPair[b] - byPair[a]); const vals = pairs.map(p => parseFloat(byPair[p].toFixed(2))); const bgColors = vals.map(v => v >= 0 ? 'rgba(34,197,94,.75)' : 'rgba(239,68,68,.75)'); const bdrColors = vals.map(v => v >= 0 ? '#22c55e' : '#ef4444'); pairProfitChart = new Chart(c, { type: 'bar', data: { labels: pairs, datasets: [{ label: 'Total P&L (USDT)', data: vals, backgroundColor: bgColors, borderColor: bdrColors, borderWidth: 1.5, borderRadius: 6, }] }, options: { ...chartDefaults, responsive: true, maintainAspectRatio: false, plugins: { ...chartDefaults.plugins, tooltip: { callbacks: { label: c => { const p = pairs[c.dataIndex]; const trades = tradesByPair[p] || 0; const wins = winsByPair[p] || 0; const wr = trades ? Math.round(wins/trades*100) : 0; return [ ` P&L: ${c.raw >= 0 ? '+' : ''}${c.raw} USDT`, ` Trades: ${trades} | Win rate: ${wr}%`, ]; } } } }, scales: { x: { ticks:{color:'#94a3b8',font:{size:12},maxRotation:0}, grid:{color:'#222b3a'} }, y: { ticks:{color:'#64748b',font:{size:11},callback:v=>(v>=0?'+':'')+v+' USDT'}, grid:{color:'#222b3a'}, beginAtZero:true } } } }); } // ════════════════════════════════════════════════ // PAIR STATS // ════════════════════════════════════════════════ function showPairTradesDetail(pair){ var matched = tradeHistory.filter(function(t){ return t.pair === pair; }); var overlay = document.getElementById('winRateDetailOverlay'); var titleEl = document.getElementById('wrdTitle'); var listEl = document.getElementById('wrdList'); if(!overlay||!titleEl||!listEl) return; titleEl.textContent = pair + ' Trades (' + matched.length + ')'; listEl.innerHTML = matched.length === 0 ? '
No trades found.
' : matched.slice().sort(function(a,b){return b.ts-a.ts;}).map(function(t){ var idx = tradeHistory.findIndex(function(x){ return x.id === t.id; }); var pnlColor = t.pnl>=0 ? 'var(--green)' : 'var(--red)'; var dateStr = new Date(t.ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); return '
' + '
'+t.pair+'
'+dateStr+'
' + '
'+(t.pnl>=0?'+':'')+fmt(t.pnl,2)+' USDT
' + '
'; }).join(''); overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function renderPairStats(){ const grid=document.getElementById('pairStatsGrid'); const empty=document.getElementById('pairStatsEmpty'); if(tradeHistory.length===0){ grid.style.display='none'; empty.style.display='flex'; return; } const pairs={}; tradeHistory.forEach(t=>{ if(!pairs[t.pair]) pairs[t.pair]={pair:t.pair,trades:0,wins:0,losses:0,totalPnl:0,totalInvested:0,bestTrade:-Infinity,worstTrade:Infinity}; const p=pairs[t.pair]; p.trades++; p.totalPnl+=t.pnl; p.totalInvested+=t.invested; if(t.win) p.wins++; else p.losses++; if(t.pnl>p.bestTrade) p.bestTrade=t.pnl; if(t.pnlb.totalPnl-a.totalPnl).map(p=>{ const wr=((p.wins/p.trades)*100).toFixed(1); const isPos=p.totalPnl>=0; return `
${p.pair}
${isPos?'▲ PROFIT':'▼ LOSS'}
Total Trades${p.trades}
Wins / Losses${p.wins}W / ${p.losses}L
Win Rate${wr}%
Total P&L${isPos?'+':''}${fmt(p.totalPnl)} USDT
Best Trade+${fmt(p.bestTrade)} USDT
Worst Trade${fmt(p.worstTrade)} USDT
Avg P&L / Trade${fmt(p.totalPnl/p.trades)} USDT
`; }).join(''); } // ════════════════════════════════════════════════ // RESET // ════════════════════════════════════════════════ function resetAll(){ document.getElementById('priceDrop').value='1.7'; document.getElementById('takeProfit').value='1.7'; document.getElementById('numOrders').value='7'; setInvestValue(0); document.getElementById('multiplier').value='1.5'; const mDisp=document.getElementById('multiplierDisp'); if(mDisp) mDisp.textContent='1.5'; document.getElementById('emptyState').style.display='flex'; document.getElementById('results').style.display='none'; document.getElementById('pnlBar').style.display='none'; const ctp=document.getElementById('closeTradePanel'); if(ctp) ctp.style.display='none'; const rb=document.getElementById('activeTradeBadge'); if(rb) rb.style.display='none'; const ab=document.getElementById('allFilledBanner'); if(ab) ab.style.display='none'; currentOrders=[]; } // ════════════════════════════════════════════════ // INIT // ════════════════════════════════════════════════ // ════════════════════════════════════ // WITHDRAWAL LOGIC // ════════════════════════════════════ withdrawalTotal = parseFloat(localStorage.getItem('mg_withdrawal') || '0'); withdrawalHistory = JSON.parse(localStorage.getItem('mg_withdrawal_log') || '[]'); function updateWithdrawal(){ // live preview only — does not add yet } function addWithdrawal(){ const val = parseFloat(document.getElementById('withdrawAmount').value); if(isNaN(val) || val <= 0){ alert('Enter a valid withdrawal amount.'); return; } withdrawalTotal += val; const now=new Date(); withdrawalHistory.unshift({ amount: val, date: now.toLocaleString('en-GB'), ts: now.getTime() }); localStorage.setItem('mg_withdrawal', withdrawalTotal.toFixed(2)); localStorage.setItem('mg_withdrawal_log', JSON.stringify(withdrawalHistory)); document.getElementById('withdrawAmount').value = ''; renderWithdrawalSummary(); // Keep log open if it was already open if(withdrawLogOpen){ const log = document.getElementById('withdrawalLog'); const stats = document.getElementById('withdrawStats'); if(log) log.style.display = 'block'; if(stats) stats.style.display = 'block'; } } withdrawLogOpen = false; function toggleWithdrawLog(){ withdrawLogOpen = !withdrawLogOpen; const log = document.getElementById('withdrawalLog'); const stats = document.getElementById('withdrawStats'); const chev = document.getElementById('withdrawLogChevron'); if(log) log.style.display = withdrawLogOpen ? 'block' : 'none'; if(stats) stats.style.display = withdrawLogOpen ? 'block' : 'none'; if(chev) chev.style.transform = withdrawLogOpen ? 'rotate(180deg)' : ''; } function deleteWithdrawal(idx){ idx = parseInt(idx); if(isNaN(idx) || idx < 0 || idx >= withdrawalHistory.length){ showToast('Withdrawal not found.'); return; } var w = withdrawalHistory[idx]; showConfirm('Delete withdrawal of ' + fmt(w.amount) + ' USDT?', 'Delete', function(){ withdrawalTotal = Math.max(0, withdrawalTotal - w.amount); withdrawalHistory.splice(idx, 1); localStorage.setItem('mg_withdrawal', withdrawalTotal.toFixed(2)); localStorage.setItem('mg_withdrawal_log', JSON.stringify(withdrawalHistory)); renderWithdrawalSummary(); showToast('Withdrawal deleted.'); }); } function renderWithdrawalSummary(){ // Use total deployed (all orders) not just first order entry amount const deployedEl = document.getElementById('sumDeployed'); const invested = deployedEl && deployedEl.textContent !== '—' ? parseFloat(deployedEl.textContent.replace(/,/g,'')) || getInvestValue() : getInvestValue(); const net = invested - withdrawalTotal; // Summary cards in main grid const sumW = document.getElementById('sumWithdrawn'); const sumN = document.getElementById('sumNetInvest'); if(sumW) sumW.textContent = fmt(withdrawalTotal); if(sumN){ sumN.textContent = fmt(Math.abs(net)); sumN.className = 'card-value ' + (net >= 0 ? 'blue' : 'green'); } const sumWc = document.getElementById('sumWithdrawnChart'); const sumNc = document.getElementById('sumNetInvestChart'); if(sumWc) sumWc.textContent = fmt(withdrawalTotal); if(sumNc){ sumNc.textContent = fmt(Math.abs(net)); sumNc.className = 'card-value ' + (net >= 0 ? 'blue' : 'green'); } // Withdrawal card stats setText('totalWithdrawnSmall', fmt(withdrawalTotal)); const cardInvest = document.getElementById('withdrawCardInvest'); const cardNet = document.getElementById('withdrawCardNet'); if(cardInvest) cardInvest.textContent = fmt(invested); if(cardNet){ cardNet.textContent = fmt(Math.abs(net)); cardNet.style.color = net >= 0 ? 'var(--accent)' : 'var(--green)'; } // Note under total const note = document.getElementById('withdrawalNote'); if(note) note.textContent = withdrawalHistory.length === 0 ? '0 withdrawals' : withdrawalHistory.length + ' withdrawal' + (withdrawalHistory.length > 1 ? 's' : ''); // Log panel // ── Withdrawal Statistics ───────────────────── const now2 = new Date(); const todayStart = new Date(now2.getFullYear(), now2.getMonth(), now2.getDate()).getTime(); const weekDay = now2.getDay(); const diffMon=(weekDay+6)%7; const weekStart = new Date(now2.getFullYear(), now2.getMonth(), now2.getDate()-diffMon).getTime(); const monthStart = new Date(now2.getFullYear(), now2.getMonth(), 1).getTime(); const yearStart = new Date(now2.getFullYear(), 0, 1).getTime(); function sumPeriod(start){ return withdrawalHistory .filter(w => (w.ts||0) >= start) .reduce((s,w)=>s+w.amount, 0); } const wDay = sumPeriod(todayStart); const wWeek = sumPeriod(weekStart); const wMonth = sumPeriod(monthStart); const wYear = sumPeriod(yearStart); // Update stats cards const statsEl = document.getElementById('withdrawStats'); if(statsEl){ statsEl.innerHTML = `
Withdrawals by Period
${[ {l:'Today', v:wDay}, {l:'This Week', v:wWeek}, {l:'This Month',v:wMonth}, {l:'This Year', v:wYear}, ].map(s=>`
${s.l}
${s.v>0?'−'+fmt(s.v):'—'}
`).join('')}
`; } // ── Log panel ──────────────────────────────── const log = document.getElementById('withdrawalLog'); if(log){ if(withdrawalHistory.length === 0){ log.innerHTML = '
No withdrawals yet.
'; } else { log.innerHTML = `
Withdrawal History · ${fmt(withdrawalTotal)} USDT total
` + withdrawalHistory.map((w,i) => `
${withdrawalHistory.length - i}
${w.date}
−${fmt(w.amount)} USDT
` ).join(''); } } } // ════════════════════════════════════ // COINGECKO STATS MODAL // ════════════════════════════════════ // (duplicate removed) const CG_ID_MAP = { // (dup-body) BTC:'bitcoin',ETH:'ethereum',BNB:'binancecoin',SOL:'solana',XRP:'ripple', // (dup-body) DOGE:'dogecoin',ADA:'cardano',AVAX:'avalanche-2',TRX:'tron',DOT:'polkadot', // (dup-body) MATIC:'matic-network',LINK:'chainlink',LTC:'litecoin',BCH:'bitcoin-cash', // (dup-body) ATOM:'cosmos',UNI:'uniswap',ETC:'ethereum-classic',XLM:'stellar',APT:'aptos', // (dup-body) NEAR:'near',ICP:'internet-computer',VET:'vechain',FIL:'filecoin',HBAR:'hedera-hashgraph', // (dup-body) ALGO:'algorand',GRT:'the-graph',SAND:'the-sandbox',MANA:'decentraland',AXS:'axie-infinity', // (dup-body) THETA:'theta-token',EOS:'eos',AAVE:'aave',MKR:'maker',COMP:'compound-governance-token', // (dup-body) SNX:'havven',CRV:'curve-dao-token',SUSHI:'sushi',FTM:'fantom',RUNE:'thorchain', // (dup-body) CAKE:'pancakeswap-token',EGLD:'elrond-erd-2',ONE:'harmony',ZIL:'zilliqa',CHZ:'chiliz', // (dup-body) ENJ:'enjincoin',GALA:'gala',IMX:'immutable-x',DYDX:'dydx',CELR:'celer-network', // (dup-body) STORJ:'storj',CELO:'celo',ANKR:'ankr',OCEAN:'ocean-protocol',BAND:'band-protocol', // (dup-body) BAT:'basic-attention-token',ZRX:'0x',OMG:'omisego',NANO:'nano',DGB:'digibyte', // (dup-body) STX:'blockstack',ROSE:'oasis-network',CFX:'conflux-token',SUI:'sui',ARB:'arbitrum', // (dup-body) OP:'optimism',INJ:'injective-protocol',SEI:'sei-network',TIA:'celestia',BLUR:'blur', // (dup-body) PEPE:'pepe',WIF:'dogwifcoin',BONK:'bonk',NOT:'notcoin',ORDI:'ordi', // (dup-body) FLOKI:'floki',PNUT:'peanut-the-squirrel',POL:'polygon-ecosystem-token', // (dup-body) EIGEN:'eigenlayer',PENDLE:'pendle',ARKM:'arkham',WLD:'worldcoin-wld', // (dup-body) PYTH:'pyth-network',JUP:'jupiter-ag',RENDER:'render-token',W:'wormhole', // (dup-body) ZK:'zksync',LISTA:'lista',ZRO:'layerzero',ONDO:'ondo-finance',BEAM:'beam-2', // (dup-body) MANTA:'manta-network',STRK:'starknet',DYM:'dymension',ZETA:'zetachain', // (dup-body) METIS:'metis-token',AEVO:'aevo',ETHFI:'ether-fi',NTRN:'neutron-3', // (dup-body) ALT:'altlayer',PIXEL:'pixel',PORTAL:'portal-fantasy-token', // (dup-body) LUNC:'terra-luna',USTC:'terrausd',FLR:'flare-networks', // (dup-body) '1000SHIB':'shiba-inu',KSM:'kusama',WAVES:'waves',ZEC:'zcash',DASH:'dash', // (dup-body) XMR:'monero',NEO:'neo',IOTA:'iota',LSK:'lisk',SC:'siacoin', // (dup-body) QTUM:'qtum',NANO:'nano',YFI:'yearn-finance',BAL:'balancer', // (dup-body) LRC:'loopring',REEF:'reef',ALICE:'my-neighbor-alice',TLM:'alien-worlds', // (dup-body) AUDIO:'audius',BADGER:'badger-dao',HOOK:'hooked-protocol',KEY:'selfkey', // (dup-body) COMBO:'furucombo',PERP:'perpetual-protocol',SCRT:'secret',API3:'api3', // (dup-body) DUSK:'dusk-network',REZ:'renzo',BB:'bouncebit',OMNI:'omni-network', // (dup-body) SAGA:'saga-2',TAIKO:'taiko',BANANA:'banana-gun',NEIRO:'neiro-on-eth', // (dup-body) HMSTR:'hamster-kombat',CATI:'catizen',DOGS:'dogs-2',MAJOR:'major', // (dup-body) GOAT:'goat',MOODENG:'moo-deng',CHILLGUY:'chillguy',VIRTUAL:'virtual-protocol', // (dup-body) MOG:'mog-coin',SPX:'spx6900',G:'gravity-2',IO:'io-net',ZKJ:'polyhedra-network', // (dup-body) }; function cgFmt(n){ if(!n&&n!==0)return'—'; n=parseFloat(n); if(n>=1e9)return'$'+((n/1e9).toFixed(2))+' Billion'; if(n>=1e6)return'$'+((n/1e6).toFixed(2))+' Million'; return'$'+n.toLocaleString('en-US',{maximumFractionDigits:2}); } function cgFmtSupply(n){ if(!n&&n!==0)return'—'; n=parseFloat(n); if(n>=1e9)return(n/1e9).toFixed(1)+' Billion'; if(n>=1e6)return(n/1e6).toFixed(1)+' Million'; return n.toLocaleString('en-US',{maximumFractionDigits:0}); } function cgFmtDate(s){ if(!s)return'—'; const d=new Date(s); return d.toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'}); } function daysBetween(s){ if(!s)return''; const diff=Date.now()-new Date(s).getTime(); return'('+Math.floor(diff/86400000)+' days)'; } async function openCGModal(){ const coin=(document.getElementById('base').value||'BTC').toUpperCase(); const cgId=CG_ID_MAP[coin]||coin.toLowerCase(); document.getElementById('cgModalTitle').textContent=coin+'/USDT — Statistics'; document.getElementById('cgModalBody').innerHTML='
Fetching from CoinGecko…
'; document.getElementById('cgOverlay').style.display='flex'; document.body.style.overflow='hidden'; try{ const urls=[ `https://api.coingecko.com/api/v3/coins/${cgId}?localization=false&tickers=false&community_data=false&developer_data=false`, `https://api.allorigins.win/raw?url=${encodeURIComponent('https://api.coingecko.com/api/v3/coins/'+cgId+'?localization=false&tickers=false&community_data=false&developer_data=false')}`, `https://corsproxy.io/?url=${encodeURIComponent('https://api.coingecko.com/api/v3/coins/'+cgId+'?localization=false&tickers=false&community_data=false&developer_data=false')}`, ]; let data=null; for(const url of urls){ try{ const r=await fetch(url,{cache:'no-store'}); if(!r.ok)continue; data=await r.json(); if(data&&data.market_data)break; }catch(_){continue;} } if(!data||!data.market_data)throw new Error('no data'); renderCGModal(data); }catch(e){ document.getElementById('cgModalBody').innerHTML= `
⚠️ Could not fetch data for ${coin}. CoinGecko may have rate-limited this request. Try again in a moment. Open on CoinGecko ↗
`; } } function renderCGModal(d){ const md=d.market_data; const ath=md.ath&&md.ath.usd; const atl=md.atl&&md.atl.usd; const athChg=md.ath_change_percentage&&md.ath_change_percentage.usd; const atlChg=md.atl_change_percentage&&md.atl_change_percentage.usd; document.getElementById('cgModalBody').innerHTML=`
Statistics
Market Cap i
${cgFmt(md.market_cap&&md.market_cap.usd)}
Circulating Supply i
${cgFmtSupply(md.circulating_supply)}
Fully Diluted Valuation i
${cgFmt(md.fully_diluted_valuation&&md.fully_diluted_valuation.usd)}
Total Supply i
${cgFmtSupply(md.total_supply)}
Market Cap / FDV Ratio
${(md.market_cap&&md.market_cap.usd&&md.fully_diluted_valuation&&md.fully_diluted_valuation.usd)?(md.market_cap.usd/md.fully_diluted_valuation.usd).toFixed(2):'—'}
Max Supply i
${cgFmtSupply(md.max_supply)}
Trading Volume i
${cgFmt(md.total_volume&&md.total_volume.usd)}
Watchlist Users
${d.watchlist_portfolio_users?d.watchlist_portfolio_users.toLocaleString():'—'}
${md.total_supply?`
Outstanding Supply i
${cgFmtSupply((md.total_supply||0)-(md.circulating_supply||0))}
`:''}
Historical Data
24H High
$${md.high_24h&&md.high_24h.usd?md.high_24h.usd.toLocaleString('en-US',{maximumFractionDigits:6}):'—'}
24H Low
$${md.low_24h&&md.low_24h.usd?md.low_24h.usd.toLocaleString('en-US',{maximumFractionDigits:6}):'—'}
All Time High
$${ath?ath.toLocaleString('en-US',{maximumFractionDigits:6}):'—'} ${athChg?`▼ ${Math.abs(athChg).toFixed(1)}%`:''}
${cgFmtDate(md.ath_date&&md.ath_date.usd)} ${daysBetween(md.ath_date&&md.ath_date.usd)}
All Time Low
$${atl?atl.toLocaleString('en-US',{maximumFractionDigits:6}):'—'} ${atlChg?`▲ ${Math.abs(atlChg).toFixed(1)}%`:''}
${cgFmtDate(md.atl_date&&md.atl_date.usd)} ${daysBetween(md.atl_date&&md.atl_date.usd)}
Price Change 7D
${md.price_change_percentage_7d?(md.price_change_percentage_7d>=0?'+':'')+md.price_change_percentage_7d.toFixed(2)+'%':'—'}
Price Change 30D
${md.price_change_percentage_30d?(md.price_change_percentage_30d>=0?'+':'')+md.price_change_percentage_30d.toFixed(2)+'%':'—'}
`; } function closeCGModal(e){ if(e&&e.target!==document.getElementById('cgOverlay'))return; document.getElementById('cgOverlay').style.display='none'; document.body.style.overflow=''; } // ════════════════════════════════════════════════ // INCREASE INVESTMENT MODAL // ════════════════════════════════════════════════ function checkAllFilled(){ if(!currentOrders.length) return; const allFilled = currentOrders.every(o=>o.activated); const banner = document.getElementById('allFilledBanner'); if(banner) banner.style.display = allFilled ? 'flex' : 'none'; } function openIIModal(){ document.getElementById('iiMaxAdds').textContent = currentOrders.length; const newAddsEl = document.getElementById('iiNewAdds'); if(newAddsEl) newAddsEl.textContent = parseInt(document.getElementById('numOrders').value) || 1; const ep = document.getElementById('entryPriceMin'); if(ep && ep.value) document.getElementById('iiEntryPrice').value = ep.value; document.getElementById('iiAmount').value = ''; document.getElementById('iiPreview').textContent = 'Fill in both fields to preview the new orders.'; document.getElementById('iiOverlay').style.display = 'flex'; document.body.style.overflow = 'hidden'; setTimeout(()=>document.getElementById('iiAmount').focus(), 100); } function closeIIModal(){ document.getElementById('iiOverlay').style.display = 'none'; document.body.style.overflow = ''; } function previewII(){ const amount = parseFloat(document.getElementById('iiAmount').value); const entryP = parseFloat(document.getElementById('iiEntryPrice').value); const preview = document.getElementById('iiPreview'); if(!amount||!entryP||isNaN(amount)||isNaN(entryP)||amount<=0||entryP<=0){ preview.textContent='Fill in both fields to preview the new order.'; return; } const takeProfit = parseFloat(document.getElementById('takeProfit').value)/100; // Single order calculation const qty = amount / entryP; const last = currentOrders.length > 0 ? currentOrders[currentOrders.length-1] : null; const cumQty = (last ? last.execQty : 0) + qty; const cumAmt = (last ? last.execAmount: 0) + amount; const newAvg = cumAmt / cumQty; const tpPrice = newAvg * (1 + takeProfit); const profit = (tpPrice - newAvg) * cumQty; preview.innerHTML=`
Order amount: ${fmt(amount)} USDT
Entry price: ${fmt(entryP,2)} USDT
Qty bought: ${fmt(qty,0)}
New avg price: ${fmt(newAvg,2)} USDT
Total invested: ${fmt(cumAmt)} USDT
New TP price: ${fmt(tpPrice,2)} USDT
If TP hit → +${fmt(profit)} USDT (+${fmt(takeProfit*100,2)}%)
`; } function confirmII(){ const amount = parseFloat(document.getElementById('iiAmount').value); const entryP = parseFloat(document.getElementById('iiEntryPrice').value); if(!amount||!entryP||isNaN(amount)||isNaN(entryP)||amount<=0||entryP<=0){ alert('Please fill in both the investment amount and entry price.'); return; } const takeProfit = parseFloat(document.getElementById('takeProfit').value)/100; // Add ONE single order at the given entry price with the full amount const prev = currentOrders.length > 0 ? currentOrders[currentOrders.length-1] : null; const qty = amount / entryP; const cumQty = (prev ? prev.execQty : 0) + qty; const cumAmt = (prev ? prev.execAmount : 0) + amount; const avgPrice = cumAmt / cumQty; const tpPrice = avgPrice * (1 + takeProfit); currentOrders.push({ price: entryP, amount: amount, avgPrice, execQty: cumQty, execAmount: cumAmt, tpPrice, activated: false, sellPrice: tpPrice, isExtra: true, }); closeIIModal(); // Update summary cards const last = currentOrders[currentOrders.length-1]; document.getElementById('sumOrders').textContent = currentOrders.length; document.getElementById('sumDeployed').textContent = fmt(last.execAmount, 0); document.getElementById('sumQty').textContent = fmt(last.execQty, 0); document.getElementById('sumAvgPrice').textContent = fmt(last.avgPrice); document.getElementById('sumTP').textContent = fmt(last.tpPrice); const banner = document.getElementById('allFilledBanner'); if(banner) banner.style.display = 'none'; renderTable(); showToast('✓ Order added — ' + fmt(amount) + ' USDT at ' + fmt(entryP, 4)); } // ════════════════════════════════════════════════ // PROFILE // ════════════════════════════════════════════════ // (duplicate removed) const COUNTRIES = [ // (dup-body) {name:"Afghanistan",code:"AF",dial:"+93"},{name:"Albania",code:"AL",dial:"+355"},{name:"Algeria",code:"DZ",dial:"+213"}, // (dup-body) {name:"Argentina",code:"AR",dial:"+54"},{name:"Australia",code:"AU",dial:"+61"},{name:"Austria",code:"AT",dial:"+43"}, // (dup-body) {name:"Bahrain",code:"BH",dial:"+973"},{name:"Bangladesh",code:"BD",dial:"+880"},{name:"Belgium",code:"BE",dial:"+32"}, // (dup-body) {name:"Brazil",code:"BR",dial:"+55"},{name:"Canada",code:"CA",dial:"+1"},{name:"Chile",code:"CL",dial:"+56"}, // (dup-body) {name:"China",code:"CN",dial:"+86"},{name:"Colombia",code:"CO",dial:"+57"},{name:"Croatia",code:"HR",dial:"+385"}, // (dup-body) {name:"Czech Republic",code:"CZ",dial:"+420"},{name:"Denmark",code:"DK",dial:"+45"},{name:"Egypt",code:"EG",dial:"+20"}, // (dup-body) {name:"Finland",code:"FI",dial:"+358"},{name:"France",code:"FR",dial:"+33"},{name:"Germany",code:"DE",dial:"+49"}, // (dup-body) {name:"Ghana",code:"GH",dial:"+233"},{name:"Greece",code:"GR",dial:"+30"},{name:"Hong Kong",code:"HK",dial:"+852"}, // (dup-body) {name:"Hungary",code:"HU",dial:"+36"},{name:"India",code:"IN",dial:"+91"},{name:"Indonesia",code:"ID",dial:"+62"}, // (dup-body) {name:"Iran",code:"IR",dial:"+98"},{name:"Iraq",code:"IQ",dial:"+964"},{name:"Ireland",code:"IE",dial:"+353"}, // (dup-body) {name:"Israel",code:"IL",dial:"+972"},{name:"Italy",code:"IT",dial:"+39"},{name:"Japan",code:"JP",dial:"+81"}, // (dup-body) {name:"Jordan",code:"JO",dial:"+962"},{name:"Kenya",code:"KE",dial:"+254"},{name:"Kuwait",code:"KW",dial:"+965"}, // (dup-body) {name:"Lebanon",code:"LB",dial:"+961"},{name:"Libya",code:"LY",dial:"+218"},{name:"Malaysia",code:"MY",dial:"+60"}, // (dup-body) {name:"Mexico",code:"MX",dial:"+52"},{name:"Morocco",code:"MA",dial:"+212"},{name:"Netherlands",code:"NL",dial:"+31"}, // (dup-body) {name:"New Zealand",code:"NZ",dial:"+64"},{name:"Nigeria",code:"NG",dial:"+234"},{name:"Norway",code:"NO",dial:"+47"}, // (dup-body) {name:"Oman",code:"OM",dial:"+968"},{name:"Pakistan",code:"PK",dial:"+92"},{name:"Palestine",code:"PS",dial:"+970"}, // (dup-body) {name:"Peru",code:"PE",dial:"+51"},{name:"Philippines",code:"PH",dial:"+63"},{name:"Poland",code:"PL",dial:"+48"}, // (dup-body) {name:"Portugal",code:"PT",dial:"+351"},{name:"Qatar",code:"QA",dial:"+974"},{name:"Romania",code:"RO",dial:"+40"}, // (dup-body) {name:"Russia",code:"RU",dial:"+7"},{name:"Saudi Arabia",code:"SA",dial:"+966"},{name:"Singapore",code:"SG",dial:"+65"}, // (dup-body) {name:"South Africa",code:"ZA",dial:"+27"},{name:"South Korea",code:"KR",dial:"+82"},{name:"Spain",code:"ES",dial:"+34"}, // (dup-body) {name:"Sudan",code:"SD",dial:"+249"},{name:"Sweden",code:"SE",dial:"+46"},{name:"Switzerland",code:"CH",dial:"+41"}, // (dup-body) {name:"Syria",code:"SY",dial:"+963"},{name:"Taiwan",code:"TW",dial:"+886"},{name:"Thailand",code:"TH",dial:"+66"}, // (dup-body) {name:"Tunisia",code:"TN",dial:"+216"},{name:"Turkey",code:"TR",dial:"+90"},{name:"UAE",code:"AE",dial:"+971"}, // (dup-body) {name:"UK",code:"GB",dial:"+44"},{name:"Ukraine",code:"UA",dial:"+380"},{name:"USA",code:"US",dial:"+1"}, // (dup-body) {name:"Vietnam",code:"VN",dial:"+84"},{name:"Yemen",code:"YE",dial:"+967"}, // (dup-body) ]; function initProfileDropdowns(){ const natSel=document.getElementById('profileNationality'); const dialSel=document.getElementById('profileDialCode'); COUNTRIES.forEach(c=>{ natSel.innerHTML+=``; dialSel.innerHTML+=``; }); } function openProfile(){ loadProfileData(); renderProfileArcadeSummary(); document.getElementById('profileOverlay').style.display='flex'; document.body.style.overflow='hidden'; } function closeProfile(){ document.getElementById('profileOverlay').style.display='none'; document.body.style.overflow=''; } function loadProfileData(){ const p=JSON.parse(localStorage.getItem('mg_profile')||'{}'); if(p.name) document.getElementById('profileName').value=p.name; if(p.nat) document.getElementById('profileNationality').value=p.nat; if(p.dob) document.getElementById('profileDob').value=p.dob; if(p.email) document.getElementById('profileEmail').value=p.email; if(p.dial) document.getElementById('profileDialCode').value=p.dial; if(p.mobile) document.getElementById('profileMobile').value=p.mobile; if(p.avatar){ document.getElementById('profileAvatarImg').src=p.avatar; document.getElementById('profileAvatarImg').style.display='block'; document.getElementById('profileAvatarEmoji').style.display='none'; document.getElementById('profileBtnContent').innerHTML=``; } if(p.name){ const initials=p.name.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2); if(!p.avatar){ document.getElementById('profileAvatarEmoji').textContent=initials||'👤'; document.getElementById('profileBtnContent').textContent=initials||'👤'; } } } function saveProfile(){ const p={ name: document.getElementById('profileName').value.trim(), nat: document.getElementById('profileNationality').value, dob: document.getElementById('profileDob').value, email: document.getElementById('profileEmail').value.trim(), dial: document.getElementById('profileDialCode').value, mobile: document.getElementById('profileMobile').value.trim(), avatar: localStorage.getItem('mg_avatar')||'', }; localStorage.setItem('mg_profile',JSON.stringify(p)); // Update header button if(p.name){ const initials=p.name.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2); if(!p.avatar) document.getElementById('profileBtnContent').textContent=initials||'👤'; } closeProfile(); // Show toast showToast('Profile saved!'); } function handleAvatarUpload(e){ const files = e.target.files || (e.dataTransfer&&e.dataTransfer.files); const file = files && files[0]; if(!file){showToast('No file selected');return;} if(!file.type.startsWith('image/')){alert('Please select an image file (JPG, PNG, etc.)');return;} const reader=new FileReader(); reader.onerror=()=>{showToast('Could not read image. Try a different file.');}; reader.onloadend=function(){ const data=reader.result; if(!data){showToast('Failed to load image.');return;} try{localStorage.setItem('mg_avatar',data);}catch(e){console.warn('Avatar storage failed:',e);} const imgEl=document.getElementById('profileAvatarImg'); const emoEl=document.getElementById('profileAvatarEmoji'); const btnEl=document.getElementById('profileBtnContent'); if(imgEl){ imgEl.src=data; imgEl.style.cssText='display:block;position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:50%;'; } if(emoEl)emoEl.style.display='none'; if(btnEl)btnEl.innerHTML=``; showToast('✓ Photo updated!'); }; reader.readAsDataURL(file); } function showToast(msg){ const t=document.createElement('div'); t.textContent=msg; t.style.cssText='position:fixed;bottom:28px;left:50%;transform:translateX(-50%);background:var(--green);color:#fff;padding:10px 22px;border-radius:30px;font-size:13px;font-weight:600;z-index:9999;font-family:var(--sans);box-shadow:0 4px 16px rgba(0,0,0,.4);animation:fadeIn .2s ease;'; document.body.appendChild(t); setTimeout(()=>t.remove(),2500); } // ════════════════════════════════════════════════ // SETTINGS // ════════════════════════════════════════════════ // (duplicate removed) const SETTINGS_KEY = 'mg_settings'; function loadSettings(){ const s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}'); // Theme const theme = s.theme || 'dark'; applyTheme(theme); document.getElementById('themeDark').classList.toggle('active', theme==='dark'); document.getElementById('themeLight').classList.toggle('active', theme==='light'); // Layout const view = s.view || 'desktop'; applyView(view); document.getElementById('viewMobile').classList.toggle('active', view==='mobile'); document.getElementById('viewDesktop').classList.toggle('active', view==='desktop'); // Currency if(s.currency) document.getElementById('settingsCurrency').value = s.currency; // Sound if(s.sound !== undefined) document.getElementById('settingsSound').checked = s.sound; // Alerts if(s.alerts !== undefined) document.getElementById('settingsAlerts').checked = s.alerts; // Language const lang = s.lang || 'en'; applyLang(lang); document.getElementById('langEN').classList.toggle('active', lang==='en'); document.getElementById('langAR').classList.toggle('active', lang==='ar'); // Fee settings if(s.makerFee !== undefined){ var mf=document.getElementById('makerFee'); if(mf)mf.value=s.makerFee; } if(s.takerFee !== undefined){ var tf=document.getElementById('takerFee'); if(tf)tf.value=s.takerFee; } if(s.kcsDiscount !== undefined){ var kf=document.getElementById('kcsDiscount'); if(kf)kf.checked=s.kcsDiscount; } // Exchange + VIP tier var exch = s.exchange || 'binance'; var exchSel = document.getElementById('exchangeSelect'); if(exchSel) exchSel.value = exch; if(typeof populateVipTiers === 'function') populateVipTiers(exch); if(s.aedRate !== undefined){ var arEl=document.getElementById('aedRate'); if(arEl)arEl.value=s.aedRate; } var accentTheme = s.accentTheme || 'orange'; applyAccentTheme(accentTheme); var ab=document.getElementById('accentBlue'); if(ab)ab.classList.toggle('active',accentTheme==='blue'); var ao=document.getElementById('accentOrange'); if(ao)ao.classList.toggle('active',accentTheme==='orange'); if(s.vipTierIdx !== undefined){ var vSel = document.getElementById('vipTierSelect'); if(vSel) vSel.value = s.vipTierIdx; } } function saveSetting(key, val){ const s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}'); s[key] = val; localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } function openSettings(){ loadSettings(); document.getElementById('settingsOverlay').style.display = 'flex'; document.body.style.overflow = 'hidden'; } function closeSettings(){ document.getElementById('settingsOverlay').style.display = 'none'; document.body.style.overflow = ''; } function setTheme(t){ saveSetting('theme', t); applyTheme(t); document.getElementById('themeDark').classList.toggle('active', t==='dark'); document.getElementById('themeLight').classList.toggle('active', t==='light'); } function applyTheme(t){ document.body.classList.toggle('theme-light', t==='light'); } function setView(v){ saveSetting('view', v); applyView(v); document.getElementById('viewMobile').classList.toggle('active', v==='mobile'); document.getElementById('viewDesktop').classList.toggle('active', v==='desktop'); } function applyView(v){ const sidebar = document.querySelector('.sidebar'); const main = document.querySelector('.main'); if(v === 'mobile'){ if(sidebar) sidebar.style.cssText='width:100%;border-right:none;border-bottom:1px solid var(--border);'; if(main) main.style.flexDirection='column'; } else { if(sidebar) sidebar.style.cssText=''; if(main) main.style.flexDirection=''; } } function setLang(l){ saveSetting('lang', l); applyLang(l); document.getElementById('langEN').classList.toggle('active', l==='en'); document.getElementById('langAR').classList.toggle('active', l==='ar'); } function applyCurrency(){ const c = document.getElementById('settingsCurrency').value; saveSetting('currency', c); showToast('Currency set to ' + c); } function confirmClearHistory(){ closeSettings(); setTimeout(()=>clearHistory(),100); } function exportHistory(){ exportExcel(); } // ── Excel export (CSV that Excel opens natively) ────────── function exportExcel(){ if(tradeHistory.length===0){showToast('No trade history to export.');return;} var headers=['Date','Pair','Orders Filled','Avg Buy Price','Sell Price','Qty','Invested (USDT)','P&L (USDT)','P&L %','Result']; var rows=tradeHistory.map(function(t){ return [fmtDate(t.ts),t.pair,t.ordersCount,fmt(t.avgBuy),fmt(t.sellPrice),fmt(t.qty,2),fmt(t.invested),fmt(t.pnl),fmt(t.pnlPct,2)+'%',t.win?'WIN':'LOSS']; }); var allRows=[headers].concat(rows); var csvLines=allRows.map(function(row){ return row.map(function(cell){ var s=String(cell).replace(/"/g,'""'); return '"'+s+'"'; }).join(','); }); var bom='\uFEFF'; var csvContent=bom+csvLines.join('\r\n'); var blob=new Blob([csvContent],{type:'text/csv;charset=utf-8;'}); var a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='martingale_history_'+new Date().toISOString().split('T')[0]+'.csv'; a.click(); showToast('\u2713 Exported to Excel (CSV)'); } // ── PDF export (print-to-PDF via window.print with styled table) ── // Play sound on trade close if enabled function playTradeCloseSound(){ const s = JSON.parse(localStorage.getItem(SETTINGS_KEY) || '{}'); if(s.sound === false) return; try{ const ctx = new (window.AudioContext || window.webkitAudioContext)(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); osc.type = 'sine'; osc.frequency.setValueAtTime(523, ctx.currentTime); osc.frequency.setValueAtTime(659, ctx.currentTime + 0.1); osc.frequency.setValueAtTime(784, ctx.currentTime + 0.2); gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5); osc.start(ctx.currentTime); osc.stop(ctx.currentTime + 0.5); }catch(_){} } // ════════════════════════════════════════════════ // P&L PERIOD PERFORMANCE // ════════════════════════════════════════════════ pnlViewMode = 'usdt'; // 'usdt' | 'pct' | 'trades' function setPnlView(mode, btn){ pnlViewMode = mode; document.querySelectorAll('.pnl-period-tab').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); renderPnlPeriods(); } function renderPnlPeriods(){ const now = new Date(); const todayStr = now.toISOString().split('T')[0]; // Week start (Monday) const dayOfWeek = now.getDay(); // 0=Sun const diffToMon = (dayOfWeek + 6) % 7; const weekStart = new Date(now); weekStart.setDate(now.getDate() - diffToMon); weekStart.setHours(0,0,0,0); // Month start const monthStart = new Date(now.getFullYear(), now.getMonth(), 1); // Year start const yearStart = new Date(now.getFullYear(), 0, 1); function calcPeriod(startDate){ const trades = tradeHistory.filter(t => new Date(t.ts) >= startDate); const pnl = trades.reduce((s,t)=>s+t.pnl, 0); const pct = trades.length ? trades.reduce((s,t)=>s+t.pnlPct, 0) / trades.length : 0; const wins = trades.filter(t=>t.win).length; return { pnl, pct, count: trades.length, wins, losses: trades.length - wins }; } const day = calcPeriod(new Date(todayStr+'T00:00:00')); const week = calcPeriod(weekStart); const month = calcPeriod(monthStart); const year = calcPeriod(yearStart); function renderCell(prefix, data){ const valEl = document.getElementById(prefix+'Val'); const subEl = document.getElementById(prefix+'Sub'); const badgeEl = document.getElementById(prefix+'Badge'); if(!valEl) return; let valStr, cls; if(pnlViewMode === 'usdt'){ valStr = (data.pnl >= 0 ? '+' : '') + fmt(data.pnl); cls = data.pnl > 0 ? 'pos' : data.pnl < 0 ? 'neg' : 'zero'; } else if(pnlViewMode === 'pct'){ valStr = (data.pct >= 0 ? '+' : '') + fmt(data.pct,2) + '%'; cls = data.pct > 0 ? 'pos' : data.pct < 0 ? 'neg' : 'zero'; } else { valStr = data.count + ' trade' + (data.count!==1?'s':''); cls = data.count > 0 ? 'pos' : 'zero'; } valEl.textContent = data.count === 0 ? '—' : valStr; valEl.className = 'pnl-period-val ' + cls; if(subEl){ if(data.count === 0){ subEl.textContent = 'No trades'; } else if(pnlViewMode === 'trades'){ subEl.textContent = data.wins+'W / '+data.losses+'L'; } else { subEl.textContent = data.count + ' trade' + (data.count!==1?'s':'') + ' · ' + data.wins+'W/'+data.losses+'L'; } } if(badgeEl){ if(data.count === 0){ badgeEl.textContent='—'; badgeEl.className='pnl-period-badge zero'; } else { const wr = Math.round(data.wins / data.count * 100); badgeEl.textContent = wr + '% win'; badgeEl.className = 'pnl-period-badge ' + (wr >= 50 ? 'pos' : 'neg'); } } } renderCell('pnlDay', day); renderCell('pnlWeek', week); renderCell('pnlMonth', month); renderCell('pnlYear', year); } function copyVal(val){ navigator.clipboard.writeText(val).then(()=>{ showToast('Copied: ' + val); }).catch(()=>{ // Fallback for environments where clipboard API is blocked const el = document.createElement('textarea'); el.value = val; el.style.cssText = 'position:fixed;opacity:0;'; document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); showToast('Copied: ' + val); }); } // ════════════════════════════════════════════════ // TRADE HISTORY SELECTION // ════════════════════════════════════════════════ selectedTrades = new Set(); function toggleTradeSelect(idx){ if(selectedTrades.has(idx)) selectedTrades.delete(idx); else selectedTrades.add(idx); const cb = document.getElementById('hcb_'+idx); const tr = cb && cb.closest('tr'); if(cb) cb.classList.toggle('checked', selectedTrades.has(idx)); if(tr) tr.classList.toggle('hist-selected', selectedTrades.has(idx)); updateSelectionUI(); } function toggleSelectAll(){ if(selectedTrades.size === tradeHistory.length){ selectedTrades.clear(); } else { tradeHistory.forEach((_,i)=>selectedTrades.add(i)); } renderSelectionState(); } function clearSelection(){ selectedTrades.clear(); renderSelectionState(); } function renderSelectionState(){ tradeHistory.forEach((_,i)=>{ const cb = document.getElementById('hcb_'+i); const tr = cb && cb.closest('tr'); if(cb){ cb.className = 'hist-cb' + (selectedTrades.has(i)?' checked':''); } if(tr){ tr.classList.toggle('hist-selected', selectedTrades.has(i)); } }); updateSelectionUI(); } function updateSelectionUI(){ const n = selectedTrades.size; const total = tradeHistory.length; const bar = document.getElementById('selActionBar'); const countEl = document.getElementById('selCount'); const allCb = document.getElementById('histSelectAll'); if(bar) bar.classList.toggle('visible', n > 0); if(countEl) countEl.textContent = n; if(allCb){ allCb.className = 'hist-cb' + (n===0?'':(n===total?' checked':' half')); } } function getSelectedTrades(){ return [...selectedTrades].map(i=>tradeHistory[i]).filter(Boolean); } function deleteSelected(){ const n = selectedTrades.size; if(n===0){ showToast('No trades selected.'); return; } showConfirm('Delete ' + n + ' selected trade' + (n>1?'s':'') + '? This cannot be undone.', 'Delete', function(){ const indices = [...selectedTrades].map(Number).sort((a,b)=>b-a); indices.forEach(i => { if(i >= 0 && i < tradeHistory.length) tradeHistory.splice(i, 1); }); localStorage.setItem('mg_history', JSON.stringify(tradeHistory)); selectedTrades.clear(); renderHistory(); renderPnlPeriods(); showToast(n + ' trade' + (n>1?'s':'') + ' deleted.'); }); } function exportSelectedExcel(){ var sel=getSelectedTrades(); if(sel.length===0){showToast('No trades selected.');return;} var headers=['No.','Date','Pair','Orders Filled','Avg Buy Price','Sell Price','Qty','Invested (USDT)','P&L (USDT)','P&L %','Result']; var rows=sel.map(function(t,i){ return [i+1,fmtDate(t.ts),t.pair,t.ordersCount,fmt(t.avgBuy),fmt(t.sellPrice),fmt(t.qty,2),fmt(t.invested),fmt(t.pnl),fmt(t.pnlPct,2)+'%',t.win?'WIN':'LOSS']; }); var allRows=[headers].concat(rows); var csvLines=allRows.map(function(row){ return row.map(function(cell){ var s=String(cell).replace(/"/g,'""'); return '"'+s+'"'; }).join(','); }); var CRLF='\r\n'; var bom='\uFEFF'; var csvContent=bom+csvLines.join(CRLF); var blob=new Blob([csvContent],{type:'text/csv;charset=utf-8;'}); var a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='selected_trades_'+new Date().toISOString().split('T')[0]+'.csv'; a.click(); showToast('\u2713 '+sel.length+' trades exported to Excel'); } // ════════════════════════════════════════════════ // AI CHAT // ════════════════════════════════════════════════ aiThinking = false; function openAIChat(){ document.getElementById('aiOverlay').style.display='flex'; document.body.style.overflow='hidden'; setTimeout(()=>document.getElementById('aiInput').focus(),100); } function closeAIChat(){ document.getElementById('aiOverlay').style.display='none'; document.body.style.overflow=''; } function aiQuick(q){ document.getElementById('aiInput').value=q; document.getElementById('aiQuickBtns').style.display='none'; sendAIMessage(); } // ── Text-to-Speech ───────────────────────────────────────── var ttsActive = null; // current SpeechSynthesisUtterance function speakText(text, btn){ if(!window.speechSynthesis){ showToast('Text-to-speech not supported on this browser.'); return; } // Stop if already speaking this message if(window.speechSynthesis.speaking){ window.speechSynthesis.cancel(); document.querySelectorAll('.tts-btn').forEach(b=>{ b.textContent='🔊'; b.classList.remove('speaking'); }); if(ttsActive && ttsActive._btn === btn){ ttsActive=null; return; } } // Strip HTML tags for clean speech var clean = text.replace(/<[^>]+>/g,' ').replace(/\*\*/g,'').replace(/\*/g,'').replace(/#+/g,'').trim(); var utt = new SpeechSynthesisUtterance(clean); utt.lang = 'en-US'; utt.rate = 1.0; utt.pitch = 1.0; utt._btn = btn; ttsActive = utt; if(btn){ btn.textContent='⏹'; btn.classList.add('speaking'); } utt.onend = utt.onerror = function(){ ttsActive = null; if(btn){ btn.textContent='🔊'; btn.classList.remove('speaking'); } }; window.speechSynthesis.speak(utt); } function addAIMessage(role, text){ const msgs = document.getElementById('aiMessages'); const div = document.createElement('div'); div.className = 'ai-msg '+role; if(role==='ai'){ var nl = String.fromCharCode(10); var cleanText = text.split(nl).join('
'); var msgId = 'aimsg_'+Date.now(); div.innerHTML = '
' + '✦ Martingale AI' + '' + '
' + cleanText; div.dataset.raw = text; // store raw text for TTS } else { div.textContent = text; } msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; return div; } function addThinking(){ const msgs = document.getElementById('aiMessages'); const div = document.createElement('div'); div.className='ai-thinking'; div.id='aiThinkingDot'; div.innerHTML=''; msgs.appendChild(div); msgs.scrollTop=msgs.scrollHeight; } function removeThinking(){ const dot=document.getElementById('aiThinkingDot'); if(dot) dot.remove(); } // ════ AI COIN CARD DETECTION ════ function detectCoinMentions(text){ const upper = text.toUpperCase(); const found = []; // Match whole-word coin symbols from the known COINS list (2-10 chars) const words = upper.match(/\b[A-Z0-9]{2,10}\b/g) || []; words.forEach(w=>{ if(COINS.indexOf(w) >= 0 && found.indexOf(w) < 0) found.push(w); }); return found.slice(0, 2); // max 2 coins per message to avoid spam } async function fetchAndShowCoinCard(coin){ const cardId = 'aiCoinCard_' + coin + '_' + Date.now(); const msgs = document.getElementById('aiMessages'); const placeholder = document.createElement('div'); placeholder.className = 'ai-msg ai'; placeholder.id = cardId; placeholder.innerHTML = '
✦ Martingale AI
' + '
Fetching '+coin+' data…
'; msgs.appendChild(placeholder); msgs.scrollTop = msgs.scrollHeight; let cardData = null; let dataSource = null; // Try CoinGecko first for full stats try { const cgId = CG_ID_MAP[coin] || coin.toLowerCase(); cardData = await fetchFromCoinGecko(coin, cgId); dataSource = 'coingecko'; } catch(e1){ // Fallback to exchange ticker for basic price/24h data try { cardData = await fetchFromBinance(coin); dataSource = 'exchange'; } catch(e2){ try { cardData = await fetchFromKuCoin(coin); dataSource = 'exchange'; } catch(e3){ try { cardData = await fetchFromOKX(coin); dataSource = 'exchange'; } catch(e4){ cardData = null; } } } } const el = document.getElementById(cardId); if(!el) return null; // user may have closed chat if(!cardData){ el.innerHTML = '
✦ Martingale AI
' + '
Could not fetch live data for '+coin+'.
'; return null; } if(dataSource === 'coingecko'){ const md = cardData.market_data; const price = md.current_price&&md.current_price.usd; const chg24h = md.price_change_percentage_24h; const high24h = md.high_24h&&md.high_24h.usd; const low24h = md.low_24h&&md.low_24h.usd; const ath = md.ath&&md.ath.usd; const athChg = md.ath_change_percentage&&md.ath_change_percentage.usd; const mcap = md.market_cap&&md.market_cap.usd; el.innerHTML = '
✦ Martingale AI
' + '
' + '
' + '
'+coin+'/USDT
' + '
📡 CoinGecko
' + '
' + '
$'+fmt(price, price<1?6:2)+'
' + '
'+(chg24h>=0?'+':'')+fmt(chg24h,2)+'% (24h)
' + '
' + '
24h High: $'+fmt(high24h, high24h<1?6:2)+'
' + '
24h Low: $'+fmt(low24h, low24h<1?6:2)+'
' + '
Market Cap: $'+cgFmt(mcap)+'
' + '
ATH: $'+fmt(ath, ath<1?6:2)+' '+fmt(athChg,1)+'%
' + '
' + '
'; return { coin, price, chg24h, mcap, high24h, low24h, ath }; } else { // Exchange data (Binance/KuCoin/OKX) — simpler card el.innerHTML = '
✦ Martingale AI
' + '
' + '
' + '
'+coin+'/USDT
' + '
📡 '+cardData.source+'
' + '
' + '
$'+fmt(cardData.price, cardData.price<1?6:2)+'
' + '
'+(cardData.change24h>=0?'+':'')+fmt(cardData.change24h,2)+'% (24h)
' + '
' + '
24h High: $'+fmt(cardData.high24h, cardData.high24h<1?6:2)+'
' + '
24h Low: $'+fmt(cardData.low24h, cardData.low24h<1?6:2)+'
' + '
24h Volume: $'+cgFmt(cardData.volume24h)+'
' + '
' + '
'; return { coin, price: cardData.price, chg24h: cardData.change24h, high24h: cardData.high24h, low24h: cardData.low24h }; } } async function sendAIMessage(){ if(aiThinking) return; const input=document.getElementById('aiInput'); const btn=document.getElementById('aiSendBtn'); const text=input.value.trim(); if(!text) return; input.value=''; input.style.height=''; addAIMessage('user', text); aiThinking=true; btn.disabled=true; // ── Detect coin mentions and show live data cards first ── const mentionedCoins = detectCoinMentions(text); const coinDataResults = []; for(const c of mentionedCoins){ const result = await fetchAndShowCoinCard(c); if(result) coinDataResults.push(result); } addThinking(); // Build context from current strategy const base = document.getElementById('base')&&document.getElementById('base').value||'BTC'; const priceDrop = document.getElementById('priceDrop')&&document.getElementById('priceDrop').value||'—'; const tp = document.getElementById('takeProfit')&&document.getElementById('takeProfit').value||'—'; const adds = document.getElementById('numOrders')&&document.getElementById('numOrders').value||'—'; const mult = document.getElementById('multiplier')&&document.getElementById('multiplier').value||'—'; const invest = document.getElementById('initAmount')&&document.getElementById('initAmount').value||'—'; let coinContext = ''; if(coinDataResults.length > 0){ coinContext = '\n\nLive market data just fetched for the coins mentioned:\n' + coinDataResults.map(r=> r.coin+': $'+r.price+' ('+(r.chg24h>=0?'+':'')+r.chg24h.toFixed(2)+'% 24h)'+ (r.mcap?', Market Cap: $'+r.mcap.toLocaleString():'')+ (r.ath?', ATH: $'+r.ath:'') ).join('\n') + '\nReference this data naturally in your response if relevant to the question.'; } const systemPrompt = `You are a professional Martingale crypto trading assistant embedded in a trading dashboard. The user is currently running a Martingale bot with these settings: - Pair: ${base}/USDT - Price Drop to Add: ${priceDrop}% - Take Profit: ${tp}% - Max Adds: ${adds} - Position Size Multiplier: ${mult}x - Total Investment: ${invest} USDT ${coinContext} Be concise, practical, and insightful. Use bullet points when helpful. Focus on actionable advice. If asked about the strategy, explain based on their current settings. Keep responses under 200 words. If live coin data was provided above, you don't need to repeat the numbers verbatim (they're already shown in a card) — just give your analysis/take.`; try { const response = await fetch('https://api.anthropic.com/v1/messages', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'claude-sonnet-4-6', max_tokens: 1000, system: systemPrompt, messages: [{ role: 'user', content: text }] }) }); const data = await response.json(); removeThinking(); if(data.content && data.content[0] && data.content[0].text){ addAIMessage('ai', data.content[0].text); } else if(data.error){ var errType = (data.error && data.error.type) || data.type || ''; var errMsg = (data.error && data.error.message) || ''; if(errType.includes('exceeded') || errMsg.toLowerCase().includes('limit') || errMsg.toLowerCase().includes('quota')){ addAIMessage('ai', '⏳ Usage limit reached. AI will be available again in a few hours.'); } else if(errType.includes('overloaded') || errMsg.toLowerCase().includes('overload')){ addAIMessage('ai', '🔄 AI is busy. Please try again in a moment.'); } else { addAIMessage('ai', '⚠️ AI unavailable right now. Please try again later.'); } } else { addAIMessage('ai', '⚠️ Could not get a response. Please try again.'); } } catch(e) { removeThinking(); addAIMessage('ai', '🌐 Connection error. Check your internet and try again.'); } aiThinking=false; btn.disabled=false; document.getElementById('aiInput').focus(); } // ════════════════════════════════════════════════ // MULTI-STRATEGY MANAGER // ════════════════════════════════════════════════ // (duplicate removed) const STRAT_KEY = 'mg_strategies'; // (duplicate removed) const STRAT_ACTIVE_KEY = 'mg_active_strat'; strategies = JSON.parse(localStorage.getItem(STRAT_KEY) || '[]'); activeStratId = localStorage.getItem(STRAT_ACTIVE_KEY) || null; function getStratState(){ return { id: activeStratId, name: (strategies.find(s=>s.id===activeStratId)||{}).name || 'Strategy 1', base: document.getElementById('base').value || 'BTC', priceDrop: document.getElementById('priceDrop').value, takeProfit: document.getElementById('takeProfit').value, numOrders: document.getElementById('numOrders').value, initAmount: document.getElementById('initAmount').value, multiplier: document.getElementById('multiplier').value, entryPrice: document.getElementById('entryPriceMin') ? document.getElementById('entryPriceMin').value : '', orders: JSON.parse(JSON.stringify(currentOrders)), ts: Date.now(), }; } function applyStratState(s){ document.getElementById('base').value = s.base || 'BTC'; document.getElementById('coinSearch').value = s.base || 'BTC'; if(document.getElementById('priceDrop')) document.getElementById('priceDrop').value = s.priceDrop || 1.7; if(document.getElementById('takeProfit')) document.getElementById('takeProfit').value = s.takeProfit || 1.7; if(document.getElementById('numOrders')) document.getElementById('numOrders').value = s.numOrders || 7; if(document.getElementById('multiplier')) document.getElementById('multiplier').value = s.multiplier || 1.5; // Investment const initEl = document.getElementById('initAmount'); if(initEl){ initEl.value = s.initAmount || '100'; } // Entry price if(s.entryPrice && document.getElementById('entryPriceMin')) document.getElementById('entryPriceMin').value = s.entryPrice; // Restore orders currentOrders = s.orders || []; if(currentOrders.length > 0){ renderTable(); document.getElementById('emptyState').style.display='none'; document.getElementById('results').style.display='block'; } else { document.getElementById('emptyState').style.display='flex'; document.getElementById('results').style.display='none'; } // Fetch live price for the new pair fetchEntryPrice(s.base); } function saveCurrentStrategy(){ if(!activeStratId) return; const idx = strategies.findIndex(s=>s.id===activeStratId); const state = getStratState(); if(idx>=0) strategies[idx] = state; else strategies.push(state); localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); } function switchStrategy(id){ // Save current before switching saveCurrentStrategy(); const s = strategies.find(x=>x.id===id); if(!s) return; activeStratId = id; localStorage.setItem(STRAT_ACTIVE_KEY, id); applyStratState(s); renderStratTabs(); } function newStrategy(){ // Save current saveCurrentStrategy(); const id = 'strat_'+Date.now(); const num = strategies.length + 1; const s = { id, name:'Strategy '+num, base:'BTC', priceDrop:'1.7', takeProfit:'1.7', numOrders:'7', initAmount:'100', multiplier:'1.5', entryPrice:'', orders:[], ts:Date.now() }; strategies.push(s); activeStratId = id; localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); localStorage.setItem(STRAT_ACTIVE_KEY, id); applyStratState(s); renderStratTabs(); fetchEntryPrice('BTC'); showToast('New strategy created!'); } function deleteSelectedStrategy(){ // Delete whichever is selected in dropdown const sel = document.getElementById('stratDropdown'); const idToDelete = sel ? sel.value : activeStratId; deleteStrategy(idToDelete); } function deleteStrategy(id){ const s = strategies.find(x=>x.id===id); if(!s) return; showConfirm('Delete strategy: ' + s.base + '/USDT?', 'Delete', function(){ const idx = strategies.findIndex(x=>x.id===id); strategies.splice(idx,1); if(strategies.length === 0){ activeStratId = null; localStorage.removeItem(STRAT_ACTIVE_KEY); localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); currentOrders = []; const emptyState = document.getElementById('emptyState'); const results = document.getElementById('results'); if(emptyState) emptyState.style.display = 'flex'; if(results) results.style.display = 'none'; const baseEl = document.getElementById('base'); const csEl = document.getElementById('coinSearch'); if(baseEl) baseEl.value = ''; if(csEl) csEl.value = ''; renderStratTabs(); showToast('All strategies deleted. Search a coin to start a new one.'); return; } if(activeStratId === id){ activeStratId = strategies[0].id; localStorage.setItem(STRAT_ACTIVE_KEY, activeStratId); applyStratState(strategies[0]); } localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); renderStratTabs(); showToast('Strategy deleted.'); }); } function renameSelectedStrategy(){ const sel = document.getElementById('stratDropdown'); const id = sel ? sel.value : activeStratId; renameStrategy(id); } function renameStrategy(id){ const s = strategies.find(x=>x.id===id); if(!s) return; const newName = prompt('Rename strategy:', s.name); if(!newName || !newName.trim()) return; s.name = newName.trim(); localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); renderStratTabs(); } function renderStratTabs(){ const sel = document.getElementById('stratDropdown'); if(!sel) return; sel.innerHTML = strategies.map(s=> `` ).join(''); // Update delete button visibility (can't delete if only 1) const delBtn = document.querySelector('.strat-del-btn'); if(delBtn) delBtn.style.opacity = strategies.length <= 1 ? '.3' : '1'; renderCurrentStrategyChip(); } function initStrategies(){ if(strategies.length === 0){ // Create first default strategy const id = 'strat_default'; strategies = [{ id, name:'Strategy 1', base:'BTC', priceDrop:'1.7', takeProfit:'1.7', numOrders:'7', initAmount:'100', multiplier:'1.5', entryPrice:'', orders:[], ts:Date.now() }]; activeStratId = id; localStorage.setItem(STRAT_KEY, JSON.stringify(strategies)); localStorage.setItem(STRAT_ACTIVE_KEY, id); } if(!activeStratId) activeStratId = strategies[0].id; renderStratTabs(); } // Auto-save current strategy every 10 seconds setInterval(saveCurrentStrategy, 10000); // ════════════════════════════════════════════════ // FAVORITES // ════════════════════════════════════════════════ // (duplicate removed) const FAV_KEY = 'mg_favorites'; favorites = JSON.parse(localStorage.getItem(FAV_KEY) || '["BTC","ETH","SOL","BNB","XRP"]'); favDraft = []; wlTab = 'all'; // 'all' | 'fav' wlSort = 'alpha'; // 'alpha' | 'added' function renderFavChips(){ const wrap = document.getElementById('favChips'); if(!wrap) return; const active = document.getElementById('base') ? document.getElementById('base').value : ''; if(favorites.length === 0){ wrap.innerHTML = 'No coins in watchlist — click Manage'; return; } wrap.innerHTML = favorites.map(c => `
${c}
` ).join(''); } function setWlTab(tab){ wlTab = tab; const allBtn = document.getElementById('wlTabAll'); const favBtn = document.getElementById('wlTabFav'); if(allBtn){ allBtn.style.background=tab==='all'?'var(--accent)':'transparent'; allBtn.style.color=tab==='all'?'#fff':'var(--muted)'; } if(favBtn){ favBtn.style.background=tab==='fav'?'var(--accent)':'transparent'; favBtn.style.color=tab==='fav'?'#fff':'var(--muted)'; } document.getElementById('favSearch').value = ''; renderFavGrid(); } function setWlSort(s){ wlSort = s; ['alpha','added'].forEach(x=>{ const b=document.getElementById('sort'+x.charAt(0).toUpperCase()+x.slice(1)); if(b){ b.style.borderColor=x===s?'var(--accent)':'var(--border)'; b.style.background=x===s?'var(--glow)':'transparent'; b.style.color=x===s?'var(--accent)':'var(--muted)'; b.style.fontWeight=x===s?'600':'400'; } }); renderFavGrid(); } function openFavManager(){ favDraft = [...favorites]; wlTab = 'all'; wlSort = 'alpha'; setWlTab('all'); document.getElementById('favSearch').value = ''; renderFavGrid(); document.getElementById('favOverlay').style.display = 'flex'; document.body.style.overflow = 'hidden'; setTimeout(()=>document.getElementById('favSearch').focus(),100); } function closeFavManager(){ document.getElementById('favOverlay').style.display = 'none'; document.body.style.overflow = ''; } // Cache for coin prices fetched in watchlist // (duplicate removed) const wlPriceCache = {}; async function fetchWlPrices(symbols){ // Batch fetch via Binance 24hr ticker try { const res = await fetch('https://data-api.binance.vision/api/v3/ticker/24hr', {cache:'no-store'}); if(!res.ok) throw 0; const data = await res.json(); data.forEach(t=>{ if(t.symbol.endsWith('USDT')){ const sym = t.symbol.replace('USDT',''); wlPriceCache[sym] = { price: parseFloat(t.lastPrice), change: parseFloat(t.priceChangePercent), high: parseFloat(t.highPrice), low: parseFloat(t.lowPrice), volume: parseFloat(t.quoteVolume), }; } }); renderFavGrid(true); // re-render with prices } catch(e){} } function getVolatility(h, l, price){ if(!h||!l||!price) return null; return ((h - l) / price * 100); } function volLabel(pct){ if(pct === null) return {cls:'neu', text:'—'}; if(pct < 3) return {cls:'low', text: pct.toFixed(1)+'% Low'}; if(pct < 7) return {cls:'med', text: pct.toFixed(1)+'% Med'}; return {cls:'high', text: pct.toFixed(1)+'% High'}; } function fmtPrice(p){ if(!p) return '—'; if(p >= 1000) return p.toLocaleString('en-US',{maximumFractionDigits:2}); if(p >= 1) return p.toFixed(2); if(p >= 0.01) return p.toFixed(5); return p.toFixed(8); } function renderFavGrid(skipFetch){ const q = document.getElementById('favSearch').value.trim().toUpperCase(); const grid = document.getElementById('favCoinGrid'); if(!grid) return; let list; if(wlTab === 'fav'){ list = q ? favDraft.filter(c=>c.includes(q)) : [...favDraft]; } else { list = q ? COINS.filter(c=>c.startsWith(q)||c.includes(q)) : [...COINS]; if(!q && wlSort==='added'){ list = [...new Set([...favDraft, ...COINS])]; } } if(wlSort==='alpha') list = [...list].sort(); const countEl = document.getElementById('wlResultCount'); if(countEl) countEl.textContent = list.length+' coins'; const favCountEl = document.getElementById('wlFavCount'); if(favCountEl) favCountEl.textContent = favDraft.length; const shown = list.slice(0,100); grid.innerHTML = shown.map(c => { const inFav = favDraft.includes(c); const pd = wlPriceCache[c]; const price = pd ? fmtPrice(pd.price) : '…'; const change = pd ? pd.change : null; const chgCls = change===null?'neu':change>0?'pos':change<0?'neg':'neu'; const chgTxt = change===null?'—':(change>0?'+':'')+change.toFixed(2)+'%'; const vol = pd ? getVolatility(pd.high, pd.low, pd.price) : null; const volInfo= volLabel(vol); const volUSD = pd ? (pd.volume>=1e9?(pd.volume/1e9).toFixed(1)+'B':(pd.volume/1e6).toFixed(0)+'M') : '—'; var isFlagged = coinFlags[c] ? true : false; return `
${c.slice(0,2)}
${c} ${inFav?'':''} ${isFlagged?'🔖':'🏷️'}
Vol ${volUSD}
${volInfo.text}
${price} USDT
${chgTxt}
`; }).join(''); if(!skipFetch && Object.keys(wlPriceCache).length === 0){ fetchWlPrices(shown); } } function toggleFavDraft(coin){ const idx = favDraft.indexOf(coin); if(idx >= 0) favDraft.splice(idx,1); else favDraft.push(coin); renderFavGrid(); // Update count badge live const favCountEl = document.getElementById('wlFavCount'); if(favCountEl) favCountEl.textContent = favDraft.length; } function saveFavorites(){ favorites = [...favDraft]; localStorage.setItem(FAV_KEY, JSON.stringify(favorites)); renderFavChips(); closeFavManager(); showToast('✓ Watchlist saved — '+favorites.length+' coins'); } function switchRecordTab(name){ const oh = document.getElementById('pane-order-history'); const tf = document.getElementById('pane-transfer'); const ro = document.getElementById('rt-order'); const rt = document.getElementById('rt-transfer'); if(oh) oh.style.display = name==='order' ? 'block' : 'none'; if(tf) tf.style.display = name==='transfer' ? 'block' : 'none'; if(ro){ ro.style.background = name==='order' ? 'var(--card2)' : 'transparent'; ro.style.color = name==='order' ? 'var(--text)' : 'var(--muted)'; } if(rt){ rt.style.background = name==='transfer' ? 'var(--card2)' : 'transparent'; rt.style.color = name==='transfer' ? 'var(--text)' : 'var(--muted)'; } } // ════════════════════════════════════════════════ // BENCHMARK CHARTS // ════════════════════════════════════════════════ // Returns array of {label, myPnl, benchValue} data points // period: 'daily' | 'monthly' | 'yearly' // benchmarkRate: annual rate (e.g. 0.025 for Zakat) function buildBenchmarkData(period, benchmarkRate){ if(tradeHistory.length === 0) return []; const sorted = [...tradeHistory].sort((a,b)=>a.ts-b.ts); const totalInvested = sorted[sorted.length-1] ? sorted.reduce((s,t)=>s+t.invested, 0) / sorted.length // avg investment : 0; const baseInvested = sorted[0].invested || 100; // Group trades by period bucket function getBucket(ts){ const d = new Date(ts); if(period === 'daily') return d.toISOString().split('T')[0]; if(period === 'monthly') return d.getFullYear()+'-'+(d.getMonth()+1).toString().padStart(2,'0'); return d.getFullYear().toString(); } // Build cumulative my P&L per bucket const buckets = {}; let cumPnl = 0; sorted.forEach(t=>{ cumPnl += t.pnl; const key = getBucket(t.ts); buckets[key] = { cumPnl, ts: t.ts, invested: t.invested }; }); const keys = Object.keys(buckets).sort(); if(keys.length === 0) return []; // Convert annual rate to per-period rate const ratePerPeriod = period === 'daily' ? Math.pow(1 + benchmarkRate, 1/365) - 1 : period === 'monthly' ? Math.pow(1 + benchmarkRate, 1/12) - 1 : benchmarkRate; // Use total capital deployed as base for benchmark const totalCapital = sorted.reduce((s,t)=>s+t.invested, 0); const data = keys.map((key, i) => { const benchReturn = totalCapital * ratePerPeriod * (i + 1); return { label: key, myPnl: parseFloat(buckets[key].cumPnl.toFixed(2)), benchmark: parseFloat(benchReturn.toFixed(2)), }; }); return data; } function renderBenchChart(canvasId, chartRef, setRef, data, lines, title, summaryId){ const ctx = document.getElementById(canvasId); if(!ctx) return null; const c = ctx.getContext('2d'); if(chartRef) chartRef.destroy(); if(data.length === 0){ c.clearRect(0,0,ctx.width,ctx.height); c.fillStyle='#64748b'; c.font='13px Inter'; c.textAlign='center'; c.fillText('No trade data yet — close some trades first', ctx.width/2, ctx.height/2); return null; } const labels = data.map(d=>d.label); const chart = new Chart(c, { type: 'line', data: { labels, datasets: lines.map(l=>({ label: l.label, data: data.map(d=>d[l.key]), borderColor: l.color, backgroundColor: l.fill ? l.color.replace(')',', 0.08)').replace('rgb','rgba') : 'transparent', fill: l.fill || false, tension: 0.3, borderWidth: 2.5, pointRadius: data.length <= 12 ? 4 : 2, pointBackgroundColor: l.color, borderDash: l.dash || [], })) }, options: { ...chartDefaults, responsive: true, maintainAspectRatio: false, interaction: { mode:'index', intersect:false }, plugins: { ...chartDefaults.plugins, tooltip: { enabled: false } }, scales: { x: { ticks:{color:'#94a3b8',font:{size:11},maxTicksLimit:10}, grid:{color:'#222b3a'} }, y: { ticks:{color:'#64748b',font:{size:11},callback:v=>(v>=0?'+':'')+fmt(v)+' USDT'}, grid:{color:'#222b3a'} } } } }); // Summary cards const lastD = data[data.length-1]; const summaryEl = document.getElementById(summaryId); if(summaryEl && lastD){ const benchLines = lines.filter(l => l.key !== 'myPnl'); const benchMax = benchLines.length ? Math.max(...benchLines.map(l => lastD[l.key])) : 0; const diff = lastD.myPnl - benchMax; const ahead = diff > 0; summaryEl.innerHTML = lines.map(l=>`
${l.label}
${lastD[l.key]>=0?'+':''}${fmt(lastD[l.key])} USDT
`).join('') + `
${ahead?'▲ You\'re Ahead by':'▼ You\'re Behind by'}
${Math.abs(diff)>=0?'+':''}${fmt(Math.abs(diff))} USDT
`; } return chart; } function renderZakatChart(){ // Zakat: 2.5% yearly, Inflation: 3% yearly // myPnl vs zakat benchmark vs inflation benchmark const datZ = buildBenchmarkData(zakatPeriod, 0.025); const datI = buildBenchmarkData(zakatPeriod, 0.030); // Merge into one dataset const merged = datZ.map((d,i)=>({ label: d.label, myPnl: d.myPnl, zakat: d.benchmark, inflation: datI[i] ? datI[i].benchmark : d.benchmark, })); const lines = [ { key:'myPnl', label:'My P&L', color:'#3b82f6', fill:true }, { key:'zakat', label:'Zakat 2.5%', color:'#f59e0b', fill:false, dash:[5,3] }, { key:'inflation', label:'Inflation 3%', color:'#ef4444', fill:false, dash:[3,3] }, ]; zakatChart = renderBenchChart('zakatChart', zakatChart, v=>zakatChart=v, merged, lines, 'Zakat & Inflation', 'zakatSummary'); } function renderRealEstateChart(){ // Real Estate: 6% yearly const data = buildBenchmarkData(realEstatePeriod, 0.06); const lines = [ { key:'myPnl', label:'My P&L', color:'#3b82f6', fill:true }, { key:'benchmark', label:'Real Estate 6%', color:'#22c55e', fill:false, dash:[5,3] }, ]; realEstateChart = renderBenchChart('realEstateChart', realEstateChart, v=>realEstateChart=v, data, lines, 'Real Estate', 'realEstateSummary'); } // ── Custom confirm (replaces browser confirm() which blocks on mobile) ── function showConfirm(msg, okLabel, callback){ const overlay = document.getElementById('confirmOverlay'); const msgEl = document.getElementById('confirmMsg'); const okBtn = document.getElementById('confirmOK'); const cancelBtn = document.getElementById('confirmCancel'); if(!overlay) { if(window.confirm(msg)) callback(); return; } msgEl.textContent = msg; okBtn.textContent = okLabel || 'Delete'; overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; function cleanup(){ overlay.style.display='none'; document.body.style.overflow=''; okBtn.onclick=null; cancelBtn.onclick=null; } okBtn.onclick = ()=>{ cleanup(); callback(); }; cancelBtn.onclick = ()=>{ cleanup(); }; overlay.onclick = (e)=>{ if(e.target===overlay) cleanup(); }; } // ── PDF Export helpers ─────────────────────────────────── function buildPDFHTML(trades, title){ const date = new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); const totalPnl = trades.reduce((s,t)=>s+t.pnl,0); const wins = trades.filter(t=>t.win).length; const losses = trades.length - wins; const rows = trades.map((t,i)=> '' + ''+(i+1)+''+ ''+fmtDate(t.ts)+''+ ''+t.pair+''+ ''+t.ordersCount+''+ ''+fmt(t.avgBuy)+''+ ''+fmt(t.sellPrice)+''+ ''+fmt(t.qty,2)+''+ ''+fmt(t.invested)+''+ ''+(t.pnl>=0?'+':'')+fmt(t.pnl)+''+ ''+(t.pnlPct>=0?'+':'')+fmt(t.pnlPct,2)+'%'+ ''+(t.win?'WIN':'LOSS')+''+ '' ).join(''); return '
' + '

'+title+'

' + '

Generated: '+date+'  ·  '+trades.length+' trades

' + '
' + '
Total P&L
' + '
'+(totalPnl>=0?'+':'')+fmt(totalPnl)+' USDT
' + '
Wins
'+wins+'
' + '
Losses
'+losses+'
' + '
Win Rate
'+(trades.length?Math.round(wins/trades.length*100):0)+'%
' + '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''+rows+'' + '
#DatePairOrdersAvg BuySell PriceQtyInvestedP&L (USDT)P&L %Result
' + '

Martingale · Trade History Report · '+date+'

' + '
'; } function triggerPrint(htmlContent){ var el = document.getElementById('pdfPrintArea'); if(!el){ showToast('Print container missing.'); return; } el.innerHTML = htmlContent; // Small delay to let DOM update before print dialog setTimeout(function(){ window.print(); }, 100); showToast('Print dialog opened — choose "Save as PDF"'); } // ── Custom Financial Report Builder ── function openFinancialReportModal(){ var overlay = document.getElementById('finReportOverlay'); if(!overlay) return; var allTs = [] .concat(tradeHistory.map(function(t){return t.ts;})) .concat((capitalHistory||[]).map(function(c){return c.ts;})) .filter(Boolean); var earliest = allTs.length ? new Date(Math.min.apply(null, allTs)) : new Date(); var today = new Date(); var fromEl = document.getElementById('frDateFrom'); var toEl = document.getElementById('frDateTo'); if(fromEl) fromEl.value = earliest.toISOString().slice(0,10); if(toEl) toEl.value = today.toISOString().slice(0,10); overlay.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function closeFinancialReportModal(){ var overlay = document.getElementById('finReportOverlay'); if(overlay) overlay.style.display = 'none'; document.body.style.overflow = ''; } function generateFinancialReport(){ var fromVal = document.getElementById('frDateFrom').value; var toVal = document.getElementById('frDateTo').value; var fromTs = fromVal ? new Date(fromVal+'T00:00:00').getTime() : 0; var toTs = toVal ? new Date(toVal+'T23:59:59').getTime() : Date.now(); var incSummary = document.getElementById('frSecSummary').checked; var incTrades = document.getElementById('frSecTrades').checked; var incCapital = document.getElementById('frSecCapital').checked; var incFees = document.getElementById('frSecFees').checked; var incZakat = document.getElementById('frSecZakat').checked; var incCharts = document.getElementById('frSecCharts').checked; var trades = tradeHistory.filter(function(t){ return t.ts >= fromTs && t.ts <= toTs; }); var caps = (capitalHistory||[]).filter(function(c){ return c.ts >= fromTs && c.ts <= toTs; }); var withs = (withdrawalHistory||[]).filter(function(w){ return w.ts >= fromTs && w.ts <= toTs; }); var dateStr = new Date().toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); var rangeStr = (fromVal||'—') + ' to ' + (toVal||'—'); var totalPnl = trades.reduce(function(s,t){return s+(t.pnl||0);},0); var wins = trades.filter(function(t){return t.pnl>0;}).length; var winRate = trades.length ? Math.round(wins/trades.length*100) : 0; var totalCap = caps.reduce(function(s,c){return s+(c.amount||0);},0); var totalWd = withs.reduce(function(s,w){return s+(w.amount||0);},0); var html = '
'; html += '

BUMELHA — Financial Report

'; html += '

Period: '+rangeStr+'  ·  Generated: '+dateStr+'

'; if(incSummary){ html += '

P&L Summary

'; html += '
'; html += '
Total P&L
'+(totalPnl>=0?'+':'')+fmt(totalPnl,2)+' USDT
'; html += '
Trades
'+trades.length+'
'; html += '
Win Rate
'+winRate+'%
'; html += '
Capital Added
'+fmt(totalCap,2)+' USDT
'; html += '
Withdrawn
'+fmt(totalWd,2)+' USDT
'; html += '
'; } if(incCharts && typeof profitChart !== 'undefined' && profitChart){ try{ html += '

Cumulative P&L Chart

'; html += ''; }catch(e){} if(typeof winLossChart !== 'undefined' && winLossChart){ try{ html += '

Win / Loss Distribution

'; html += ''; }catch(e){} } } if(incCapital){ html += '

Capital Management

'; html += '

Capital Added: '+fmt(totalCap,2)+' USDT ('+caps.length+' entries)  |  Withdrawn: '+fmt(totalWd,2)+' USDT ('+withs.length+' withdrawals)

'; } if(incFees){ var totalFeesR = trades.reduce(function(s,t){return s+(t.fees||0);},0); html += '

Fees

'; html += '

Total fees paid across selected period: '+fmt(totalFeesR,2)+' USDT

'; } if(incZakat){ var zakatable = totalCap + totalPnl; var zakatDue = zakatable * 0.025; html += '

Zakat Summary

'; html += '

Zakatable portfolio: '+fmt(zakatable,2)+' USDT  |  Zakat due (2.5%): '+fmt(zakatDue,2)+' USDT

'; } if(incTrades){ html += '

Trade History ('+trades.length+')

'; var rows = trades.slice().sort(function(a,b){return a.ts-b.ts;}).map(function(t,i){ var d = new Date(t.ts).toLocaleDateString('en-GB',{day:'2-digit',month:'short',year:'numeric'}); var pnlColor = t.pnl>=0 ? '#16a34a' : '#dc2626'; return ''+ ''+(i+1)+''+ ''+d+''+ ''+t.pair+''+ ''+fmt(t.invested,2)+''+ ''+(t.pnl>=0?'+':'')+fmt(t.pnl,2)+''+ ''; }).join(''); html += ''+ ''+ ''+(rows||'')+''+ '
#DatePairInvestedP&L (USDT)
No trades in this period
'; } html += '

BUMELHA Martingale · Financial Report · '+dateStr+'

'; html += '
'; closeFinancialReportModal(); triggerPrint(html); } window.onload=()=>{ // ── LocalStorage polyfill for Android WebView (file://) ── (function(){ try{ localStorage.setItem('__test__','1'); localStorage.removeItem('__test__'); } catch(e){ // localStorage unavailable — use in-memory store + cookie chunk persistence var memStore = {}; try{ // Try to load from cookies on startup document.cookie.split(';').forEach(function(c){ var eq = c.indexOf('='); if(eq<0) return; var k = decodeURIComponent(c.slice(0,eq).trim()); if(k.startsWith('mg_')) memStore[k] = decodeURIComponent(c.slice(eq+1)); }); }catch(_){} Object.defineProperty(window,'localStorage',{value:{ getItem: function(k){ return memStore.hasOwnProperty(k)?memStore[k]:null; }, setItem: function(k,v){ memStore[k]=String(v); try{ // Store in cookie (4KB chunks — for large data we cap and just do in-memory) var encoded = encodeURIComponent(String(v)); if(encoded.length < 3900){ document.cookie = encodeURIComponent(k)+'='+encoded+';path=/;max-age=31536000'; } }catch(_){} }, removeItem: function(k){ delete memStore[k]; try{document.cookie=encodeURIComponent(k)+'=;path=/;max-age=0';}catch(_){} }, clear: function(){ Object.keys(memStore).forEach(function(k){ window.localStorage.removeItem(k); }); }, key: function(i){ return Object.keys(memStore)[i]||null; }, get length(){ return Object.keys(memStore).length; } }}); } })(); // Splash hides immediately on load — auth screen appears on top window._hideSplash = function(){ setTimeout(function(){ var splash = document.getElementById('splashScreen'); if(splash){ splash.style.opacity = '0'; splash.style.visibility = 'hidden'; setTimeout(function(){ if(splash.parentNode) splash.remove(); }, 550); } }, 800); }; // Always hide splash on load regardless of auth state window._hideSplash(); // Deferred init — runs only after successful login to keep auth screen fast window._appInit = function(){ if(window._appInited) return; window._appInited = true; // Run in small chunks to keep UI responsive setTimeout(function(){ try{ loadSettings(); initProfileDropdowns(); loadProfileData(); }catch(_){} }, 0); setTimeout(function(){ try{ initStrategies(); renderFavChips(); }catch(_){} }, 50); setTimeout(function(){ try{ var cs = document.getElementById('coinSearch'); if(cs) cs.value = 'BTC'; var ia = document.getElementById('initAmount'); if(ia && typeof formatInvestInput==='function') formatInvestInput(ia); if(typeof fetchEntryPrice==='function') fetchEntryPrice('BTC'); }catch(_){} }, 100); setTimeout(function(){ try{ if(typeof renderHistory==='function') renderHistory(); }catch(_){} }, 200); setTimeout(function(){ try{ if(typeof renderWithdrawalSummary==='function') renderWithdrawalSummary(); }catch(_){} }, 300); setTimeout(function(){ try{ if(typeof renderPnlPeriods==='function') renderPnlPeriods(); }catch(_){} }, 400); setTimeout(function(){ try{ if(typeof loadArLoans==='function') loadArLoans(); if(typeof checkArReminders==='function') checkArReminders(); }catch(_){} }, 500); setTimeout(function(){ try{ if(typeof initBudgetTab==='function') initBudgetTab(); if(typeof initCharityTab==='function') initCharityTab(); }catch(_){} }, 600); }; };