/* ============================================================ zengtrade · Crypto Algo Studio Regime-aware systematic trading on live Binance prices (24/7) ============================================================ */ 'use strict'; const CRYPTO_ONLY = !!window.ZENG_CRYPTO_ONLY; /* ---------- data ---------- */ // The watchlist is now a UNIVERSAL instrument list, any segment (NSE/BSE equity & indices, // NFO/BFO futures & options, MCX commodities, CDS currency). These 10 are the default seed; // the user can add ANY instrument via the universal search (top bar or "Add scrip"). Each item // carries exch/type/key/token so quotes, streaming and charts address it across exchanges. let SYMS = [ {sym:'MARUTI', name:'Maruti Suzuki', ltp:12480.0,chg:1.1, beta:1.05, sector:'Auto'}, {sym:'INFY', name:'Infosys', ltp:1845.6, chg:2.1, beta:0.88, sector:'IT', hold:{qty:25, avg:1690}}, {sym:'SBIN', name:'State Bank', ltp:842.3, chg:1.6, beta:1.18, sector:'PSU Bank', hold:{qty:60, avg:705}}, {sym:'RELIANCE', name:'Reliance Ind', ltp:2945.5, chg:1.2, beta:1.05, sector:'Energy',hold:{qty:10, avg:2810}}, {sym:'ICICIBANK', name:'ICICI Bank', ltp:1234.8, chg:0.9, beta:1.02, sector:'Banks'}, {sym:'HDFCBANK', name:'HDFC Bank', ltp:1678.2, chg:0.4, beta:0.98, sector:'Banks'}, {sym:'ITC', name:'ITC Ltd', ltp:438.9, chg:-0.3, beta:0.64, sector:'FMCG'}, {sym:'TCS', name:'TCS', ltp:3890.0, chg:-0.6, beta:0.72, sector:'IT'}, {sym:'BAJFINANCE', name:'Bajaj Finance', ltp:6890.5, chg:-1.9, beta:1.25, sector:'NBFC', hold:{qty:5, avg:7350}}, {sym:'ADANIENT', name:'Adani Ent', ltp:2456.0, chg:-2.8, beta:1.66, sector:'Energy',hold:{qty:8, avg:2720}}, ]; SYMS.forEach(s=>{ s.exch='NSE'; s.type='EQ'; s.key='NSE:'+s.sym; }); // seed = NSE equities const SEED_SYMS = SYMS.map(s=>s.sym); // ---- universal-instrument helpers ---- const itemKey = s => s.key || (s.exch?s.exch:'NSE')+':'+s.sym; // EXCH:TS identity for data const watchKeys = () => SYMS.map(itemKey); function byKey(key){ return SYMS.find(s=>itemKey(s)===key); } const isEq = s => (s.type||'EQ')==='EQ'; // analytics widgets are equity-only const TYPE_LABEL = {EQ:'Equity',FUT:'Future',CE:'Call',PE:'Put',CUR:'Currency'}; function instSub(s){ // watchlist subtitle per segment if(isEq(s) && s.sector) return s.sector; const t=TYPE_LABEL[s.type]||s.type||''; const exp=s.expiry?(' · '+new Date(s.expiry).toLocaleDateString('en-IN',{day:'2-digit',month:'short'})):''; const k=(s.type==='CE'||s.type==='PE')&&s.strike?(' '+Math.round(s.strike)):''; return `${s.exch||'NSE'} · ${t}${k}${exp}`; } /* ---------- helpers ---------- */ const $ = id => document.getElementById(id); const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)); // Formatters are null/NaN-safe: missing live data renders an honest ", " / neutral, never "₹NaN", "NaN%", or a false green. const inr=n=>Number.isFinite(n)?'₹'+Math.round(n).toLocaleString('en-IN'):'-'; const inrL=n=>Number.isFinite(n)?'₹'+(n/100000).toFixed(2)+'L':'-'; const pct=n=>Number.isFinite(n)?(n>=0?'+':'')+n.toFixed(2)+'%':'-'; const cls=n=>Number.isFinite(n)?(n>=0?'up':'down'):''; const tone=n=>n>0?'up':n<0?'down':''; const bySym=s=>SYMS.find(x=>x.sym===s||itemKey(x)===s); /* ---------- icon set (monochrome, currentColor) ---------- */ const ICONS={ bull:'', bear:'', neutral:'', search:'', plus:'', minus:'', expand:'', compress:'', close:'', sliders:'', shield:'', scale:'', scissors:'', droplet:'', trendUp:'', trendDown:'', clock:'', target:'', link:'', bolt:'', check:'', alert:'', swap:'', star:'', grip:'', sun:'', moon:'', wallet:'', sprout:'', pie:'', flag:'', repeat:'', layout:'', spark:'', cpu:'', send:'', activity:'', download:'', lock:'', layers:'', eye:'', eyeOff:'', }; function icon(name,size){const s=size||16;return `${ICONS[name]||''}`;} /* ============================================================ DETECTION ENGINE ============================================================ */ function readSignals(){return{trend:+$('sTrend').value,vix:+$('sVix').value,ad:+$('sAd').value,rsi:+$('sRsi').value,pnl:+$('sPnl').value,macd:$('macdToggle').dataset.on==='true'};} function scoreSignals(s){return{ trend:clamp(s.trend,-100,100), vix:clamp(Math.round((15.5-s.vix)*20),-100,100), ad:clamp(Math.round(s.ad>=1?(s.ad-1)*75:(s.ad-1)*125),-100,100), mom:clamp(Math.round((s.rsi-50)*3+(s.macd?15:-15)),-100,100), pers:clamp(Math.round(s.pnl*14),-100,100), };} const W={trend:.30,vix:.20,ad:.20,mom:.20,pers:.10}; const composite=sc=>Math.round(sc.trend*W.trend+sc.vix*W.vix+sc.ad*W.ad+sc.mom*W.mom+sc.pers*W.pers); function classify(S,prev){ if(prev==='bull')return S<15?(S<=-25?'bear':'neutral'):'bull'; if(prev==='bear')return S>-15?(S>=25?'bull':'neutral'):'bear'; return S>=25?'bull':S<=-25?'bear':'neutral'; } // 4-state regime LABEL (the bot's taxonomy) ← 3-state directional THEME + VIX. The terminal skin stays 3-state // (bull/neutral/bear); High-Vol and Choppy are richer *labels* that map onto the bear/neutral skins respectively. const THEME_OF={Bull:'bull',Bear:'bear',Choppy:'neutral','High-Vol':'bear'}; // label → terminal theme function regime4(theme3,vix,prevLabel){ const hi = vix>=20 || (prevLabel==='High-Vol' && vix>=18); // VIX band: enter High-Vol at 20, hold until <18 (no flicker) if(hi) return 'High-Vol'; return theme3==='bull'?'Bull':theme3==='bear'?'Bear':'Choppy'; } function confidence(S,sc){const sign=Math.sign(S)||1;const agree=[sc.trend,sc.vix,sc.ad,sc.mom,sc.pers].filter(v=>Math.sign(v)===sign).length/5;return Math.round((0.6*Math.min(Math.abs(S)/50,1)+0.4*agree)*100);} function reasonText(regime,sc){ const map=[[sc.trend,'BTC '+(sc.trend>0?'above stacked MAs':'broke below key MAs')],[sc.vix,sc.vix>0?'volatility cooling':'volatility spiking'],[sc.ad,sc.ad>0?'breadth strong':'breadth collapsing'],[sc.mom,sc.mom>0?'momentum up':'momentum weak'],[sc.pers,sc.pers>0?'your P&L rising':'your portfolio drawing down']]; const want=regime==='bull'?1:-1; return map.filter(m=>Math.sign(m[0])===want).sort((a,b)=>Math.abs(b[0])-Math.abs(a[0])).slice(0,2).map(m=>m[1]).join(', ')||'signals balanced'; } /* ---------- live orderbook ---------- */ let ORDER_ID=104; // Order book starts empty: no fabricated history. Real paper orders (paper:true) appear // here as the user places them from the order pad; live orders need a connected Kite session. const SEED_ORDERS=[]; const REGIME_SYM={bull:'RELIANCE',neutral:'HDFCBANK',bear:'ADANIENT'}; const SL_W={bull:0.025,neutral:0.018,bear:0.010}; // Curated cross-market headline tape, one ticker spanning equities, indices & commodities const HEADLINE_INDEX=[['NIFTY 50',23450],['SENSEX',77100],['BANK NIFTY',50200],['FIN NIFTY',23010],['GOLD',71800],['SILVER',89500],['CRUDE OIL',6420]]; // Everything a user can pin to the rolling ticker: headline indices/commodities + every stock in the universe. const TICKER_UNIVERSE=[ ...HEADLINE_INDEX.map(([name,base])=>({name,base,grp:'Indices & commodities'})), ...SYMS.map(s=>({name:s.sym,base:s.ltp,grp:'Stocks'})), ]; const TICKER_DEFAULT={items:HEADLINE_INDEX.map(([n])=>n), speed:60, rolling:true}; // speed = px/sec scroll rate /* ---------- state ---------- */ const state={mode:'auto',displayed:'bull',engine:'bull',prevVix:12.4,forceHard:false,suggesting:false, plan:'algopro',billing:'mo', // monetization: current tier + billing cycle (persisted) simTimer:null,panelTab:null, selected:null, orderSide:null, orderQty:null, // order ticket orders:SEED_ORDERS.slice(), // live orderbook wlOrder:null, dragSym:null, // watchlist custom order paneW:null, chartH:null, // resizing surface:'day', tapeT:null, // floor mode + live tape tradeFromChart:null, // bracket dragged on the chart persona:null, // 'trader' | 'investor' (null = first run) layout:'originals', // trader workspace preset (originals|charts|watchlist|options|futures|build) desk:{view:'chain',under:0,exp:0,legs:[]}, // derivatives desk (ephemeral) canvas:[], dragCv:null, // build-your-own widget canvas (persisted list of {key,span}) toolState:{}, // ephemeral per-tool UI (sub-tab/filter/draft); not persisted widgets:null, dragWidget:null, // per-persona widget stack revealing:false, // cinematic transition in flight simOverride:false, // regime panel: false = mirrors live Kite signals; true = user what-if (drag/preset/demo) regimeLabel:null, // 4-state regime label (Bull/Bear/Choppy/High-Vol) shown in the panel readout cards:{}, // per-card min/max state ticker:{items:TICKER_DEFAULT.items.slice(),speed:TICKER_DEFAULT.speed,rolling:TICKER_DEFAULT.rolling}, // rolling index tape (user-configurable) lastFocus:null}; let onboarding=false; // first-run wizard in flight (keeps the persona gate open across steps) /* ---------- persona axis (orthogonal to regime) ---------- */ const PERSONA={ trader:{ label:'Trading', icon:'trendUp', fundsLabel:r=>r==='bear'?'Cash / dry powder':'Margin available', chartTf:'15m' }, investor:{ label:'Investing', icon:'sprout', fundsLabel:()=>'Investable surplus', chartTf:'1D' }, algo:{ label:'Algo Studio', icon:'cpu', fundsLabel:()=>'Deployable capital', chartTf:'15m' }, ai:{ label:'AI', icon:'spark', fundsLabel:()=>'Buying power', chartTf:'1D' }, }; const PERSONA_KEYS=['trader','investor','algo','ai']; /* persona × regime playbook line shown in the regime bar */ const PLAY={ trader:{ bull:`Opportunity-capture mode. Leads with breakout scans & long entries; watchlist sorted by momentum and the order pad defaults to BUY with a bracket stop.`, neutral:`Signals are mixed. BUY and SELL stay equally weighted: trade the range and keep size light until a decisive break.`, bear:`Capital-preservation mode. Leads with portfolio risk, hedges & stops: losers surface first and the order pad defaults to PROTECT/SELL.`, }, investor:{ bull:`Stay-disciplined mode. Prices look extended, keep your DCA plans running and don't chase highs; that's what averaging in is for.`, neutral:`Keep compounding. A range is ideal for cost averaging: let your DCA plans keep buying and accumulate quality dips.`, bear:`Accumulation mode. Lower prices are a chance to keep DCA plans active, average down on quality coins. Don't panic-sell.`, }, algo:{ bull:`Momentum regime, trend-following algos lead. Breakout & momentum strategies are favoured; keep stops trailing.`, neutral:`Choppy regime, mean-reversion algos lead. Range & RSI strategies suit; throttle trend systems and cut size.`, bear:`Risk-off regime, defensive & short algos lead. Tighten max-drawdown limits; the kill-switch is one tap away.`, }, ai:{ bull:`AI copilot is watching momentum & breadth, ask for fresh ideas, screeners or a hedge. Confidence is shown on every signal.`, neutral:`AI copilot sees a balanced tape, ask it to scan ranges, compare names or explain your portfolio.`, bear:`AI copilot is flagging risk, ask for hedges, downside screens or a portfolio health check. Nothing trades without your confirm.`, }, }; /* ---------- persistence (localStorage) ---------- */ const LS_KEY='tradepro.terminal.v1'; function saveState(){try{ // BUG FIX (2026-09-20): used to overwrite the whole blob unconditionally, silently dropping any // field this function doesn't itself manage. studio.js stamps an _owner (signed-in user id) on // this same key to stop one user's data leaking to the next person who signs in on a shared // device, that tag has to survive every save this function does, or the very first order/tab // switch after login would erase it. Merge onto the previous blob instead of replacing it. let prev; try{prev=JSON.parse(localStorage.getItem(LS_KEY))||{};}catch(e){prev={};} localStorage.setItem(LS_KEY,JSON.stringify(Object.assign({},prev,{ mode:state.mode,regime:state.displayed,surface:state.surface,regimeCollapsed:state.regimeCollapsed, watchlist:SYMS.map(s=>({sym:s.sym,name:s.name,exch:s.exch,type:s.type,key:itemKey(s),token:s.token,sector:s.sector,beta:s.beta,lot:s.lot,expiry:s.expiry,strike:s.strike,seg:s.seg,hold:s.hold})),wlCustom:!!state.wlCustom, selected:state.selected,paneW:state.paneW,chartH:state.chartH, persona:state.persona,investSection:state.investSection,layout:state.layout,customLayouts:state.customLayouts,activeCustom:state.activeCustom,aiCfg:state.aiCfg,widgets:state.widgets,cards:state.cards,ticker:state.ticker,algo:(state.algo?{view:state.algo.view,exec:state.algo.exec,market:state.algo.market}:null),chart:(window.TPChart?TPChart.serialize():null), orders:state.orders,trading:state.trading,investing:state.investing})));}catch(e){}} function saveChart(){saveState();} // persist callback for the chart engine function loadState(){ let s; try{s=JSON.parse(localStorage.getItem(LS_KEY));}catch(e){return null;} if(!s||typeof s!=='object') return null; // BUG FIX (2026-09-09): everything below reconstructs `out` from a persisted snapshot that could // be anything (an old schema, a partially-written value, a future field). It used to have no // enclosing try/catch, and neither does the caller at boot (line ~6976) - one unexpected shape // anywhere in here threw uncaught and aborted the whole boot sequence mid-way, leaving the header // chrome mounted but everything after it (including the Algo Studio) never rendered, with no // in-app recovery. Any failure now clears the corrupted snapshot and boots fresh instead. try{ const oneOf=(v,arr,d)=>arr.indexOf(v)>=0?v:d; const numIn=(v,lo,hi)=>typeof v==='number'&&isFinite(v)&&v>=lo&&v<=hi?v:null; const out={ mode:oneOf(s.mode,['auto','manual'],'auto'), regime:oneOf(s.regime,['bull','neutral','bear'],'bull'), surface:oneOf(s.surface,['day','night'],'day'), regimeCollapsed:typeof s.regimeCollapsed==='boolean'?s.regimeCollapsed:undefined, persona:oneOf(s.persona,['trader','investor','algo','ai'],null), investSection:null, layout:oneOf(s.layout,['originals','charts','watchlist','options','futures','build'],'originals'), canvas:Array.isArray(s.canvas)?s.canvas.filter(c=>c&&typeof c.key==='string').map(c=>({key:c.key,span:c.span===2?2:1})):null, // legacy single canvas (migrated on boot) customLayouts:Array.isArray(s.customLayouts)?s.customLayouts.filter(l=>l&&typeof l.id==='string'&&typeof l.name==='string').map(l=>{ const okIdx=n=>Number.isInteger(n)&&n>=0&&n<=2; // 3 underlyings (clamped, ordering-independent) const mkCards=arr=>Array.isArray(arr)?arr.filter(c=>c&&typeof c.key==='string').map(c=>{const o={key:c.key,span:c.span===3?3:c.span===2?2:1}; if(c.grp==='A'||c.grp==='B')o.grp=c.grp; return o;}):[]; const mkSync=sy=>({A:okIdx(sy&&sy.A)?sy.A:0,B:okIdx(sy&&sy.B)?sy.B:1}); let tabs=Array.isArray(l.tabs)?l.tabs.filter(t=>t&&typeof t.id==='string').map(t=>({id:String(t.id).slice(0,40),name:(typeof t.name==='string'?t.name:'Tab').slice(0,24),cards:mkCards(t.cards),sync:mkSync(t.sync)})):[]; if(!tabs.length) tabs=[{id:'t'+Math.random().toString(36).slice(2,8),name:'Main',cards:mkCards(l.cards),sync:{A:0,B:1}}]; // migrate old flat `cards` const activeTab=(typeof l.activeTab==='string'&&tabs.some(t=>t.id===l.activeTab))?l.activeTab:tabs[0].id; return {id:String(l.id).slice(0,40),name:String(l.name).slice(0,40),tabs,activeTab}; }):[], activeCustom:(typeof s.activeCustom==='string')?s.activeCustom:null, aiCfg:(s.aiCfg&&typeof s.aiCfg==='object')?{endpoint:(typeof s.aiCfg.endpoint==='string'&&/^https?:\/\//.test(s.aiCfg.endpoint))?s.aiCfg.endpoint.slice(0,300):'',model:['claude-opus-4-8','claude-sonnet-4-6','claude-haiku-4-5'].indexOf(s.aiCfg.model)>=0?s.aiCfg.model:'claude-opus-4-8'}:null, widgets:(()=>{const def={trader:WIDGET_DEFAULTS.trader.slice(),investor:WIDGET_DEFAULTS.investor.slice()}; if(s.widgets&&typeof s.widgets==='object')['trader','investor'].forEach(p=>{ const valid=(WIDGET_CATALOG[p]||[]).map(w=>w.key); if(Array.isArray(s.widgets[p])){const a=[...new Set(s.widgets[p].filter(k=>valid.includes(k)))]; def[p]=a;}}); return def;})(), cards:(()=>{const o={};if(s.cards&&typeof s.cards==='object')['watchlist','chart','panel','order','context'].forEach(k=>{o[k]=oneOf(s.cards[k],['normal','min','max'],'normal');});return o;})(), ticker:(()=>{const d={items:TICKER_DEFAULT.items.slice(),speed:TICKER_DEFAULT.speed,rolling:true}; if(s.ticker&&typeof s.ticker==='object'){ if(Array.isArray(s.ticker.items)){const valid=TICKER_UNIVERSE.map(u=>u.name);const a=[...new Set(s.ticker.items.filter(n=>valid.includes(n)))]; if(a.length)d.items=a;} if(typeof s.ticker.speed==='number'&&s.ticker.speed>=20&&s.ticker.speed<=180)d.speed=Math.round(s.ticker.speed); if(typeof s.ticker.rolling==='boolean')d.rolling=s.ticker.rolling; } return d;})(), selected:(typeof s.selected==='string')?s.selected:null, // validated after the watchlist is rebuilt chartH:numIn(s.chartH,120,500), wlCustom:!!s.wlCustom, paneW:null, chart:(s.chart&&typeof s.chart==='object')?s.chart:null, // validated inside TPChart.restore orders:Array.isArray(s.orders)?s.orders.filter(o=>o&&Number.isFinite(o.id)&&typeof o.sym==='string'&& ['buy','sell'].indexOf(o.side)>=0&&typeof o.qty==='number'&&o.qty>0&&typeof o.price==='number'&&o.price>0&& typeof o.type==='string'&&['Filled','Open','Pending','Cancelled'].indexOf(o.status)>=0&&o.paper===true) .slice(0,500).map(o=>({id:o.id,sym:o.sym,side:o.side,qty:o.qty,price:o.price,type:o.type,status:o.status,paper:true})):[], trading:(s.trading&&typeof s.trading==='object')?{view:oneOf(s.trading.view,['trade','positions','history'],'trade'), sym:CRYPTO_UNIVERSE.some(c=>c.sym===s.trading.sym)?s.trading.sym:CRYPTO_UNIVERSE[0].sym}:null, investing:(s.investing&&typeof s.investing==='object')?{ view:oneOf(s.investing.view,['portfolio','dca','goals'],'portfolio'), dca:Array.isArray(s.investing.dca)?s.investing.dca.filter(p=>p&&typeof p.id==='string'&&CRYPTO_UNIVERSE.some(c=>c.sym===p.sym)&& typeof p.amount==='number'&&p.amount>0&&['weekly','biweekly','monthly'].indexOf(p.cadence)>=0) .slice(0,50).map(p=>({id:p.id,sym:p.sym,amount:p.amount,cadence:p.cadence,active:!!p.active, createdAt:typeof p.createdAt==='number'?p.createdAt:Date.now(), history:Array.isArray(p.history)?p.history.filter(h=>h&&typeof h.ts==='number'&&typeof h.price==='number'&&h.price>0&& typeof h.qty==='number'&&h.qty>0&&typeof h.amount==='number'&&h.amount>0).slice(0,1000):[]})):[], goals:Array.isArray(s.investing.goals)?s.investing.goals.filter(g=>g&&typeof g.id==='string'&&typeof g.label==='string'&& typeof g.targetValue==='number'&&g.targetValue>0) .slice(0,50).map(g=>({id:g.id,label:g.label.slice(0,80),targetValue:g.targetValue, targetDate:typeof g.targetDate==='string'?g.targetDate:null, createdAt:typeof g.createdAt==='number'?g.createdAt:Date.now()})):[], }:null, }; // universal watchlist: validate the persisted instrument list (any segment) if(Array.isArray(s.watchlist)){ const seen=new Set(), wl=[]; for(const it of s.watchlist){ if(!it||typeof it.sym!=='string') continue; const exch=typeof it.exch==='string'?it.exch:'NSE'; const key=(typeof it.key==='string'&&it.key.includes(':'))?it.key:exch+':'+it.sym; if(seen.has(key)) continue; seen.add(key); wl.push({sym:sanInstr(it.sym), name:sanInstr(typeof it.name==='string'?it.name:it.sym), exch, key, ltp:0, chg:0, live:false, type:sanInstr(typeof it.type==='string'?it.type:'EQ'), token:Number.isFinite(it.token)?it.token:null, sector:typeof it.sector==='string'?it.sector:undefined, beta:typeof it.beta==='number'?it.beta:undefined, lot:typeof it.lot==='number'?it.lot:undefined, expiry:typeof it.expiry==='string'?it.expiry:undefined, strike:typeof it.strike==='number'?it.strike:undefined, seg:typeof it.seg==='string'?it.seg:undefined, hold:(it.hold&&typeof it.hold==='object')?it.hold:undefined}); } if(wl.length) out.watchlist=wl.slice(0,100); // cap a runaway list } if(s.paneW&&typeof s.paneW==='object'){ const l=numIn(s.paneW.left,200,600), rr=numIn(s.paneW.right,200,600); if(l!=null&&rr!=null) out.paneW={left:l,right:rr}; } return out; }catch(e){ try{ localStorage.removeItem(LS_KEY); }catch(e2){} return null; } } /* ---------- a11y live announcer ---------- */ function announce(msg){const el=$('srAnnounce');if(el)el.textContent=msg;} /* ============================================================ RENDER: TOP INDEX + REGIME BAR ============================================================ */ function tickerItems(){ const uni=new Map(TICKER_UNIVERSE.map(u=>[u.name,u])); return (state.ticker.items||[]).map(n=>uni.get(n)).filter(Boolean); } // Header market toggle (left of search), Indian ⇄ Crypto. Always visible; drives the rolling tape + Algo Studio scope. function renderHdrMarket(){ const el=$('hdrMkt'); if(!el) return; if(!state.algo) state.algo={}; if(CRYPTO_ONLY) state.algo.market='crypto'; else if(!state.algo.market) state.algo.market='in'; const crypto=state.algo.market==='crypto'; if(CRYPTO_ONLY){ el.innerHTML=`₿ Crypto`; return; } el.innerHTML=`
` +`` +`
`; el.querySelectorAll('[data-algomkt]').forEach(b=>b.onclick=()=>setMarket(b.dataset.algomkt)); } // Single source of truth for the market switch, used by the header toggle (and any other surface). function setMarket(m){ if(!state.algo) state.algo={}; if(CRYPTO_ONLY) m='crypto'; if(state.algo.market===m) return; state.algo.market=m; saveState(); renderHdrMarket(); renderTopIndex(); // swap the rolling tape (Indian ⇄ Crypto) if(m==='crypto'){ loadCrypto().then(()=>{ if(state.algo.market==='crypto'){ patchCryptoTape(); if(isAlgo()) renderAlgo(); } }); connectCryptoWS(); } else disconnectCryptoWS(); // leave crypto → tear down the socket if(isAlgo()) renderAlgo(); // re-scope the Algo Studio if it's open } function renderTopIndex(){ const track=$('topIndex'); if(!track) return; // CRYPTO MODE: the global tape rolls live Binance prices (independent of Kite). Real data only. // CRYPTO_ONLY is known at script load, before state.algo.market gets set during boot (init() sets // it a few calls later) - checking it directly here closes that gap instead of racing it, which // used to flash the old "connect Kite" banner for a moment on every fresh load. const bar=track.closest('.ticker-bar'); const crypto=CRYPTO_ONLY||!!(state.algo&&state.algo.market==='crypto'); if(bar) bar.classList.toggle('crypto',crypto); if(crypto){ renderCryptoTape(track); return; } // NO FAKE PRICES: when Kite isn't connected, show an honest banner instead of synthetic ticks. if(!BOT.live){ track.classList.remove('rolling'); const vp0=track.parentElement; if(vp0) vp0.classList.add('static'); track.innerHTML=`
${icon('shield',12)}Live market data off, run python3 login.py to connect Kite
`; track.dataset.tsig='off'; return; } const items=tickerItems(); // Structure guard: when the instrument SET + rolling flag are unchanged, DON'T rebuild the DOM // (a full innerHTML swap flickers and resets the scroll). Patch live values in place instead, // so the tape is flicker-free no matter who calls this (loadMarket / recompute / ticker settings). const tsig='L|'+(!!state.ticker.rolling)+'|'+items.map(i=>i.name).join(','); if(track.dataset.tsig===tsig && track.querySelector('.tix')){ patchTape(); return; } track.dataset.tsig=tsig; const seq=items.map(({name})=>{ const rq=realQuote(name); // data-tname lets patchTopIndex() update the value/chg in place on every tick (no animation restart) if(!rq) return `
${esc(name)}
-
`; const dec=rq.ltp>=20000?0:(rq.ltp>=1000?1:2); return `
${esc(name)}
${rq.ltp.toLocaleString('en-IN',{maximumFractionDigits:dec})} ${pct(rq.chg||0)}
`; }).join('') || `
No instruments, add some from ticker settings ▸
`; const roll=!!state.ticker.rolling && items.length>1; track.classList.toggle('rolling',roll); const vp=track.parentElement; if(vp) vp.classList.toggle('static',!roll); // two identical sequences let the track loop seamlessly at translateX(-50%) track.innerHTML=`
${seq}
`+(roll?``:''); applyTickerSpeed(); connectStream(); // keep the live tick subscription in sync with the ticker's instruments } // Patch ONLY the rolling tape's price/chg cells in place (both looping copies) so the scroll // animation never restarts. Driven by the fast tick loop + SSE, not the 30s market poll. function patchTopIndex(){ const track=$('topIndex'); if(!track||!BOT.live) return; track.querySelectorAll('.tix[data-tname]').forEach(el=>{ const name=el.dataset.tname, rq=realQuote(name); const valEl=el.querySelector('.tix-val'), chgEl=el.querySelector('.tix-chg'); if(!valEl) return; if(!rq){ valEl.textContent='-'; valEl.classList.add('muted'); if(chgEl)chgEl.hidden=true; return; } const dec=rq.ltp>=20000?0:(rq.ltp>=1000?1:2); const txt=rq.ltp.toLocaleString('en-IN',{maximumFractionDigits:dec}); if(valEl.textContent!==txt){ const prev=parseFloat(valEl.dataset.raw); const dir=isFinite(prev)?Math.sign(rq.ltp-prev):0; valEl.textContent=txt; valEl.classList.remove('muted'); valEl.dataset.raw=rq.ltp; if(dir){ valEl.classList.remove('tick-up','tick-dn'); void valEl.offsetWidth; valEl.classList.add(dir>0?'tick-up':'tick-dn'); } } if(chgEl){ chgEl.hidden=false; chgEl.textContent=pct(rq.chg||0); chgEl.className='tix-chg '+cls(rq.chg||0)+' num'; } }); } /* ---- CRYPTO rolling tape: same flicker-free marquee, fed by live Binance data (no Kite) ---- */ function renderCryptoTape(track){ // Kick a fetch if we've never loaded crypto; patch the tape (+resize) once it lands. if(!CRYPTO.loaded && !CRYPTO.busy) loadCrypto().then(()=>{ if(state.algo&&state.algo.market==='crypto'){ patchCryptoTape(); applyTickerSpeed(); } }); const tsig='C|'+CRYPTO_UNIVERSE.map(c=>c.sym).join(','); // Structure guard: only rebuild the DOM when the SET changes, else patch in place. if(track.dataset.tsig===tsig && track.querySelector('.tix[data-cname]')){ patchCryptoTape(); return; } track.dataset.tsig=tsig; const seq=CRYPTO_UNIVERSE.map(c=>{ const q=CRYPTO.quotes[c.sym]; if(!q) return `
${esc(c.tk)}
-
`; return `
${esc(c.tk)}
` +`${cryptoFmt(q.ltp)}` +`${pct(q.chg)}
`; }).join(''); track.classList.add('rolling'); const vp=track.parentElement; if(vp) vp.classList.remove('static'); // two identical sequences → seamless translateX(-50%) loop, regardless of tile width track.innerHTML=`
${seq}
`; applyTickerSpeed(); } // Patch ONLY the crypto tape's value/chg cells in place (both looped copies), driven by the 5s Binance poll. function patchCryptoTape(){ const track=$('topIndex'); if(!track) return; track.querySelectorAll('.tix[data-cname]').forEach(el=>{ const q=CRYPTO.quotes[el.dataset.cname], valEl=el.querySelector('.tix-val'), chgEl=el.querySelector('.tix-chg'); if(!valEl) return; if(!q){ valEl.textContent='-'; valEl.classList.add('muted'); if(chgEl)chgEl.hidden=true; return; } const txt=cryptoFmt(q.ltp); if(valEl.textContent!==txt){ const prev=parseFloat(valEl.dataset.raw); const dir=isFinite(prev)?Math.sign(q.ltp-prev):0; valEl.textContent=txt; valEl.classList.remove('muted'); valEl.dataset.raw=q.ltp; if(dir){ valEl.classList.remove('tick-up','tick-dn'); void valEl.offsetWidth; valEl.classList.add(dir>0?'tick-up':'tick-dn'); } } if(chgEl){ chgEl.hidden=false; chgEl.textContent=pct(q.chg); chgEl.className='tix-chg '+cls(q.chg)+' num'; } }); const src=$('tbCryptoSrc'); if(src) src.textContent=CRYPTO.live?'● Live · Binance · 24h':(CRYPTO.error?'Binance unreachable, retrying':'Connecting to Binance…'); } function applyTickerSpeed(){ const track=$('topIndex'); if(!track) return; if(!track.classList.contains('rolling')){track.style.removeProperty('--tk-dur');return;} const seqA=track.querySelector('.tb-seq-a'); if(!seqA) return; requestAnimationFrame(()=>{const w=seqA.scrollWidth, pps=clamp(state.ticker.speed||60,20,180); track.style.setProperty('--tk-dur',Math.max(6,w/pps).toFixed(1)+'s');}); } /* ---- REALTIME ROLLING TAPE ------------------------------------------------------------ The index tape used to refresh ONLY via loadMarket(), every 30s, and /api/market is itself 30s-cached server-side, so it sat frozen between cycles while the watchlist ticked every second. Here we stream the index LTPs from the WebSocket-fed /api/ticks cache (the SAME sub-second feed the watchlist uses) on the existing 2s poll and patch the tape values IN PLACE, no innerHTML swap, so the scroll never resets/jumps. Commodities (MCX) have no cheap realtime source (need the resolved futures symbol) and stay on the 30s loadMarket cycle. */ const TAPE_LIVE={'NSE:NIFTY 50':['indices','NIFTY 50'],'NSE:NIFTY BANK':['indices','NIFTY BANK'], 'NSE:NIFTY FIN SERVICE':['indices','NIFTY FIN SERVICE'],'BSE:SENSEX':['indices','SENSEX'], 'NSE:INDIA VIX':['vix',null]}; async function loadTape(){ if(!BOT.live||!BOT.market) return; try{ const keys=Object.keys(TAPE_LIVE); const d=await fetch(`${BOT_API}/api/ticks?keys=${encodeURIComponent(keys.join(','))}`).then(r=>r.json()); const t=d&&d.ticks; if(!t) return; for(const k in TAPE_LIVE){ const v=t[k]; if(!v||v.ltp==null) continue; const sec=TAPE_LIVE[k][0], name=TAPE_LIVE[k][1]; if(sec==='vix'){ BOT.market.vix={ltp:v.ltp,chgPct:v.chg}; } else { const m=(BOT.market[sec]=BOT.market[sec]||{}); const prev=m[name]||{}; m[name]={ltp:v.ltp,prevClose:prev.prevClose,chgPct:v.chg}; } } patchTape(); }catch(e){} } function patchTape(){ const track=$('topIndex'); if(!track) return; track.querySelectorAll('.tix').forEach(tx=>{ // key off data-tname (robust) not display text, so an esc()'d name still matches const key=(tx.dataset.tname || (tx.querySelector('.tix-name')||{}).textContent || '').trim(); if(!key) return; const rq=realQuote(key); if(!rq||rq.ltp==null) return; const dec=rq.ltp>=20000?0:(rq.ltp>=1000?1:2); const val=tx.querySelector('.tix-val'); if(val){ val.classList.remove('muted'); val.textContent=rq.ltp.toLocaleString('en-IN',{maximumFractionDigits:dec}); } const chg=tx.querySelector('.tix-chg'); if(chg){ chg.hidden=false; chg.textContent=pct(rq.chg||0); chg.className='tix-chg '+cls(rq.chg||0)+' num'; } // un-hide: a tile that first painted with no quote must show its % once live }); } /* ---- ticker settings popover (speed · rolling · instruments) ---- */ function openTickerSettings(open){ const p=$('tickerSettings'), g=$('tickerGear'); if(!p) return; if(open===undefined) open=p.hidden; if(!open){ p.hidden=true; document.removeEventListener('click',tickerOutside,true); document.removeEventListener('keydown',tickerEsc); if(g)g.setAttribute('aria-expanded','false'); return; } renderTickerSettings(); p.hidden=false; if(g){const r=g.getBoundingClientRect(); p.style.top=(r.bottom+8)+'px'; p.style.right=Math.max(12,window.innerWidth-r.right)+'px'; g.setAttribute('aria-expanded','true');} setTimeout(()=>{document.addEventListener('click',tickerOutside,true);document.addEventListener('keydown',tickerEsc);},0); } function tickerOutside(e){ if(!e.target.closest('#tickerSettings,#tickerGear')) openTickerSettings(false); } function tickerEsc(e){ if(e.key==='Escape'){openTickerSettings(false); const g=$('tickerGear'); if(g)g.focus();} } let tkQuery='', tkActive=-1, tkRefocus=false; // ticker instrument search: query text, keyboard cursor, refocus-after-add flag function renderTickerSettings(){ const p=$('tickerSettings'); if(!p) return; const t=state.ticker, has=new Set(t.items); const chips=t.items.map(n=>`${esc(n)}`).join('')||`No instruments yet, search below to add one.`; p.innerHTML=`
Ticker settings
SlowFast
Instruments ${t.items.length}
${chips}
${icon('search',13)}
${TICKER_UNIVERSE.length} instruments available
`; $('tkClose').onclick=()=>openTickerSettings(false); $('tkRoll').onclick=()=>{t.rolling=!t.rolling;renderTopIndex();renderTickerSettings();saveState();}; const sp=$('tkSpeed'); if(sp) sp.oninput=()=>{t.speed=+sp.value;applyTickerSpeed();saveState();}; p.querySelectorAll('[data-tkremove]').forEach(b=>b.onclick=()=>{t.items=t.items.filter(n=>n!==b.dataset.tkremove);renderTopIndex();renderTickerSettings();saveState();}); $('tkReset').onclick=()=>{tkQuery='';state.ticker={items:TICKER_DEFAULT.items.slice(),speed:TICKER_DEFAULT.speed,rolling:true};renderTopIndex();renderTickerSettings();saveState();}; // --- searchable instrument picker (type to filter · ↑↓ to navigate · Enter to add) --- const search=$('tkSearch'), results=$('tkResults'), clear=$('tkClear'); const hlite=(name,q)=>{ if(!q)return esc(name); const i=name.toLowerCase().indexOf(q); return i<0?esc(name):esc(name.slice(0,i))+''+esc(name.slice(i,i+q.length))+''+esc(name.slice(i+q.length)); }; const addInstr=name=>{ if(!name||has.has(name))return; t.items.push(name); tkRefocus=true; renderTopIndex(); renderTickerSettings(); saveState(); announce(name+' added to ticker'); }; const itemEls=()=>[...results.querySelectorAll('.tk-res')]; const setActive=i=>{ const els=itemEls(); if(!els.length){tkActive=-1;return;} tkActive=(i+els.length)%els.length; els.forEach((el,j)=>el.classList.toggle('on',j===tkActive)); els[tkActive].scrollIntoView({block:'nearest'}); }; const closeResults=()=>{ results.hidden=true; tkActive=-1; if(search)search.setAttribute('aria-expanded','false'); }; const renderResults=()=>{ const q=tkQuery.trim().toLowerCase(), avail=TICKER_UNIVERSE.filter(u=>!has.has(u.name)); if(!avail.length){ results.innerHTML=`
Every instrument is already on your ticker.
`; results.hidden=false; return; } let m=q?avail.filter(u=>u.name.toLowerCase().includes(q)):avail; if(q)m.sort((a,b)=>(a.name.toLowerCase().startsWith(q)?0:1)-(b.name.toLowerCase().startsWith(q)?0:1)||a.name.localeCompare(b.name)); const total=m.length; m=m.slice(0,40); if(!m.length){ results.innerHTML=`
No instrument matches “${esc(tkQuery)}”.
`; results.hidden=false; search.setAttribute('aria-expanded','true'); return; } const groups={}; m.forEach(x=>{(groups[x.grp]=groups[x.grp]||[]).push(x);}); results.innerHTML=Object.keys(groups).map(g=>`
${esc(g)}
`+groups[g].map(x=>{const dec=x.base>=20000?0:1; return ``;}).join('')).join('') + (total>m.length?`
+${total-m.length} more, keep typing to narrow
`:''); results.hidden=false; search.setAttribute('aria-expanded','true'); results.querySelectorAll('[data-tkadd]').forEach(b=>{b.onmousedown=e=>e.preventDefault();b.onclick=()=>addInstr(b.dataset.tkadd);}); setActive(0); }; if(search){ search.oninput=()=>{ tkQuery=search.value; if(clear)clear.hidden=!tkQuery; renderResults(); }; search.onfocus=()=>renderResults(); search.onkeydown=e=>{ if(e.key==='ArrowDown'){ e.preventDefault(); if(results.hidden){renderResults();}else setActive(tkActive+1); } else if(e.key==='ArrowUp'){ e.preventDefault(); setActive(tkActive-1); } else if(e.key==='Enter'){ e.preventDefault(); const els=itemEls(), el=els[tkActive]||els[0]; if(el)addInstr(el.dataset.tkadd); } else if(e.key==='Escape'&&!results.hidden){ e.preventDefault(); e.stopPropagation(); closeResults(); } }; } if(clear)clear.onclick=()=>{ tkQuery=''; if(search){search.value='';search.focus();} clear.hidden=true; renderResults(); }; if(tkRefocus){ tkRefocus=false; if(search){search.focus(); const L=search.value.length; try{search.setSelectionRange(L,L);}catch(_){}} renderResults(); } } function wireTicker(){ const g=$('tickerGear'); if(g) g.onclick=e=>{e.stopPropagation();openTickerSettings();}; } function renderRegimeBar(r){ const bar=$('regimeBar'); if(!bar) return; const cfg={ bull:{kick:'Risk-on · Momentum',title:'Markets trending up', read:`Opportunity-capture mode. The terminal leads with breakout scans & long entries; risk tools stay one tap away. Your watchlist is sorted by momentum and the order pad defaults to BUY.`}, neutral:{kick:'Neutral · Rangebound',title:'Markets rangebound, wait for clarity', read:`Signals are mixed. BUY and SELL stay equally weighted and the engine waits for a decisive break before committing to a regime.`}, bear:{kick:'Risk-off · Defensive',title:'Markets under pressure', read:`Capital-preservation mode. The terminal now leads with portfolio risk, hedges & stops: the risk pane widens, losers surface first and the order pad defaults to PROTECT/SELL.`}, }[r]; const read=(PLAY[state.persona||'trader']||PLAY.trader)[r]||cfg.read; // BUG FIX (2026-09-19): this was a binary isInvestor() check, so it permanently showed "Trading" // in Algo Studio (and would have in AI mode too) - harmless while only algo/ai were reachable, // a real visible wrong label now that all modes are live. Reads the real persona via the // existing PERSONA map instead; 'algo' is special-cased to match the header toggle's own wording. const pTag=state.persona==='algo'?'Algo Studio':(PERSONA[state.persona]?PERSONA[state.persona].label:'Trading'); if(state.regimeCollapsed===undefined) state.regimeCollapsed=true; // compact by default, engine stats live in the header now const collapsed=state.regimeCollapsed; // Guard: the bar's content is purely regime + persona + collapsed. When none changed, skip the // rebuild so the 30s poll (and recompute, called every poll) don't flicker the banner. const rbsig=r+'|'+(state.persona||'trader')+'|'+(collapsed?1:0)+'|'+pTag; if(bar.dataset.rbsig===rbsig) return; bar.dataset.rbsig=rbsig; bar.classList.toggle('collapsed',collapsed); if(collapsed){ bar.innerHTML=``; } else { bar.innerHTML=`
${cfg.kick} · ${pTag}${cfg.title}
${read}
`; } const t=$('rbToggle'); if(t)t.onclick=()=>{state.regimeCollapsed=!state.regimeCollapsed; renderRegimeBar(state.displayed); if(typeof saveState==='function')saveState();}; } /* ============================================================ RENDER: WATCHLIST (left pane) ============================================================ */ function spark(seed,up){ let s=seed;const rnd=()=>{s=(s*9301+49297)%233280;return s/233280;}; let y=10,pts=[];for(let i=0;i<10;i++){y=clamp(y+(rnd()-(up?0.42:0.58))*5,2,16);pts.push([i*5,+y.toFixed(1)]);} const line=pts.map((p,i)=>`${i?'L':'M'}${p[0]},${p[1]}`).join(''); const col=up?'var(--green)':'var(--red)', gid=up?'sgu':'sgd'; return ``; } // segment-filter tab (All / F&O / Indices) over the universal watchlist function wlFilter(){ const t=state.wlTab||'all'; if(t==='fno') return SYMS.filter(s=>s.exch==='NFO'||s.exch==='BFO'||['FUT','CE','PE'].includes(s.type)); if(t==='idx') return SYMS.filter(s=>s.seg==='INDICES'||/NIFTY|SENSEX|INDIA VIX|BANKEX/i.test(s.sym)); return SYMS.slice(); } function wlList(r){ const base=wlFilter(); if(state.wlCustom) return base; // user's drag order = the array order const bear=r==='bear'; return [...base].sort((a,b)=>bear?(a.chg||0)-(b.chg||0):(b.chg||0)-(a.chg||0)); } function renderWatchlist(r){ const bear=r==='bear', inv=isInvestor(); const list=wlList(r); const fallbackSym = (typeof CRYPTO_ONLY!=='undefined'&&CRYPTO_ONLY) ? (r==='bear'?'SOLUSDT':r==='neutral'?'ETHUSDT':'BTCUSDT') : (REGIME_SYM[r]||'RELIANCE'); const effSel=state.selected||fallbackSym; $('wlMeta').innerHTML=state.wlCustom ? `${SYMS.length} instruments · custom order` : inv ? `Quality & your holdingsLTP · Chg%` : `${list.length} · ${bear?'Weakest first':'Top movers first'}LTP · Chg%`; if(!list.length){ $('wlRows').innerHTML=`
${icon('search',16)}No instruments in this tab, use search or “Add scrip” to add any instrument (stocks, futures, options, indices, MCX).
`; return; } $('wlRows').innerHTML=list.map(s=>{ const lv = s.live!==false && BOT.live; // real Kite quote present for THIS instrument const eq=isEq(s); let badge=''; if(inv){ badge = s.hold?`holding`:(lv&&s.chg<-1.5?`on sale`:''); } else if(bear){ badge = lv&&s.chg<0?`near support`:(s.hold?`holding`:''); } else { badge = lv&&s.chg>1.5?`near breakout`:''; } const sub = `${esc(instSub(s))}${(bear&&eq&&s.beta!=null)?`β ${s.beta}`:''}${badge}`; const ltpCell = lv ? `
${(+s.ltp).toLocaleString('en-IN')}
${pct(s.chg)}
` : `
-
·
`; const _selRow=(itemKey(s)===effSel||s.sym===effSel); return `
${icon('grip',12)}
${esc(s.sym)} ${!eq?`${s.type}`:(s.hold?`${icon('star',11)}`:'')}
${sub}
${ltpCell}
${lv&&eq?spark(s.sym.length*97+ (bear?3:11), s.chg>=0):''}
`; }).join(''); const rst=$('wlReset'); if(rst) rst.onclick=()=>{state.wlCustom=false;renderWatchlist(state.displayed);saveState();}; $('wlRows').querySelectorAll('[data-wlremove]').forEach(b=>b.onclick=e=>{e.stopPropagation();removeInstrument(b.dataset.wlremove);}); } // add ANY instrument (from the universal search) to the watchlist function addInstrument(meta){ if(!meta||!meta.key) return; if(byKey(meta.key)){ selectSym(meta.sym); return; } // already pinned → just select it SYMS.push({sym:sanInstr(meta.ts||meta.sym), name:sanInstr(meta.name||meta.ts||meta.sym), exch:meta.exch, type:sanInstr(meta.type||'EQ'), key:meta.key, token:meta.token, seg:meta.seg, lot:meta.lot, ltp:0, chg:0, expiry:meta.expiry, strike:meta.strike, sector:meta.sector, live:false}); saveState(); if(BOT.live){ connectStream(); loadMarket(); } // pull a quote + (re)subscribe the stream for the new key renderWatchlist(state.displayed); selectSym(meta.ts||meta.sym); announce((meta.ts||meta.sym)+' added to watchlist'); } function removeInstrument(key){ const i=SYMS.findIndex(s=>itemKey(s)===key); if(i<0) return; const was=SYMS[i].sym; if(SYMS.length<=1){ quickToast('Keep at least one','Add another instrument before removing the last one.'); return; } SYMS.splice(i,1); if(state.selected===key||state.selected===was) state.selected=null; // if this stock was also pinned to the rolling tape, drop it so it can't become a dead ", " cell if(state.ticker&&Array.isArray(state.ticker.items)&&state.ticker.items.includes(was)){ state.ticker.items=state.ticker.items.filter(n=>n!==was); if(BOT.tickerLive) delete BOT.tickerLive[was]; renderTopIndex(); } saveState(); if(BOT.live) connectStream(); renderWatchlist(state.displayed); renderChart(state.displayed); announce(was+' removed from watchlist'); } function selectSym(sym){ if(!bySym(sym))return; state.selected=sym; state.orderSide=null; state.orderQty=null; state.tradeFromChart=null; renderWatchlist(state.displayed); renderChart(state.displayed); renderOrder(state.displayed); if(BOT.live && document.querySelector('.wg-card[data-wkey="depth"]')) loadDepth(sym); // refresh depth ladder for new symbol announce(sym+' selected'); saveState(); } /* trade-from-chart: the engine drags an entry/SL/target bracket → order pad */ function tradeFromChart(p){ if(!p) return; if(p.autoExecute){ executeBracketOrder(p); return; } if(state.persona==='trader'){ state.trading=state.trading||{}; state.trading.sym=p.sym; state.orderSide=p.side; state.tradeFromChart={sym:p.sym,side:p.side,sl:p.sl,target:p.target,entry:p.entry}; quickToast('Chart level applied',`${(p.side||'').toUpperCase()} ${p.sym} at ${cryptoFmt(p.entry)}`); renderTrading(); saveState(); return; } if(!bySym(p.sym)) return; state.selected=p.sym; state.tradeFromChart={sym:p.sym,side:p.side,sl:p.sl,target:p.target,entry:p.entry}; state.orderSide=p.side; state.orderQty=null; renderOrder(state.displayed); const pad=$('orderPad'); if(pad){pad.classList.remove('flash-pad');void pad.offsetWidth;pad.classList.add('flash-pad');} announce(`Chart bracket sent, ${p.side==='buy'?'long':'short'} ${p.sym}, SL ${p.sl}, target ${p.target}`); saveState(); } /* ============================================================ RENDER: CHART (center) → delegates to the interactive engine ============================================================ */ function chartSymbol(r){ if(state.selected && bySym(state.selected)) return bySym(state.selected); const fallbackSym = (typeof CRYPTO_ONLY!=='undefined'&&CRYPTO_ONLY) ? (r==='bear'?'SOLUSDT':r==='neutral'?'ETHUSDT':'BTCUSDT') : (REGIME_SYM[r]||'RELIANCE'); return bySym(fallbackSym) || (Array.isArray(SYMS)&&SYMS[0]) || (typeof CRYPTO_UNIVERSE!=='undefined'&&CRYPTO_UNIVERSE[0]?{sym:CRYPTO_UNIVERSE[0].sym,name:CRYPTO_UNIVERSE[0].name,ltp:100,chg:0}:{sym:'BTCUSDT',name:'Bitcoin',ltp:100,chg:0}); } function renderChart(r){ if(!window.TPChart) return; const s=chartSymbol(r); if(!s||!s.sym) return; TPChart.render({symbol:s.sym, name:s.name||s.sym, regime:r, basePrice:s.ltp, change:s.chg}); } /* Real OHLCV feed for the chart, pulls Kite historical via /api/candles. Returns null when offline or the symbol has no real series, so the chart shows an honest empty state instead of synthetic candles. The chart calls this on every symbol/timeframe change. */ async function chartFeed(sym, tfKey){ if(!BOT.live) return null; // any segment: chart by EXCH:TS key when the instrument isn't a plain NSE equity const it=bySym(sym); const q=(it && it.exch && it.exch!=='NSE') ? `key=${encodeURIComponent(itemKey(it))}` : `symbol=${encodeURIComponent((it&&it.sym)||sym)}`; try{ const d=await fetch(`${BOT_API}/api/candles?${q}&tf=${encodeURIComponent(tfKey)}`).then(r=>r.json()); if(d && Array.isArray(d.candles) && d.candles.length) return d.candles; }catch(e){} return null; } /* ============================================================ RENDER: ORDER PAD (right) ============================================================ */ function orderModel(r){ const fallbackSym = (typeof CRYPTO_ONLY!=='undefined'&&CRYPTO_ONLY) ? (r==='bear'?'SOLUSDT':r==='neutral'?'ETHUSDT':'BTCUSDT') : (REGIME_SYM[r]||'RELIANCE'); const s=bySym(state.selected||fallbackSym)||(Array.isArray(SYMS)&&SYMS[0])||(typeof CRYPTO_UNIVERSE!=='undefined'&&CRYPTO_UNIVERSE[0]?{sym:CRYPTO_UNIVERSE[0].sym,name:CRYPTO_UNIVERSE[0].name,ltp:100,chg:0}:{sym:'BTCUSDT',ltp:100,chg:0}); const sym=s.sym; const tf=(state.tradeFromChart&&state.tradeFromChart.sym===sym)?state.tradeFromChart:null; const side=tf?tf.side:(state.orderSide||(r==='bear'?'sell':'buy')); const qty=state.orderQty!=null?state.orderQty:(r==='bear'?4:10); const w=SL_W[r]; const live=!!BOT.live && s.live!==false; // a REAL quote exists for this symbol const px=(live&&typeof s.ltp==='number'&&isFinite(s.ltp))?s.ltp:0; // no live quote → no price (honest) const entry=tf?tf.entry:px; const sl=tf?tf.sl:(side==='buy'? px*(1-w) : px*(1+w)); const target=tf?tf.target:null; const type=tf?'BRACKET':(r==='bear'?'SL-M':r==='bull'?'BRACKET':'LIMIT'); return {sym,s,px,priced:px>0,side,qty,w,entry,sl,target,type,value:qty*entry,fromChart:!!tf}; } function renderOrder(r){ const m=orderModel(r); const types=r==='bear'?['SL-M','GTT stop','Cover','Margin ×5']:r==='bull'?['BRACKET','Cover','Margin ×5']:['LIMIT','Market','SL']; const typePills=types.map((tp,i)=>`${tp}`).join(''); const ctaCls=m.side==='buy'?(r==='neutral'?'cta-navy':'cta-buy'):'cta-sell'; const ctaTxt=m.side==='buy'?'BUY '+m.sym:(r==='bear'?'PROTECT / SELL '+m.sym:'SELL '+m.sym); const note=m.fromChart?`
${icon('target',14)}Levels pulled from your chart bracket, entry, SL & target are live on the chart.
` :r==='bear'?`
${icon('alert',14)}High VIX, qty trimmed, SL tightened. Consider a protective put.
` :r==='neutral'?`
${icon('swap',14)}Rangebound, wait for a breakout before sizing up.
` :`
${icon('check',14)}Trend intact, trail SL, let winners run.
`; const rr=(m.fromChart&&m.target!=null)?Math.abs(m.target-m.entry)/Math.max(1,Math.abs(m.entry-m.sl)):0; const entryFld=m.fromChart?`
${Math.round(m.entry).toLocaleString('en-IN')}
`:''; const ladder=(m.fromChart&&m.target!=null) ? `
${Math.round(m.target).toLocaleString('en-IN')}
` : (r==='bull'&&m.priced)?`
T1 ${Math.round(m.px*1.022).toLocaleString('en-IN')} · T2 ${Math.round(m.px*1.046).toLocaleString('en-IN')} · T3 ${Math.round(m.px*1.073).toLocaleString('en-IN')}
`:''; $('orderPad').innerHTML=`
${esc(m.sym)} PAPER${m.priced?m.px.toLocaleString('en-IN'):'-'} ${m.s.live!==false?pct(m.s.chg):''}${cardCtl('order')}
BUY
${r==='bear'?'SELL / HEDGE':'SELL'}
${typePills}
${m.priced?Math.round(m.sl).toLocaleString('en-IN'):'-'}
${entryFld}${ladder}
${m.priced?inr(m.value):'-'}
${note}
`; $('orderPad').querySelectorAll('[data-side]').forEach(b=>b.onclick=()=>{state.orderSide=b.dataset.side;state.tradeFromChart=null;renderOrder(state.displayed);}); $('orderPad').querySelectorAll('[data-qty]').forEach(b=>b.onclick=()=>{const d=+b.dataset.qty;state.orderQty=Math.max(1,(state.orderQty!=null?state.orderQty:m.qty)+d);renderOrder(state.displayed);}); const qi=$('ordQty'); if(qi) qi.oninput=()=>{const v=parseInt(qi.value)||0;state.orderQty=Math.max(0,v);const vv=$('ordVal');if(vv)vv.textContent=m.priced?inr(state.orderQty*m.entry):'-';}; $('placeBtn').onclick=()=>openOrderModal(r); // paper/simulated order, honest, no real execution updateCardBtns(); } function openOrderModal(r){ const m=orderModel(r); const qty=state.orderQty!=null?state.orderQty:m.qty; if(!qty){const qi=$('ordQty');if(qi){qi.focus();}return;} const sideTxt=m.side==='buy'?'BUY':(r==='bear'?'SELL / HEDGE':'SELL'); setModalTitle('Confirm order'); $('modalBody').innerHTML=` `; const cf=$('modalConfirm'); cf.style.display=''; cf.className='tbtn '+(m.side==='buy'?'primary':'danger'); cf.textContent=m.side==='buy'?'Confirm Buy':'Confirm '+(r==='bear'?'Protect':'Sell'); cf.onclick=()=>{placeOrder({sym:m.sym,side:m.side,type:m.type,qty,price:Math.round(m.entry)});closeModal();}; state.lastFocus=document.activeElement; showModal(true); setTimeout(()=>{const c=$('modalConfirm');if(c)c.focus();},40); } function showModal(o){$('orderModal').classList.toggle('show',o);$('modalScrim').classList.toggle('show',o);$('orderModal').setAttribute('aria-hidden',String(!o));} function closeModal(){showModal(false);if(state.lastFocus&&state.lastFocus.focus)state.lastFocus.focus();} function setModalTitle(t){const el=$('modalTitle');if(el)el.textContent=t;$('orderModal').setAttribute('aria-label',t);} function successToast(o,status){ const t=document.createElement('div');t.className='toast'; t.innerHTML=`
${icon('check',22)}
Paper ${o.side==='buy'?'buy':'sell'} order, ${o.sym}${o.qty} qty @ ${o.price.toLocaleString('en-IN')} · ${o.type} · ${status} · simulated, no real order
`; $('toastWrap').appendChild(t); t.querySelector('[data-act=ok]').onclick=()=>dismiss(t); t._timer=setTimeout(()=>{if(document.body.contains(t))dismiss(t);},4500); } function placeOrder(o){ const status=o.type.startsWith('SL')?'Pending':(o.type==='MARKET'||o.type==='BRACKET')?'Filled':'Open'; state.orders.unshift({id:++ORDER_ID,...o,status,paper:true}); // SIMULATED, no real order is placed saveState(); // BUG FIX (2026-09-19): an order used to only exist in memory until some unrelated // action (e.g. a persona switch) happened to trigger a save - a reload right after // placing a trade silently lost the whole order. Persist the moment it's placed. if(typeof renderTrading==='function') renderTrading(); successToast(o,status); } function executeBracketOrder(p){ if(!p) return; const sym = p.sym || (state.trading && state.trading.sym) || state.selected; const m = tradeModel(sym); const side = p.side || 'buy'; const qty = (p.qty != null && p.qty > 0) ? p.qty : (m.qty || 1); const price = p.entry != null ? p.entry : (m.px || 100); const sl = p.sl; const target = p.target; playAudioFeedback('bracket'); placeOrder({ sym, side, qty, price, type: 'BRACKET', sl, target }); quickToast('1-Click Bracket Executed', `Paper ${side.toUpperCase()} ${qty} ${sym} filled @ ${cryptoFmt(price)}. TP: ${cryptoFmt(target)} | SL: ${cryptoFmt(sl)}`); } function checkBracketOrders(sym, ltp){ if(!state.orders || !state.orders.length || ltp == null || !isFinite(ltp)) return; let changed = false; state.orders.forEach(o => { if(!o.paper || o.type !== 'BRACKET' || o.status !== 'Filled' || o.sym !== sym) return; const isBuy = o.side === 'buy'; let hitType = null; if(isBuy){ if(o.target != null && ltp >= o.target) hitType = 'TP'; else if(o.sl != null && ltp <= o.sl) hitType = 'SL'; } else { if(o.target != null && ltp <= o.target) hitType = 'TP'; else if(o.sl != null && ltp >= o.sl) hitType = 'SL'; } if(hitType){ o.status = hitType === 'TP' ? 'Closed (TP)' : 'Closed (SL)'; o.exitPrice = ltp; o.exitTime = Date.now(); const pnlPct = isBuy ? ((ltp - o.price) / o.price * 100) : ((o.price - ltp) / o.price * 100); o.pnlPct = pnlPct; changed = true; if(hitType === 'TP'){ playAudioFeedback('success'); quickToast('Target Hit!', `Paper ${o.sym} target filled at ${cryptoFmt(ltp)}! +${pnlPct.toFixed(1)}%`); } else { quickToast('Stop Loss Filled', `Paper ${o.sym} SL exited at ${cryptoFmt(ltp)}. ${pnlPct.toFixed(1)}%`); } } }); if(changed){ saveState(); if(typeof renderTrading === 'function') renderTrading(); } } function flowModal(o){ setModalTitle(o.title||'Confirm'); $('modalBody').innerHTML=o.body||''; const cf=$('modalConfirm'); if(o.hideConfirm){ cf.style.display='none'; } else{ cf.style.display=''; cf.className='tbtn '+(o.danger?'danger':'primary'); cf.textContent=o.confirm||'Confirm'; cf.onclick=()=>{ const r=o.onConfirm?o.onConfirm($('modalBody')):true; if(r!==false) closeModal(); }; } if(o.wire) o.wire($('modalBody')); state.lastFocus=document.activeElement; showModal(true); setTimeout(()=>{ const first=$('modalBody').querySelector(o.focus||'input:not([disabled]),select,button,[tabindex="0"]'); (first||$('modalConfirm')).focus(); },50); } function cancelOrder(id){const o=state.orders.find(x=>x.id===id);if(o&&(o.status==='Pending'||o.status==='Open')){o.status='Cancelled';saveState();if(typeof renderTrading==='function') renderTrading();}} function applyRegime(regime){ state.displayed=regime; document.documentElement.dataset.regime=regime; document.querySelectorAll('[data-regime-btn]').forEach(b=>{const on=b.dataset.regimeBtn===regime;b.classList.toggle('active',on);b.setAttribute('aria-selected',on);}); $('fundsLabel').textContent=(PERSONA[state.persona||'trader']||PERSONA.trader).fundsLabel(regime); $('fundsVal').textContent=fundsText(); renderTopIndex(); renderRegimeBar(regime); renderWatchlist(regime); renderChart(regime); renderWidgetStack(); renderAlgo(); if(typeof renderTrading==='function') renderTrading(); if(typeof renderInvesting==='function') renderInvesting(); applyPaneWidths(); // keep any manual resize across regime switches flashRegime(); announce(`${regime.charAt(0).toUpperCase()+regime.slice(1)} regime, ${regime==='bull'?'markets trending up':regime==='bear'?'markets under pressure':'markets rangebound'}`); saveState(); } function flashRegime(){const s=$('regimeSweep');if(!s)return;s.classList.remove('go');void s.offsetWidth;s.classList.add('go');} const RV_SEL='.regime-bar,#tradingView,#investHub,#algoView,.pane-left,.chart-card,.panel,#orderPad,#contextModule'; const RV_META={ bull: {word:'BULL', tag:'Risk-on · Momentum'}, neutral:{word:'NEUTRAL', tag:'Wait for clarity'}, bear: {word:'BEAR', tag:'Protect capital'}, }; const prefersReduced=()=>window.matchMedia&&window.matchMedia('(prefers-reduced-motion:reduce)').matches; /* original mascot artwork (filled vector), charging bull · roaring bear · balance scale */ const MASCOTS={ bull:` `, bear:` `, neutral:` `, }; function rvCards(){return [...document.querySelectorAll(RV_SEL)];} function runStage(html,cls,dur){ const st=$('revealStage'); if(!st)return; clearTimeout(st._t); st.className='reveal-stage '+cls; st.innerHTML=html; void st.offsetWidth; st.classList.add('go'); st._t=setTimeout(()=>{st.classList.remove('go');st.className='reveal-stage';st.innerHTML='';},dur); } function buildMascot(r){ const m=RV_META[r], dirY=r==='bull'?-1:r==='bear'?1:0; const streaks=Array.from({length:8},(_,i)=>``).join(''); let pcl='';for(let i=0;i<16;i++){const x=(Math.random()*2-1); const tx=(x*(r==='neutral'?125:55)).toFixed(0); const ty=(dirY*(60+Math.random()*75)+(r==='neutral'?(Math.random()*2-1)*22:0)).toFixed(0); pcl+=``;} let smoke='';for(let i=0;i<7;i++){smoke+=``;} return `
${streaks}
${smoke}
${pcl}
${m.word}${m.tag}
`; } function buildPersonaFx(p){ const sub = p==='investor' ? 'Long-term wealth & DCA' : p==='algo' ? 'Systematic quantitative studio' : 'Active charts & trade signals'; return `
${icon(PERSONA[p].icon,34)} ${PERSONA[p].label}${sub}
`; } function cinematicRegime(r){ if(prefersReduced()||state.revealing){applyRegime(r);return;} state.revealing=true; rvCards().forEach((c,i)=>{c.style.setProperty('--rvd',(i*45)+'ms');c.classList.remove('rv-in');c.classList.add('rv-out');}); setTimeout(()=>{ applyRegime(r); runStage(buildMascot(r),'reveal-regime '+r,1450); },300); setTimeout(()=>{ rvCards().forEach((c,i)=>{c.classList.remove('rv-out');c.style.setProperty('--rvd',(i*55)+'ms');c.classList.add('rv-in');}); },840); setTimeout(()=>{ rvCards().forEach(c=>{c.classList.remove('rv-in');c.style.removeProperty('--rvd');}); state.revealing=false; },1700); } function cinematicPersona(p){ if(prefersReduced()||state.revealing){applyPersona(p,{user:true});return;} const gateOpen=$('personaGate')&&$('personaGate').classList.contains('show'); state.revealing=true; runStage(buildPersonaFx(p),'reveal-persona '+p,900); if(gateOpen){ applyPersona(p,{user:true}); rvCards().forEach((c,i)=>{c.style.setProperty('--rvd',(i*50)+'ms');c.classList.add('pm-in');}); setTimeout(()=>{rvCards().forEach(c=>{c.classList.remove('pm-in');c.style.removeProperty('--rvd');});state.revealing=false;},780); }else{ rvCards().forEach((c,i)=>{c.style.setProperty('--rvd',(i*38)+'ms');c.classList.add('pm-out');}); setTimeout(()=>{ applyPersona(p,{user:true}); rvCards().forEach((c,i)=>{c.classList.remove('pm-out');c.style.setProperty('--rvd',(i*50)+'ms');c.classList.add('pm-in');}); setTimeout(()=>{rvCards().forEach(c=>{c.classList.remove('pm-in');c.style.removeProperty('--rvd');});state.revealing=false;},760); },230); } } // Crypto trades 24/7, no NSE-style market-hours/Kite-connection status to show, so the header // clock strip stays hidden here. Live status is the Binance ticker banner (cryptoStatusBar()). function updateClock(){const el=$('mktStatus');if(el)el.hidden=true;const eng=$('hdrEngine');if(eng)eng.hidden=true;} const isInvestor=()=>state.persona==='investor'; const isAlgo=()=>state.persona==='algo'; // BUG FIX (2026-09-19): this used to hard-force p='algo' whenever CRYPTO_ONLY was true, regardless // of state.persona, which is exactly what kept Investing/Trading permanently unreachable. Reads // the real persona now; CRYPTO_ONLY only ever affects which market/data source is used (crypto), // never which of the three modes is active. function syncFab(){const p=state.persona||'algo'; document.querySelectorAll('[data-mode-btn]').forEach(b=>{const on=b.dataset.modeBtn===p;b.classList.toggle('active',on);b.setAttribute('aria-selected',on);});} function applyPersona(p,opts){ opts=opts||{}; if(PERSONA_KEYS.indexOf(p)<0)p='trader'; const changed=state.persona!==p; state.persona=p; document.documentElement.dataset.persona=p; applyPaneWidths(); // re-apply (3-pane) or clear (algo/ai single-pane) the grid width on every persona switch syncFab(); const gate=$('personaGate'); if(gate && !onboarding) gate.classList.remove('show'); // onboarding keeps the gate open to advance to the connect step // reset order-pad context so verbs/defaults match the new persona state.orderSide=null; state.orderQty=null; state.tradeFromChart=null; $('fundsLabel').textContent=PERSONA[p].fundsLabel(state.displayed); if(opts.user && changed && window.TPChart && TPChart.setTimeframe) TPChart.setTimeframe(PERSONA[p].chartTf); const r=state.displayed; renderRegimeBar(r); renderWatchlist(r); renderWidgetStack(); renderAlgo(); if(typeof renderTrading==='function') renderTrading(); if(typeof renderInvesting==='function') renderInvesting(); if(opts.user){ announce(`${PERSONA[p].label} mode, terminal retuned for ${p==='investor'?'long-term investing':'active trading'}`); const t=document.querySelector('.terminal'); if(t){t.classList.remove('persona-morph');void t.offsetWidth;t.classList.add('persona-morph');setTimeout(function(){t.classList.remove('persona-morph');},480);} } saveState(); } function startOnboarding(){ onboarding=true; const g=$('personaGate'); if(!g) return; if(CRYPTO_ONLY){ applyPersona('algo',{user:true}); obStep(2); g.classList.add('show'); return; } obStep(1); g.classList.add('show'); } function obStep(n){ const g=$('personaGate'); if(!g) return; g.dataset.step=String(n); g.querySelectorAll('.pg-dot').forEach((d,i)=>d.classList.toggle('on',i{ if(onboarding) renderOnboardConnect(); }).catch(()=>{}); const f=$('pgStep2').querySelector('[data-obfinish]'); if(f) f.focus(); } } function onboardPick(p){ applyPersona(p,{user:true}); obStep(2); } function renderOnboardConnect(){ const el=$('pgStep2'); if(!el) return; const botOff=!BOT.loaded||BOT.error; const running=BOT.status&&BOT.status.harnessRunning; const statusCard=botOff ?`
${icon('shield',16)}
Setting up your paper accountLive prices and the paper book connect automatically, no action needed.
` :`
Crypto engine ${running?'running':'ready'} Live Binance spot prices · 24/7 paper book · no exchange keys required.
`; const legend=`
● LIVE real Binance prices PAPER simulated fills, honest costs

zengtrade never fabricates numbers, prove your edge forward before going live.

`; el.innerHTML=`

Welcome to zengtrade Crypto

Backtest, forward-test, and paper-trade systematic strategies on live crypto prices, 24/7.

${statusCard}${legend}
`; } function finishOnboarding(){ onboarding=false; const g=$('personaGate'); if(g) g.classList.remove('show'); saveState(); // persona was already saved on pick; this also persists post-onboarding state if(state.lastFocus&&state.lastFocus.focus) try{state.lastFocus.focus();}catch(e){} } function quickToast(title,sub){ const t=document.createElement('div');t.className='toast'; t.innerHTML=`
${icon('wallet',22)}
${title}${sub}
`; $('toastWrap').appendChild(t); t.querySelector('[data-act=ok]').onclick=()=>dismiss(t); t._timer=setTimeout(()=>{if(document.body.contains(t))dismiss(t);},4000); } function ts(k,def){ return (state.toolState[k]!==undefined)?state.toolState[k]:def; } function secStats(items){return `
${items.map(s=>`
${s.g?``:`${s.l}`}${s.v}${s.s?`${s.s}`:''}
`).join('')}
`;} function secEmpty(ic,title,msg,cta){return `
${icon(ic,28)}${title}

${msg}

${cta||''}
`;} // Inline "what does this mean" tooltips on secStats() labels, wired to the real glossary already // shipped at /learn/glossary/{slug}/. Definitions below are copied verbatim from content/glossary.py's // `short` field (single source of truth) - if that text changes there, update it here too. This is // the DIY-simplicity lever: no human ever explains these numbers, so the product has to, in place. const GLOSS={ 'win-rate':'The percentage of trades that close profitably, informative, but meaningless without knowing the size of wins versus losses too.', 'profit-factor':'Gross profit divided by gross loss, a single number summarizing whether winners meaningfully outweigh losers, independent of win rate.', 'drawdown':"The decline from a portfolio's peak value to its lowest point since, the real, lived cost of a losing stretch.", 'sharpe-ratio':'A risk-adjusted return measure: average return divided by the volatility (standard deviation) of those returns, higher means more return per unit of risk taken.', 'cagr':'The smoothed annual growth rate that would take a starting value to an ending value over a period, assuming steady compounding, useful for comparing returns across different timeframes.', 'market-regime':'The prevailing character of a market, trending up, trending down, or range-bound/choppy, that determines which strategies actually have an edge right now.', }; function initGlossTips(){ let tip=null; function ensureTip(){ if(!tip){ tip=document.createElement('div'); tip.className='gloss-tip'; tip.setAttribute('role','tooltip'); document.body.appendChild(tip); } return tip; } function place(btn){ const slug=btn.dataset.g, def=GLOSS[slug]; if(!def) return; const t=ensureTip(), r=btn.getBoundingClientRect(); t.innerHTML=`${def} Full definition →`; t.classList.add('show'); const tw=Math.min(260, window.innerWidth-24); t.style.maxWidth=tw+'px'; const left=Math.min(Math.max(8,r.left), window.innerWidth-tw-8); let top=r.bottom+8; if(top+120>window.innerHeight) top=r.top-8-t.offsetHeight; t.style.left=left+'px'; t.style.top=top+'px'; } function hide(){ if(tip) tip.classList.remove('show'); } document.addEventListener('mouseover',e=>{const b=e.target.closest('.ss-help'); if(b) place(b);}); document.addEventListener('mouseout',e=>{const b=e.target.closest('.ss-help'); if(b) hide();}); document.addEventListener('focusin',e=>{const b=e.target.closest('.ss-help'); if(b) place(b);}); document.addEventListener('focusout',e=>{const b=e.target.closest('.ss-help'); if(b) hide();}); document.addEventListener('click',e=>{const b=e.target.closest('.ss-help'); if(b){ e.preventDefault(); place(b); } else if(!e.target.closest('.gloss-tip')) hide();}); document.addEventListener('keydown',e=>{ if(e.key==='Escape') hide(); }); } function lcMsg(s){ return s==='paper'?'Live data, simulated fills, no real money. Building forward evidence.' : s==='paused'?'Paused, no new entries; open paper positions are kept.' : (s===null||s==='off')?'Stopped, open paper positions square off next cycle; nothing else trades it.' : 'Updated.'; } async function setStrategyState(id, stateVal, title){ try{ let r=null; try{ r=await fetch(BOT_API+'/api/strategy',{method:'POST',headers:{'Content-Type':'application/json'}, body:JSON.stringify({id,state:stateVal})}).then(x=>x.json()); }catch(err){ r=null; } if(r&&r.error){ if(r.upgrade){ quickToast('Upgrade to Pro', r.error); setTimeout(function(){ location.href=r.upgrade; }, 700); return r; } quickToast('Action failed', r.error); return r; } if(r&&r.locked){ quickToast('Live locked '+'🔒', r.reason||'Arm ALLOW_LIVE on the bot machine to go live.'); } else { quickToast(title||'Updated', lcMsg(stateVal)); CRYPTOMON.data=CRYPTOMON.data||{strategies:[]}; CRYPTOMON.data.strategies=CRYPTOMON.data.strategies||[]; let st=CRYPTOMON.data.strategies.find(x=>x.id===id); if(!st){ const meta=CRYPTO_STRATEGIES.find(x=>x.bid===id)||{name:id,cat:'Breakout',pair:'BTCUSDT'}; st={id,name:meta.name,pair:meta.pair,deployed:stateVal==='paper',status:stateVal,pnl:0,trades:0}; CRYPTOMON.data.strategies.push(st); } else { st.deployed=(stateVal==='paper'); st.status=stateVal; } CRYPTOMON.loaded=true; } await Promise.all([loadBotData(), loadCryptoMonitor()]); if(typeof renderAlgo==='function') renderAlgo(); return r; }catch(e){ quickToast('Action failed','The trading engine is temporarily unreachable, try again shortly.'); } } function algoDeploy(a){ if(!a.wired){ quickToast('Not deployable yet', `${a.name} has no live engine, backtest/validate it first.`); return; } flowModal({title:'Deploy in Paper, '+a.name, confirm:'Deploy in Paper', body:`
${esc(a.name)}${esc(a.cat)} · ${esc(a.risk)}
${esc(a.risk)}
Best regime${esc(a.bestRegime||'-')}
Validation${a.vstatus==='validated'?'Validated':'Candidate'}

${icon('shield',13)}Risk-free. Paper deploy runs this strategy on live Binance data with simulated fills: no real orders, no money at risk. Your bot harness starts trading it; closed-trade P&L builds toward the Go-Live gate (≥${BOT.nudgeMin||10} profitable trades). You can Pause or Stop anytime.

`, onConfirm(){ setStrategyState(a.id,'paper','Deployed, '+a.name); } }); } function lcStop(a){ const flatten=a.sub==='live'; flowModal({title:(flatten?'Stop & Flatten, ':'Stop, ')+a.name, confirm:flatten?'Stop & Flatten':'Stop', danger:true, body:`

${icon('alert',13)}${flatten ? `This squares off real positions and stops the strategy. Real-money action, confirm you want to exit now.` : `Stops paper-trading ${esc(a.name)}. Any open paper positions are squared off next cycle. You can redeploy anytime.`}

`, onConfirm(){ setStrategyState(a.id,'off','Stopped, '+a.name); }}); } function cryptoLibDeploy(bid){ const c=CRYPTO_STRATEGIES.find(x=>x.bid===bid); if(!c){ quickToast('Engine loading','Wait for the strategy catalog to load from the API.'); return; } if(!c.wired){ quickToast('Not deployable yet', `${c.name} has no live engine on the cloud worker yet, backtest/validate it first.`); return; } const m=((CRYPTOMON.data&&CRYPTOMON.data.strategies)||[]).find(x=>x.id===bid); if(!CRYPTOMON.loaded&&!CRYPTOMON.busy) loadCryptoMonitor(); const a={ id:bid, name:c.name, cat:c.cat+' · '+c.pair, risk:c.risk, wired:true, bestRegime:null, vstatus:'validated', sub:(m&&m.deployed)?'paper':null }; if(a.sub==='paper') lcStop(a); else algoDeploy(a); } const newLayoutId=()=>'L'+Date.now().toString(36)+(_lid++); function customLayouts(){ if(!Array.isArray(state.customLayouts))state.customLayouts=[]; return state.customLayouts; } function activeCustom(){ const a=customLayouts(); let cl=a.find(l=>l.id===state.activeCustom); if(!cl){cl=a[0]||null; state.activeCustom=cl?cl.id:null;} return cl; } function newTabId(){ return 't'+Math.random().toString(36).slice(2,8)+Date.now().toString(36).slice(-3); } function tabsOf(cl){ if(!cl) return []; if(!Array.isArray(cl.tabs)){ cl.tabs=[{id:newTabId(),name:'Main',cards:Array.isArray(cl.cards)?cl.cards:[],sync:{A:0,B:1}}]; cl.activeTab=cl.tabs[0].id; } if('cards' in cl) delete cl.cards; // drop the migrated flat list cl.tabs.forEach(t=>{ if(!t.id)t.id=newTabId(); if(!Array.isArray(t.cards))t.cards=[]; if(typeof t.name!=='string')t.name='Tab'; if(!t.sync||typeof t.sync!=='object')t.sync={A:0,B:1}; }); if(!cl.tabs.length) cl.tabs.push({id:newTabId(),name:'Main',cards:[],sync:{A:0,B:1}}); if(!cl.tabs.find(t=>t.id===cl.activeTab)) cl.activeTab=cl.tabs[0].id; return cl.tabs; } function activeTab(){ const cl=activeCustom(); if(!cl) return null; const ts=tabsOf(cl); return ts.find(t=>t.id===cl.activeTab)||ts[0]; } const isDesk=()=>state.persona==='trader'&&(state.layout==='options'||state.layout==='futures'); const isCenterTakeover=()=>state.persona==='trader'&&['options','futures','build'].indexOf(state.layout)>=0; function renderDeskView(){ const v=$('deskView'); if(!v) return; v.innerHTML=''; } function canvasCatalog(){ if(_canvasCat) return _canvasCat; _canvasCat=(WIDGET_CATALOG.trader||[]).map(w=>({key:w.key,name:w.name,icon:w.icon,span:1,render:w.render})); return _canvasCat; } const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); function sanInstr(s){ return typeof s==='string' ? s.replace(/[<>"'`]/g,'') : s; } const BOT_API=(()=>{ const h=location.hostname; if(h==='localhost'||h==='127.0.0.1'||h==='::1'||h==='') return 'http://localhost:8756'; return ''; // production: SAME-ORIGIN, the tunnel routes /api/* to the bot, so the Cloudflare // Access cookie rides along automatically. No CORS, no CSP change, no secret in JS. })(); let BOT={loaded:false,connected:false,status:null,paperMode:true,error:false,chains:{},chainExp:{},futures:null}; let ALGOS=[]; // no fabricated strategies, loadBotData() fills this from the real crypto engine async function loadBotData(){ try{ const reqs=CRYPTO_ONLY ?[fetch(BOT_API+'/api/strategies').then(r=>r.json()).catch(()=>({strategies:[]})), fetch(BOT_API+'/api/status').then(r=>r.json()).catch(()=>({connected:false})), fetch(BOT_API+'/api/trades').then(r=>r.json()).catch(()=>({trades:[]})), Promise.resolve({stopped:[]})] :[fetch(BOT_API+'/api/strategies').then(r=>r.json()).catch(()=>({strategies:[]})), fetch(BOT_API+'/api/status').then(r=>r.json()).catch(()=>({connected:false})), fetch(BOT_API+'/api/trades').then(r=>r.json()).catch(()=>({trades:[]})), fetch(BOT_API+'/api/stopped').then(r=>r.json()).catch(()=>({stopped:[]}))]; const [s,st,tr,sp]=await Promise.all(reqs); BOT.trades=tr.trades||[]; BOT.stopped=sp.stopped||[]; BOT.stoppedTotal=sp.totalFlattenPnl||0; BOT.connected=CRYPTO_ONLY?!!(st.ok||st.connected):!!st.connected; BOT.live=CRYPTO_ONLY?BOT.connected:!!st.connected; BOT.status=st; BOT.paperMode=CRYPTO_ONLY?true:(s.paperMode!==false); BOT.updated=s.updated; BOT.error=false; BOT.segments=s.segments||[{id:'crypto',label:'Crypto Spot',note:''}]; if(s.strategies&&s.strategies.length){ ALGOS=s.strategies.map(x=>({ id:x.id,name:x.name,cat:x.cat,segment:x.segment,win:x.win,minCap:x.minCap,risk:x.risk, product:x.product,vstatus:x.status,bestRegime:x.bestRegime,regimeFit:x.regimeFit,requires:x.requires, desc:x.desc,sharpe:x.oos_sharpe,totalRet:x.totalRet,trades:x.trades,dd:x.dd||3, style:x.style,minDeploy:x.minDeploy,riskPerTrade:x.riskPerTrade,maxDD:x.maxDD, // capital + risk + style model verdict:x.verdict,paperPnl:x.paperPnl,realisedPnl:x.realisedPnl,openPnl:x.openPnl,openPositions:x.openPositions,live:x.live,real:true, nudge:x.nudge,fwdTrades:x.fwdTrades,readyExceptCapital:x.readyExceptCapital,blockers:x.blockers,nudgeMsg:x.nudgeMsg,positions:x.positions, sub:x.sub,deployed:x.deployed,wired:x.wired, // lifecycle: sub = paper|paused|live|null status:x.live?'live':'idle', cap:x.live?x.minCap:0, cagr:x.totalRet })); BOT.nudgeMin=s.nudgeMinTrades||10; } }catch(e){ BOT.error=true; } BOT.loaded=true; } // Structural signature: everything that, if changed, genuinely needs a heavy panel re-render. // Deliberately EXCLUDES live prices/P&L, those are patched in place by the 2s loops + 1s clock, // so the 30s poll no longer repaints the whole screen (that was the idle "flickers on its own"). function marketSig(){ const m=BOT.market||{}; const funds=(m.funds!=null)?Math.round(m.funds):'x'; const hold=BOT.holdings?(Array.isArray(BOT.holdings)?BOT.holdings.length:Object.keys(BOT.holdings).length):0; const reg=(m.engine&&m.engine.regime)||state.displayed; return [BOT.live?1:0, BOT.connected?1:0, reg, funds, hold, state.selected||'', state.layout||'', state.persona||'', state.panelTab||'', (state.desk&&state.desk.view)||'', SYMS.map(s=>itemKey(s)).join(','), (typeof algoLiveSig==='function'?algoLiveSig():'')].join('~'); } async function loadMarket(){ try{ const m=await fetch(BOT_API+'/api/market').then(r=>r.json()); BOT.live=!!(m && m.real && !m.error && m.engine); BOT.market=BOT.live?m:null; if(BOT.live){ const [q,h]=await Promise.all([ fetch(BOT_API+'/api/uquotes?keys='+encodeURIComponent(watchKeys().join(','))).then(r=>r.json()).catch(()=>({quotes:{}})), fetch(BOT_API+'/api/holdings').then(r=>r.json()).catch(()=>null) ]); BOT.quotes=(q&&q.quotes)||{}; // overlay REAL ltp/chg onto each watchlist instrument BY KEY (any segment), no synthetic // prices. s.live marks whether THIS instrument has a real quote; a null quote (delisted / // illiquid / market-closed) stays BLANK, never the stale catalog price. SYMS.forEach(s=>{const r=BOT.quotes[itemKey(s)]; if(r&&r.ltp!=null){s.ltp=r.ltp; if(r.chg!=null)s.chg=r.chg; s.live=true;} else {s.live=false;}}); BOT.holdings=(h&&!h.error)?h:null; } else { BOT.quotes=null; BOT.holdings=null; BOT.tickerLive={}; BOT._regimeSynced=false; SYMS.forEach(s=>{s.live=false;}); } }catch(e){ BOT.live=false; BOT.market=null; BOT.quotes=null; BOT.holdings=null; BOT.tickerLive={}; BOT._regimeSynced=false; SYMS.forEach(s=>{s.live=false;}); } // Adaptive risk: when the LIVE regime actually flips, re-apply every "Adapt"-enabled // strategy's stand-aside / re-engage rule (paper only; never silent on real money). if(BOT.live && BOT.market && BOT.market.engine){ const reg=BOT.market.engine.regime; if(BOT._adaptRegime && BOT._adaptRegime!==reg && typeof enforceAdapt==='function') enforceAdapt(); BOT._adaptRegime=reg; } // Regime panel mirrors the LIVE Kite signals (unless the user is running a what-if): feed the // real signals into the sliders so the gauge, composite and AUTO switching reflect the real market. if(BOT.live && !state.simOverride && typeof syncSlidersFromLive==='function' && syncSlidersFromLive()){ if(!BOT._regimeSynced){ BOT._regimeSynced=true; recompute({silent:true}); } // adopt live regime quietly on first connect else recompute(); // later polls → AUTO reacts to genuine flips } else if(typeof renderEngineSrc==='function'){ renderEngineSrc(); } if(BOT.live) connectStream(); else disconnectStream(); // sub-second push when live; keeps symset in sync applyFunds(); updateClock(); // renderTopIndex + renderRegimeBar self-guard (patch in place / skip when unchanged) → cheap every poll. if(typeof renderTopIndex==='function') renderTopIndex(); if(typeof renderRegimeBar==='function') renderRegimeBar(state.displayed); // The heavy panels do FULL innerHTML rebuilds, so only run them when something STRUCTURAL changed // (regime, connection, funds, holdings, symbol set, selection, layout/persona/tab, a trade opening // or closing). Live prices stay fresh via the 2s loops (applyTickDom / patchTape / patchAlgoLive). const _msig=marketSig(); if(_msig!==BOT._mktSig){ BOT._mktSig=_msig; if(typeof renderWatchlist==='function') renderWatchlist(state.displayed); if(typeof renderWidgetStack==='function') renderWidgetStack(); if(typeof renderDeskView==='function') renderDeskView(); // refresh canvas cards (movers/pnl/heatmap) with live SYMS if(typeof renderChart==='function') renderChart(state.displayed); // pull real candles once live if(typeof renderPanel==='function') renderPanel(state.displayed); // holdings/positions panels -> real if(typeof isAlgo==='function' && isAlgo() && typeof renderAlgo==='function') renderAlgo(); } } /* ---- REAL-TIME ticks via Kite WebSocket (/api/ticks, KiteTicker-fed). Updates only the watchlist price cells + chart last candle IN PLACE (no full re-render → drag, sort and selection survive). Self-gates on live + visible tab. ---- */ /* ===== Sub-second PUSH via Server-Sent Events (bot→browser stream) ===== The bot holds one Kite WebSocket and pushes each tick to the browser over /api/stream (EventSource) the instant it lands, no 2s polling lag. The 2s poll below stays as an automatic fallback: it only fires when the stream isn't actively delivering (first paint, stream dropped, or EventSource unsupported), so prices are never stale and never doubled. */ const STREAM={es:null, syms:'', on:false, last:0}; function streamSyms(){ return liveKeys().join(','); } // EXCH:TS keys: watchlist ∪ rolling-tape indices function connectStream(){ if(!BOT.live || typeof EventSource==='undefined') return; const keys=streamSyms(); if(STREAM.es && STREAM.syms===keys) return; // already streaming this exact set disconnectStream(); STREAM.syms=keys; try{ const es=new EventSource(`${BOT_API}/api/stream?keys=${encodeURIComponent(keys)}`); es.onopen=()=>{ STREAM.on=true; STREAM.last=Date.now(); }; es.onmessage=ev=>{ try{ const d=JSON.parse(ev.data); if(d&&d.ticks){ applyTicks(d.ticks); STREAM.on=true; STREAM.last=Date.now(); if(d.stream)BOT.tickStream=d.stream; } }catch(e){} }; es.onerror=()=>{ STREAM.on=false; }; // EventSource auto-reconnects; the poll covers the gap STREAM.es=es; }catch(e){ STREAM.on=false; } } function disconnectStream(){ if(STREAM.es){ try{STREAM.es.close();}catch(e){} } STREAM.es=null; STREAM.on=false; STREAM.syms=''; } // Apply a PARTIAL tick update (only the instruments that ticked) keyed by EXCH:TS: push stream. function applyTicks(ticks){ if(!ticks) return; const dir={}; for(const key in ticks){ const t=ticks[key], s=byKey(key); if(s&&t&&t.ltp!=null){ dir[key]=Math.sign(t.ltp-(s.ltp||t.ltp)); s.ltp=t.ltp; if(t.chg!=null)s.chg=t.chg; s.live=true; } } applyTickDom(ticks,dir); applyTickerTicks(ticks); // same ticks drive the rolling tape (indices + pinned stocks) in lock-step } let TICK_BUSY=false; async function loadTicks(){ if(TICK_BUSY || !BOT.live || document.visibilityState!=='visible') return; if(STREAM.on && Date.now()-STREAM.last < 6000) return; // push stream is live → skip the poll TICK_BUSY=true; try{ const d=await fetch(`${BOT_API}/api/ticks?keys=${encodeURIComponent(liveKeys().join(','))}`).then(r=>r.json()); if(d&&d.ticks){ const dir={}; SYMS.forEach(s=>{const k=itemKey(s),t=d.ticks[k]; if(t&&t.ltp!=null){ dir[k]=Math.sign(t.ltp-(s.ltp||t.ltp)); s.ltp=t.ltp; if(t.chg!=null)s.chg=t.chg; s.live=true; } else { s.live=false; }}); applyTickDom(d.ticks,dir); applyTickerTicks(d.ticks); // refresh the rolling tape's indices on the same poll BOT.tickStream=d.stream; } }catch(e){} TICK_BUSY=false; } function applyTickDom(ticks,dir){ document.querySelectorAll('#wlRows .wl-row').forEach(row=>{ const key=row.dataset.key, t=ticks[key]; if(!t||t.ltp==null) return; const ltpEl=row.querySelector('.wl-ltp'), chgEl=row.querySelector('.wl-chg'); if(ltpEl){ ltpEl.textContent=(+t.ltp).toLocaleString('en-IN'); ltpEl.classList.remove('muted'); const dr=dir[key]; if(dr){ltpEl.classList.remove('tick-up','tick-dn');void ltpEl.offsetWidth;ltpEl.classList.add(dr>0?'tick-up':'tick-dn');} } if(chgEl){ const c=t.chg!=null?t.chg:0; chgEl.textContent=pct(c); chgEl.className='wl-chg num '+cls(c); } }); // nudge the chart's last candle for the SELECTED instrument const si=state.selected&&bySym(state.selected); const t=si&&ticks[itemKey(si)]; if(t&&t.ltp!=null&&window.TPChart&&TPChart.tick) TPChart.tick(si.sym,t.ltp); } /* ---- REAL 5-level market depth (/api/depth, full Kite quote().depth). Targeted body update so it never rebuilds the whole widget stack. Empty levels show " (" (honest) the order book is thin/empty after 15:30; all 5 levels fill during market hours). ---- */ let DEPTH_BUSY=false; async function loadDepth(sym){ if(!BOT.live || !sym || DEPTH_BUSY) return; DEPTH_BUSY=true; try{ const d=await fetch(`${BOT_API}/api/depth?symbol=${encodeURIComponent(sym)}`).then(r=>r.json()); BOT.depth=(d&&!d.error)?d:{symbol:sym,bids:[],asks:[],error:(d&&d.error)||'no data'}; }catch(e){ BOT.depth={symbol:sym,bids:[],asks:[],error:'fetch failed'}; } DEPTH_BUSY=false; const body=document.querySelector('.wg-card[data-wkey="depth"] .wg-body'); if(body && BOT.depth.symbol===(state.selected||'RELIANCE')){ body.innerHTML = BOT.depth.error ? `
No depth for ${esc(BOT.depth.symbol)}, ${esc(BOT.depth.error)}.
` : depthLadderHtml(BOT.depth); } } function depthLadderHtml(d){ let rows=''; for(let i=0;i<5;i++){const b=d.bids[i]||{}, a=d.asks[i]||{}; rows+=`
${b.price?b.price.toFixed(1):'-'}` +`${b.qty?b.qty.toLocaleString('en-IN'):''}` +`${a.qty?a.qty.toLocaleString('en-IN'):''}` +`${a.price?a.price.toFixed(1):'-'}
`;} const tb=d.totalBuyQty||0, ts=d.totalSellQty||0; const foot=`
${esc(d.symbol)} · bid ${tb.toLocaleString('en-IN')} · ask ${ts.toLocaleString('en-IN')}${(tb+ts)===0?' · book opens 09:15':''}
`; return `
BidQtyQtyAsk
${rows}${foot}`; } // FAST, lean poll for the live Monitor, every strategy's real-time P&L from /api/monitor (cached ~1ms). async function loadMonitor(){ try{ const m=await fetch(BOT_API+'/api/monitor').then(r=>r.json()); if(m&&m.running){ BOT.monitor=m; const byId={}; m.running.forEach(r=>byId[r.id]=r); ALGOS.forEach(a=>{const r=byId[a.id]; if(r){ a.paperPnl=r.paperPnl; a.openPnl=r.openPnl; a.realisedPnl=r.realisedPnl; // DON'T clobber a.live here: that conflated "subscribed" with "monitored" and raced // loadBotData every 2s (the old P&L flip-flop). All algo tabs filter on a.deployed (stable, // from /api/strategies); /api/monitor now returns only subscribed strategies, so they agree. a.openPositions=r.openPositions; a.fwdTrades=r.fwdTrades; a.positions=r.positions; // forward-test ACCURACY (real out-of-sample, parsed from closed paper trades) a.fwdWins=r.fwdWins; a.fwdLosses=r.fwdLosses; a.fwdWinPct=r.fwdWinPct; a.fwdProfitFactor=r.fwdProfitFactor; a.fwdAvgWin=r.fwdAvgWin; a.fwdAvgLoss=r.fwdAvgLoss; a.fwdExpectancy=r.fwdExpectancy; }}); } }catch(e){} } function fundsText(){ if(BOT.live && BOT.market && typeof BOT.market.funds==='number') return inr(BOT.market.funds); return '-'; } function applyFunds(){ const e=$('fundsVal'); if(e) e.textContent=fundsText(); } // Map a ticker/headline name to its REAL quote from /api/market (indices+commodities) or /api/quotes (stocks). const MKT_INDEX={'NIFTY 50':['indices','NIFTY 50'],'SENSEX':['indices','SENSEX'],'BANK NIFTY':['indices','NIFTY BANK'], 'FIN NIFTY':['indices','NIFTY FIN SERVICE'],'GOLD':['commodities','GOLD'],'SILVER':['commodities','SILVER'],'CRUDE OIL':['commodities','CRUDEOIL']}; // EXCH:TS tick keys for the headline instruments. Indices stream over the Kite WS (sub-second); // MCX commodities don't, so they have no tick key and fall back to the 30s /api/market snapshot. const TICKER_TICK_KEY={'NIFTY 50':'NSE:NIFTY 50','SENSEX':'BSE:SENSEX','BANK NIFTY':'NSE:NIFTY BANK','FIN NIFTY':'NSE:NIFTY FIN SERVICE'}; // {tickKey -> tickerName} for every ticker item that can stream live (indices + any pinned, watched stock). function tickerTickMap(){ const out={}; (state.ticker.items||[]).forEach(name=>{ const k=TICKER_TICK_KEY[name]; if(k){ out[k]=name; return; } // headline index const s=bySym(name); if(s) out[itemKey(s)]=name; // a stock pinned to the tape (rides its watchlist quote) }); return out; } // All EXCH:TS keys the live tick path should subscribe/poll: watchlist ∪ ticker indices. function liveKeys(){ const set=new Set(watchKeys()); Object.keys(tickerTickMap()).forEach(k=>set.add(k)); return [...set]; } // Fold a fresh tick payload into the ticker's live overlay, then patch the tape in place. Returns true if anything changed. // Iterates the ticker's OWN keys (not the payload) so a poll that returns a null/stale quote drops the overlay entry, // indices then fall back to the 30s snapshot, a stale pinned stock blanks honestly. SSE partials (key absent) are left alone. function applyTickerTicks(ticks){ if(!ticks) return false; const map=tickerTickMap(); let changed=false; for(const key in map){ const name=map[key], t=ticks[key]; if(t===undefined) continue; // not in this (partial SSE) payload → leave as-is if(t && t.ltp!=null){ (BOT.tickerLive||(BOT.tickerLive={}))[name]={ltp:t.ltp,chg:t.chg!=null?t.chg:0}; changed=true; } else if(BOT.tickerLive && BOT.tickerLive[name]!=null){ delete BOT.tickerLive[name]; changed=true; } // source went stale } if(changed) patchTopIndex(); return changed; } function realQuote(name){ if(!BOT.live) return null; // 1) live tick overlay (indices stream sub-second; refreshed every tick by the fast path) const lv=BOT.tickerLive&&BOT.tickerLive[name]; if(lv&&lv.ltp!=null) return {ltp:lv.ltp,chg:lv.chg}; // 2) headline index/commodity from the latest /api/market snapshot (commodities don't tick → 30s refresh) const m=MKT_INDEX[name]; if(m){ if(!BOT.market) return null; const o=(BOT.market[m[0]]||{})[m[1]]; return (o&&o.ltp!=null)?{ltp:o.ltp,chg:o.chgPct}:null; } // 3) a stock pinned to the tape → its live watchlist quote (SYMS carry tick-updated ltp/chg) const s=bySym(name); return (s&&s.live&&s.ltp!=null)?{ltp:s.ltp,chg:s.chg||0}:null; } // Manual reconnect: trigger the headless TOTP re-login, then refresh everything. async function botReconnect(){ if(BOT.reconnecting) return; BOT.reconnecting=true; if(isAlgo()&&typeof renderAlgo==='function') renderAlgo(); let res={}; try{ res=await fetch(BOT_API+'/api/relogin',{method:'POST'}).then(r=>r.json()); }catch(e){ res={ok:false,error:'bot API offline'}; } BOT.reconnecting=false; if(res&&res.ok){ announce('Kite reconnected'); } else { announce('Reconnect failed'); quickToast&&quickToast('Couldn’t auto-reconnect', (res&&res.error)||'Run python3 login.py in the bot folder.'); } await loadBotData(); await loadMarket(); // refresh status + prices either way if(isAlgo()&&typeof renderAlgo==='function') renderAlgo(); if(onboarding) renderOnboardConnect(); // reflect the new connection state in the wizard } document.addEventListener('click',e=>{ if(e.target.closest('[data-relogin]')) botReconnect(); }); /* ---- Live patching: update only the ticking values in place (no full re-render → no flicker) ---- */ function algoLiveSig(){ // the STRUCTURE that, if it changes (a trade opens/closes, a strategy goes live), needs a full re-render return (state.algo&&state.algo.view)+'~'+(state.algo&&state.algo.exec)+'~'+ ALGOS.map(a=>`${a.id}.${a.live?1:0}.${a.openPositions||0}.${a.fwdTrades||0}.${(a.positions||[]).map(p=>p.sym).join(',')}`).join('|'); } function patchAlgoLive(){ const exec=(state.algo&&state.algo.exec)||'paper'; const setNum=(el,v)=>{ if(!el)return; el.textContent=sgn(v||0); el.classList.remove('up','down'); el.classList.add(cls(v||0)); }; let total=0, totalR=0, totalU=0, depTotal=0; // total = actively-running IN THE ACTIVE CLASS (Monitor); totalR/U = its realised/unrealised split; depTotal = all deployed (Forward Test) const brk={equity:0,options:0,futures:0}; let overall=0; // per-class + combined (all classes) for the Monitor breakdown ALGOS.forEach(a=>{ const v=exec==='live'?(a.livePnl||0):(a.paperPnl||0); if(a.live && inScope(a)){ total+=v; totalR+=(a.realisedPnl||0); totalU+=(a.openPnl||0); } // scoped to the Equity/Options/Futures toggle, matches the list shown if(a.deployed) depTotal+=v; if(a.deployed){ const ik=algoInstr(a); if(brk[ik]!=null) brk[ik]+=v; overall+=v; } document.querySelectorAll('[data-live-pnl="'+a.id+'"]').forEach(el=>setNum(el,v)); const sub=document.querySelector('[data-live-sub="'+a.id+'"]'); if(sub) sub.textContent=(a.fwdTrades||a.openPositions||a.realisedPnl||a.openPnl)?`R ${sgn(a.realisedPnl||0)} · U ${sgn(a.openPnl||0)}`:'no trades yet'; // both realised & unrealised, live (a.positions||[]).forEach(p=>{ if(p.entry==null) return; const key=a.id+'::'+p.sym; const ltp=document.querySelector('[data-live-ltp="'+key+'"]'); if(ltp) ltp.textContent=p.ltp!=null?p.ltp.toLocaleString('en-IN'):'-'; const up=document.querySelector('[data-live-upnl="'+key+'"]'); if(up){ up.textContent=sgn(p.unreal)+(p.chgPct!=null?` · ${p.chgPct>=0?'+':''}${p.chgPct}%`:''); up.classList.remove('up','down'); up.classList.add(cls(p.unreal)); } }); }); document.querySelectorAll('[data-live="monReal"]').forEach(el=>setNum(el,totalR)); document.querySelectorAll('[data-live="monUnreal"]').forEach(el=>setNum(el,totalU)); document.querySelectorAll('[data-live="monNet"]').forEach(el=>setNum(el,totalR+totalU)); document.querySelectorAll('[data-live="algoTotal"]').forEach(el=>setNum(el,depTotal)); document.querySelectorAll('[data-live="monOverall"]').forEach(el=>setNum(el,overall)); document.querySelectorAll('[data-live="brkEquity"]').forEach(el=>setNum(el,brk.equity)); document.querySelectorAll('[data-live="brkOptions"]').forEach(el=>setNum(el,brk.options)); document.querySelectorAll('[data-live="brkFutures"]').forEach(el=>setNum(el,brk.futures)); let anReal=0, anOpen=0; ALGOS.forEach(a=>{ if(a.live){ anReal+=(a.realisedPnl||0); anOpen+=(a.openPnl||0); } }); document.querySelectorAll('[data-live="anRealised"]').forEach(el=>setNum(el,anReal)); document.querySelectorAll('[data-live="anOpen"]').forEach(el=>setNum(el,anOpen)); document.querySelectorAll('[data-live="anBook"]').forEach(el=>setNum(el,total)); // Moonshot Mission banner (computed inline in missionBanner, so patch its live numbers here) const ms=ALGOS.find(a=>a.id==='moonshot'); if(ms){ const START=5000, TARGET=5e10, eq=START+(ms.paperPnl||0), mult=eq/START; const prog=Math.max(0,Math.min(100,Math.log(Math.max(eq,1)/START)/Math.log(TARGET/START)*100)); document.querySelectorAll('[data-live="msnEq"]').forEach(el=>{ el.textContent=inrShort(eq); el.className=(ms.paperPnl||0)>=0?'up':'down'; }); document.querySelectorAll('[data-live="msnMult"]').forEach(el=>{ el.textContent=mult>=1?mult.toFixed(mult>=100?0:2)+'× start':'−'+((1-mult)*100).toFixed(1)+'%'; }); document.querySelectorAll('[data-live="msnFill"]').forEach(el=>{ el.style.width=prog.toFixed(4)+'%'; }); document.querySelectorAll('[data-live="msnProg"]').forEach(el=>{ el.textContent=prog.toFixed(prog<1?4:2)+'% of the way (log scale)'; }); } } /* ============================================================ STRATEGY LIBRARY: every strategy family, as honest, browseable, educational entries. Risk-first: each leads with how it FAILS and the survival guard, not just the upside. Entries are "Candidate" (recognised, not yet engine-backtested) until a backend-validated strategy of the same id graduates them. The whole survival ethos: pick what fits TODAY's regime, stand aside when nothing does. ============================================================ */ const STRAT_FAMILIES=[ ['trend', 'Trend / Momentum', 'trendUp', 'Ride a move that has already started; lose small when it reverses.'], ['meanrev', 'Mean Reversion', 'repeat', 'Fade a stretched price back to its average. Wins often, must cap the rare big loss.'], ['breakout', 'Breakout / Volatility','bolt', 'Enter as price escapes a range on expanding volume.'], ['time', 'Time / Calendar', 'clock', 'The clock is the signal, time-of-day, expiry, day-of-week, auto square-off.'], ['statarb', 'Stat-Arb / Pairs', 'swap', 'Trade the spread between two related instruments, not the market direction.'], ['event', 'Event-Driven', 'flag', 'Position around a scheduled catalyst, results, rebalances, news.'], ['sentiment', 'Sentiment / OI / Flow','spark', 'Read positioning, open interest, PCR, max-pain, FII/DII flows.'], ['factor', 'Factor / Rotation', 'pie', 'Rotate a basket by a persistent edge, momentum, low-vol, quality, sector.'], ['ml', 'ML / Regime', 'cpu', 'A model picks the signal or switches the active strategy by regime.'], ['opt_dir', 'Options · Directional','target', 'Express a view with limited, known risk using long options or debit spreads.'], ['opt_income', 'Options · Income', 'wallet', 'Sell premium for steady credit, capped upside, must defend the tails.'], ['opt_vol', 'Options · Volatility', 'scale', 'Trade volatility itself, straddles, calendars, delta-neutral theta.'], ['arb', 'Cash-Futures / Arb', 'scissors','Lock a near-riskless spread between cash & futures or across expiries.'], ]; const SEG_LABEL={equity:'Equity',index:'Index F&O',fno:'Stock F&O',options:'Options',any:'Any market'}; /* ── Instrument × holding-style bifurcation (Library navigation) ────────────── Primary class is DERIVED from the catalog seg (equity→Equity, options→Options, index|fno→Futures). Holding style is an explicit per-strategy tag, a judgement call about how the strategy is actually held; retune freely in STRAT_HOLD. */ const INSTR_LABEL={equity:'Equity',options:'Options',futures:'Futures'}; const INSTR_TABS=[ ['equity', 'Equity', 'layers', 'Cash stocks, delivery & intraday'], ['options','Options', 'target', 'Defined-risk & premium, calls & puts'], ['futures','Futures', 'trendUp', 'Leveraged index & stock F&O'], ]; const HOLD_LABEL={carry:'Carry-forward',intraday:'Intraday',swing:'Swing',expiry:'Expiry',scalper:'Scalper'}; const HOLD_DESC={ carry:'Positional, held overnight to multi-day (CNC / carried F&O).', intraday:'Entered and squared off within the same session.', swing:'Held a few days on a directional or volatility view.', expiry:'Anchored to the options expiry cycle, theta, pin, expiry-day.', scalper:'Very short intraday, many small, fast trades.', }; // per-instrument holding-style tabs (order as the user mapped them) const HOLD_TABS={ equity:['carry','intraday','scalper'], options:['carry','intraday','swing','expiry','scalper'], futures:['carry','intraday','swing','expiry','scalper'], }; const instrOf=s=>(s.seg==='equity'||s.seg==='any')?'equity':(s.seg==='options')?'options':'futures'; const STRAT_HOLD={ // Equity (cash stocks) lib_adx:'carry',lib_rsmom:'carry',lib_rsi2:'carry',lib_bollrev:'carry',lib_donchian:'carry', lib_vcp:'carry',lib_btst:'carry',lib_pairs:'carry',lib_earnings:'carry',lib_rebal:'carry', lib_sectorrot:'carry',lib_mompf:'carry',lib_lowvol:'carry',lib_quality:'carry',lib_mlsignal:'carry', lib_nr7:'intraday',lib_eodsq:'intraday',lib_news:'intraday',lib_gapfill:'scalper', // Futures (index & stock F&O) lib_macross:'carry',lib_fiiflow:'carry',lib_regimeswitch:'carry',lib_supertrend:'carry', lib_orb:'intraday',lib_dow:'swing',lib_ratio:'swing',lib_oibuildup:'swing', lib_idxarb:'expiry',lib_cashfut:'expiry',lib_calroll:'expiry',lib_ema921:'scalper',lib_vwaprev:'scalper', // Options lib_coveredcall:'carry',lib_calendar:'carry',lib_deltaneutral:'carry',lib_longopt:'intraday', lib_pcr:'swing',lib_debit:'swing',lib_credit:'swing',lib_straddle:'swing', lib_expiry:'expiry',lib_maxpain:'expiry',lib_strangle:'expiry',lib_condor:'expiry',lib_920:'scalper', }; const holdOf=s=>STRAT_HOLD[s.id]||'carry'; /* Same Equity/Options/Futures × holding-style classes, but for the REAL backend bots (ALGOS). Instrument from segment+name; holding from product (CNC=carry, MIS=intraday, NRML=swing) with an explicit per-id map for the cases where that's too coarse. Scope is studio-wide. */ const ALGO_HOLD={ orb:'intraday',vwap_rev:'scalper',vwap_mom:'scalper',ema_scalp:'scalper',bb_breakout:'intraday',meanrev:'intraday', momentum:'carry',rsi2:'carry',macross:'carry',supertrend:'carry',lowvol:'carry',xs_momentum:'carry', ema_cross:'carry',adx_trend:'carry',bollinger:'carry',zscore:'carry',nr7:'carry',opportunity:'carry',moonshot:'carry', pairs:'swing',strangle:'expiry',fut_trend:'swing',iron_condor:'expiry',basis:'expiry',mcx_trend:'swing',goldsilver:'swing', }; function algoInstr(a){ const id=(a&&a.id||'').toLowerCase(), seg=(a&&a.segment||'').toLowerCase(), nm=(a&&a.name||'').toLowerCase(); if(/strangle|condor|straddle|option|theta|premium selling|iron/.test(id+' '+nm)) return 'options'; if(seg==='cash') return 'equity'; if(seg==='fno'||seg==='commodity') return 'futures'; return 'equity'; } // Crypto deployed = Σ open-position notional (qty×entry); perps book 20% margin. Options premium ≈ 0 notional. function cryptoDeployed(s){ const seg=(s&&s.instr)||'spot'; return (s&&s.positions||[]).reduce((a,p)=>{ const n=Math.abs((p.qty||0)*(p.entry||0)); return a+(seg==='perps'?n*0.20:n); },0); } // Shared monitor sort+filter (Indian & crypto books). Returns {sorted, bar}; chips reuse state.algo.monSort/monFilter. const MON_SORTS=[['pnl','Top P&L'],['unreal','Unrealised'],['deployed','Deployed'],['open','Open'],['name','Name']]; const MON_FILTS=[['all','All'],['profit','In profit'],['loss','Losing'],['open','Open now']]; function monSortFilter(list){ const mSort=state.algo.monSort||'pnl', mFilt=state.algo.monFilter||'all'; const filtFn=({all:()=>true,profit:x=>(x.paperPnl||0)>0,loss:x=>(x.paperPnl||0)<0,open:x=>(x.openPositions||0)>0})[mFilt]||(()=>true); const sortFn=({pnl:(a,b)=>(b.paperPnl||0)-(a.paperPnl||0),unreal:(a,b)=>(b.openPnl||0)-(a.openPnl||0),open:(a,b)=>(b.openPositions||0)-(a.openPositions||0), deployed:(a,b)=>(b._dep||0)-(a._dep||0),name:(a,b)=>String(a.name||'').localeCompare(String(b.name||''))})[mSort]||((a,b)=>(b.paperPnl||0)-(a.paperPnl||0)); const sorted=list.filter(filtFn).sort(sortFn); const bar=`
Sort${MON_SORTS.map(([k,l])=>``).join('')}
`+ `
Show${MON_FILTS.map(([k,l])=>``).join('')}${sorted.length} of ${list.length}
`; return {sorted,bar}; } function algoHold(a){ const id=(a&&a.id||'').toLowerCase(); if(ALGO_HOLD[id]) return ALGO_HOLD[id]; if(/scalp/.test(id)) return 'scalper'; const p=(a&&a.product||'').toUpperCase(); if(p==='MIS') return 'intraday'; if(p==='NRML') return 'swing'; return 'carry'; } // studio-wide instrument × holding scope, the single source of truth every Algo tab honours function studioScope(){ const a=state.algo=state.algo||{}; if(!a.instr){ a.instr=(a.lib&&a.lib.instr)||'equity'; } if(!a.hold){ a.hold=(a.lib&&a.lib.hold)||'all'; } return {instr:a.instr,hold:a.hold}; } function inScope(a){ const sc=studioScope(); return algoInstr(a)===sc.instr && (sc.hold==='all'||algoHold(a)===sc.hold); } // ALGOS // risk → existing badge class (Conservative=b-up, Moderate=b-neu, Aggressive=b-warn) const STRAT_LIBRARY=[ // ---- Trend / Momentum ---- {id:'lib_macross',name:'Moving-Average Crossover',fam:'trend',risk:'Moderate',seg:'index',best:'Bull', what:'Goes long when a fast MA crosses above a slow MA; flat/short when it crosses below.', rule:'BUY when price/fast-MA crosses above the slow MA (e.g. 50 over 200); exit on the opposite cross.', works:'Sustained, trending markets, it catches the meat of a directional move.', fails:'Choppy, sideways markets, it gets whipsawed, buying high and selling low repeatedly.', guard:'Add a regime filter (only take longs in Bull) and a hard stop; stand aside in Choppy.', params:[['Fast MA','50'],['Slow MA','200'],['Stop','ATR×2']]}, {id:'lib_ema921',name:'EMA 9/21 Momentum',fam:'trend',risk:'Aggressive',seg:'index',best:'Bull', what:'Faster intraday version of the crossover for momentum bursts.', rule:'Long when EMA9 > EMA21 and price holds above both; exit when EMA9 crosses back below EMA21.', works:'Strong intraday trends and momentum days with a clear direction.', fails:'Range-bound, low-volume sessions, frequent small losses pile up.', guard:'Cap trades/day, trade only the trending session window, square off by close.', params:[['Fast EMA','9'],['Slow EMA','21'],['Max trades/day','3']]}, {id:'lib_supertrend',name:'Supertrend Follow',fam:'trend',risk:'Moderate',seg:'fno',best:'Bull', what:'Trails an ATR-based band; flips long/short as price closes through it.', rule:'Long when price closes above the Supertrend line; reverse/flat when it closes below.', works:'Clean trends with steady volatility, the band trails the move well.', fails:'Sharp volatility spikes flip it repeatedly (false reversals).', guard:'Use a longer ATR period in High-Vol; size down when VIX is elevated.', params:[['ATR period','10'],['Multiplier','3']]}, {id:'lib_adx',name:'ADX Trend Filter',fam:'trend',risk:'Conservative',seg:'equity',best:'Bull', what:'Only trades when trend strength (ADX) confirms a real trend exists.', rule:'Take trend entries only when ADX > 25 and rising; otherwise stay flat.', works:'As a filter on top of any trend system, it cuts the worst chop trades.', fails:'Lags at trend births and ends; can keep you out of early moves.', guard:'Pair with a momentum trigger so you are not late; never trade ADX < 20.', params:[['ADX threshold','25'],['Lookback','14']]}, {id:'lib_rsmom',name:'Relative-Strength Rotation',fam:'trend',risk:'Moderate',seg:'equity',best:'Bull', what:'Holds the strongest stocks in a universe, drops the laggards.', rule:'Rank a universe by 3–6 month return; hold the top decile, rebalance monthly.', works:'Trending broad markets where leadership persists.', fails:'Sharp reversals/crashes, last month’s winners fall hardest.', guard:'Add a market-trend filter (go to cash below the 200-DMA of the index).', params:[['Lookback','6mo'],['Top N','10'],['Rebalance','Monthly']]}, // ---- Mean Reversion ---- {id:'lib_rsi2',name:'RSI(2) Pullback',fam:'meanrev',risk:'Moderate',seg:'equity',best:'Bull', what:'Buys short, sharp dips inside an established uptrend.', rule:'In an uptrend (price > 200-DMA), buy when RSI(2) < 10; exit when RSI(2) > 70 or on a time stop.', works:'Uptrending markets that pull back and resume, high win rate.', fails:'Catches a falling knife if the uptrend has actually broken.', guard:'Never fade without the trend filter; hard time-stop so a loser can’t compound.', params:[['RSI period','2'],['Entry','<10'],['Exit','>70']]}, {id:'lib_bollrev',name:'Bollinger Band Reversion',fam:'meanrev',risk:'Moderate',seg:'equity',best:'Choppy', what:'Fades touches of the outer band back toward the mean.', rule:'Buy a close below the lower band, exit at the middle band; mirror for shorts.', works:'Range-bound, mean-reverting names with stable volatility.', fails:'Trending breakouts, “cheap” keeps getting cheaper.', guard:'Skip when bands are expanding fast (volatility regime change); stop beyond the band.', params:[['Period','20'],['StdDev','2']]}, {id:'lib_vwaprev',name:'VWAP Reversion',fam:'meanrev',risk:'Aggressive',seg:'index',best:'Choppy', what:'Intraday fade of price stretched far from VWAP back to it.', rule:'Fade when price is >N stdev from VWAP with no fresh news; target VWAP.', works:'Liquid intraday instruments on balanced, two-sided days.', fails:'Trend days, price rides far from VWAP and never returns.', guard:'Detect trend-day early (opening drive) and disable; tight per-trade stop.', params:[['Band','2σ'],['Target','VWAP']]}, {id:'lib_gapfill',name:'Gap-Fill Fade',fam:'meanrev',risk:'Aggressive',seg:'equity',best:'Choppy', what:'Fades an overnight gap betting it fills toward the prior close.', rule:'On a moderate gap with no catalyst, fade toward the prior close; stop beyond the gap extreme.', works:'Emotion-driven gaps with no real news behind them.', fails:'News/results gaps that run, fading those is how accounts blow up.', guard:'Never fade an earnings/news gap; cap gap size; hard stop at the day extreme.', params:[['Max gap','2%'],['Stop','Day extreme']]}, // ---- Breakout / Volatility ---- {id:'lib_orb',name:'Opening Range Breakout',fam:'breakout',risk:'Aggressive',seg:'index',best:'High-Vol', what:'Trades a break of the first 15–30 min range of the day.', rule:'Mark the first 15-min high/low; go with a breakout on volume; stop at the opposite end.', works:'High-volatility opens and trending days.', fails:'Quiet, rangebound days produce repeated false breaks.', guard:'Require a volume/ATR expansion to confirm; one re-entry max; square off intraday.', params:[['Range','First 15m'],['Confirm','Volume>avg']]}, {id:'lib_donchian',name:'20-Day High Breakout',fam:'breakout',risk:'Moderate',seg:'equity',best:'Bull', what:'The classic Turtle breakout, buy new highs, ride the trend.', rule:'Buy a close above the 20-day high; trail with the 10-day low; exit on the trail.', works:'Strong, persistent trends and momentum regimes.', fails:'Whipsaws at range edges in sideways markets.', guard:'ATR position sizing so each trade risks a fixed small % of capital.', params:[['Entry','20-day high'],['Exit trail','10-day low']]}, {id:'lib_nr7',name:'NR7 / Inside-Bar Breakout',fam:'breakout',risk:'Moderate',seg:'equity',best:'High-Vol', what:'Trades expansion out of the narrowest-range bar (a coiled spring).', rule:'After an NR7 / inside bar, enter on a break of its range in the trend direction.', works:'Post-consolidation volatility expansion.', fails:'Failed breaks reverse straight through the other side.', guard:'Trade only in the higher-timeframe trend direction; stop at the bar’s other extreme.', params:[['Pattern','NR7'],['Filter','HTF trend']]}, {id:'lib_vcp',name:'Volatility Contraction',fam:'breakout',risk:'Moderate',seg:'equity',best:'Bull', what:'Buys breakouts from tightening bases (contracting volatility).', rule:'Identify successively tighter pullbacks on declining volume; buy the pivot breakout.', works:'Leading stocks in bull markets forming clean bases.', fails:'Late-stage bases and bear markets, most breakouts fail.', guard:'Tight stop under the pivot; only in confirmed uptrends.', params:[['Base','Tightening'],['Stop','Under pivot']]}, // ---- Time / Calendar ---- {id:'lib_920',name:'9:20 ORB (time entry)',fam:'time',risk:'Aggressive',seg:'options',best:'High-Vol', what:'A fixed-time entry just after the open captures the day’s initial drive.', rule:'At 9:20, enter in the direction of the opening 5-min candle; SL at its low/high.', works:'Days with a strong directional open.', fails:'Flat opens reverse and stop you out fast.', guard:'Predefined SL/target, one shot per day, hard square-off time.', params:[['Entry','09:20'],['Square-off','15:15']]}, {id:'lib_eodsq',name:'Intraday Auto Square-off',fam:'time',risk:'Conservative',seg:'any',best:'Any', what:'A discipline overlay, flatten everything before the close, no overnight risk.', rule:'Force-exit all intraday positions at a set time (e.g. 15:15) regardless of P&L.', works:'Every intraday strategy, removes gap risk and emotional holding.', fails:'Can exit a winner early; that is the price of zero overnight risk.', guard:'This IS the guard, pair with any intraday system.', params:[['Square-off','15:15']]}, {id:'lib_btst',name:'BTST Momentum',fam:'time',risk:'Aggressive',seg:'equity',best:'Bull', what:'Buy strong-closing stocks today, sell tomorrow on follow-through.', rule:'Buy names closing at day highs on volume; exit next morning on strength or a stop.', works:'Strong-trend markets with overnight continuation.', fails:'Overnight gap-downs on negative global cues, full overnight risk.', guard:'Small size; avoid event nights; predefined gap-down exit.', params:[['Hold','1 night'],['Exit','Next open']]}, {id:'lib_expiry',name:'Expiry-Day Theta',fam:'time',risk:'Aggressive',seg:'options',best:'Choppy', what:'Sells options on expiry day to harvest the fastest time decay.', rule:'Sell ATM/OTM options near the open on expiry; manage with a stop-loss on premium.', works:'Pinned, low-movement expiry sessions.', fails:'A trending expiry day, short options can lose multiples of the credit fast.', guard:'Hard premium SL, defined-risk spreads not naked, size for the worst case.', params:[['Day','Expiry'],['Stop','2× credit']]}, {id:'lib_dow',name:'Day-of-Week / Seasonality',fam:'time',risk:'Conservative',seg:'index',best:'Any', what:'Exploits recurring calendar tendencies (e.g. monthly expiry, turn-of-month).', rule:'Take positions only on statistically favourable calendar days from backtests.', works:'When a seasonal edge is statistically robust out-of-sample.', fails:'Overfitting, many “seasonal” edges are noise that vanish live.', guard:'Demand a large sample + OOS proof before trusting; tiny size.', params:[['Edge','Calendar'],['Proof','OOS required']]}, // ---- Stat-Arb / Pairs ---- {id:'lib_pairs',name:'Pairs Trading',fam:'statarb',risk:'Moderate',seg:'equity',best:'Choppy', what:'Long one stock, short a correlated one when their spread stretches.', rule:'When the spread z-score > 2, short the rich / long the cheap; exit at mean reversion.', works:'Stable, cointegrated pairs in range-bound markets, market-neutral.', fails:'When the relationship breaks (a fundamental change in one name).', guard:'Re-test cointegration regularly; stop if the spread keeps diverging.', params:[['Entry','z>2'],['Exit','z→0']]}, {id:'lib_ratio',name:'Ratio / Spread Trade',fam:'statarb',risk:'Moderate',seg:'fno',best:'Choppy', what:'Trades the ratio between two related futures (e.g. sector pair).', rule:'Mean-revert the historical ratio band between two related contracts.', works:'Structurally linked instruments with a stable ratio.', fails:'Regime shifts that permanently re-rate one leg.', guard:'Hard spread stop; cap leverage, spreads still blow out.', params:[['Band','Historical'],['Hedge','β-weighted']]}, {id:'lib_idxarb',name:'Index Arbitrage',fam:'statarb',risk:'Conservative',seg:'index',best:'Any', what:'Captures mispricing between an index and its futures/constituents.', rule:'When futures deviate from fair value beyond costs, trade the convergence.', works:'High-liquidity, low-cost execution environments.', fails:'Costs/slippage eat the tiny edge; needs fast execution.', guard:'Only when net of all costs is clearly positive; automate execution.', params:[['Edge','Basis'],['Need','Low latency']]}, // ---- Event-Driven ---- {id:'lib_earnings',name:'Earnings Drift',fam:'event',risk:'Aggressive',seg:'equity',best:'Any', what:'Rides the post-results drift after a strong earnings surprise.', rule:'After a big beat + gap on volume, enter on the day-1 close; trail the move.', works:'Clear, high-quality surprises with institutional follow-through.', fails:'“Buy the rumour, sell the news” fades; gaps that reverse.', guard:'Wait for confirmation (no pre-results bet); stop under the results-day low.', params:[['Trigger','Beat+gap'],['Stop','Day-1 low']]}, {id:'lib_rebal',name:'Index Rebalance',fam:'event',risk:'Moderate',seg:'equity',best:'Any', what:'Front-runs forced index-fund buying/selling on add/drop announcements.', rule:'Buy confirmed index additions / sell deletions ahead of the effective date.', works:'Large, predictable passive flows around rebalance dates.', fails:'Crowded trade, much of the move is already priced in.', guard:'Enter early on the announcement, exit into the rebalance-day flow.', params:[['Trigger','Add/Drop'],['Exit','Effective date']]}, {id:'lib_news',name:'News-Catalyst Momentum',fam:'event',risk:'Aggressive',seg:'equity',best:'High-Vol', what:'Trades the immediate momentum from a material news catalyst.', rule:'On confirmed material news + volume surge, trade the initial direction with a tight stop.', works:'Genuine, high-impact catalysts with volume confirmation.', fails:'Stale/priced-in news and fake-outs; spreads widen on the spike.', guard:'Volume + price confirmation before entry; very tight stop; small size.', params:[['Trigger','News+vol'],['Stop','Tight']]}, // ---- Sentiment / OI / Flow ---- {id:'lib_oibuildup',name:'OI Buildup',fam:'sentiment',risk:'Moderate',seg:'fno',best:'Bull', what:'Reads price + open-interest together to classify long/short buildup.', rule:'Price up + OI up = long buildup (go with it); price up + OI down = short covering (fade carefully).', works:'Confirming a directional move with real positioning.', fails:'OI lags; can mislead near expiry when positions roll.', guard:'Combine with price action, not alone; avoid the last expiry hours.', params:[['Inputs','Price+OI'],['Avoid','Expiry close']]}, {id:'lib_pcr',name:'PCR Contrarian',fam:'sentiment',risk:'Moderate',seg:'options',best:'Choppy', what:'Uses the put-call ratio as a contrarian sentiment extreme gauge.', rule:'Very high PCR (excess fear) = look for longs; very low PCR (greed) = caution/shorts.', works:'Sentiment extremes that mark short-term turning points.', fails:'In strong trends sentiment stays extreme far longer than you can stay solvent.', guard:'Need a price-confirmation trigger; never fade a trend on PCR alone.', params:[['High PCR','>1.3'],['Low PCR','<0.7']]}, {id:'lib_maxpain',name:'Max-Pain Pin',fam:'sentiment',risk:'Aggressive',seg:'options',best:'Choppy', what:'Bets price gravitates to the max-pain strike into expiry.', rule:'Sell defined-risk premium around the max-pain strike as expiry approaches.', works:'Low-event, range-bound expiries where pinning tends to occur.', fails:'Trending expiries ignore max-pain entirely.', guard:'Defined-risk only; exit if price trends away from the pin.', params:[['Anchor','Max-pain'],['Risk','Defined']]}, {id:'lib_fiiflow',name:'FII/DII Flow Follow',fam:'sentiment',risk:'Conservative',seg:'index',best:'Bull', what:'Tilts with sustained institutional buying/selling pressure.', rule:'Bias long on persistent net FII+DII inflows; reduce on sustained outflows.', works:'As a slow regime/bias filter, not a precise timing signal.', fails:'Flows are reported with a lag and are noisy day to day.', guard:'Use as a bias overlay only; pair with a price trigger for timing.', params:[['Input','Net flows'],['Use','Bias filter']]}, // ---- Factor / Rotation ---- {id:'lib_sectorrot',name:'Sector Rotation',fam:'factor',risk:'Moderate',seg:'equity',best:'Bull', what:'Rotates capital into the strongest sectors, out of the weakest.', rule:'Rank sectors by relative strength; overweight leaders, underweight laggards; rebalance monthly.', works:'Markets with clear sector leadership cycles.', fails:'Rapid rotations and reversals whipsaw the basket.', guard:'Hold a diversified set; cap single-sector weight; trend filter on the index.', params:[['Rank','Rel-strength'],['Rebalance','Monthly']]}, {id:'lib_mompf',name:'Momentum Factor Portfolio',fam:'factor',risk:'Moderate',seg:'equity',best:'Bull', what:'Systematic long basket of the highest-momentum names.', rule:'Hold top-momentum decile, equal-weight, monthly rebalance, market-trend filter.', works:'Persistent bull trends, momentum is a durable long-run factor.', fails:'Momentum crashes at sharp market turns.', guard:'De-risk to cash below the index 200-DMA; cap volatility per name.', params:[['Factor','12-1 mom'],['Filter','Index 200-DMA']]}, {id:'lib_lowvol',name:'Low-Volatility Factor',fam:'factor',risk:'Conservative',seg:'equity',best:'Bear', what:'Holds the lowest-volatility names for smoother, defensive returns.', rule:'Rank universe by realised volatility; hold the lowest-vol basket, rebalance quarterly.', works:'Choppy/bear markets, it draws down far less.', fails:'Lags badly in roaring bull markets.', guard:'Use as the defensive sleeve; combine with momentum for balance.', params:[['Factor','Low vol'],['Rebalance','Quarterly']]}, {id:'lib_quality',name:'Quality / Value Tilt',fam:'factor',risk:'Conservative',seg:'equity',best:'Any', what:'Owns financially strong, reasonably-priced companies for the long run.', rule:'Screen for high ROE/low debt + sensible valuation; hold long-term, rebalance yearly.', works:'Long horizons, quality compounds and survives downturns.', fails:'Can underperform for long stretches when junk rallies.', guard:'Diversify; this is a survive-and-compound sleeve, not a trade.', params:[['Screen','ROE/Debt/Val'],['Horizon','Years']]}, // ---- ML / Regime ---- {id:'lib_regimeswitch',name:'Regime-Switching Meta',fam:'ml',risk:'Moderate',seg:'index',best:'Any', what:'A meta-strategy that turns sub-strategies on/off by the live regime.', rule:'Detect regime (trend/chop/high-vol); run only the sub-strategy validated for that regime.', works:'Across full market cycles, it stands aside when nothing fits.', fails:'Regime detection lags at turning points; transitions are costly.', guard:'This is the survival engine, when uncertain, it goes to cash.', params:[['Detector','VIX+breadth+trend'],['Default','Stand aside']]}, {id:'lib_mlsignal',name:'ML Predictive Signal',fam:'ml',risk:'Aggressive',seg:'equity',best:'Any', what:'A trained model outputs a directional probability used to size trades.', rule:'Enter when model confidence clears a threshold; size proportional to confidence.', works:'When the model has a genuine, walk-forward-validated edge.', fails:'Overfitting and regime drift, the model decays as markets change.', guard:'Strict walk-forward validation, live monitoring, kill-switch on decay.', params:[['Validation','Walk-forward'],['Sizing','Confidence']]}, // ---- Options · Directional ---- {id:'lib_longopt',name:'Directional Long Option',fam:'opt_dir',risk:'Aggressive',seg:'options',best:'High-Vol', what:'Buy a call or put for a leveraged, limited-risk directional bet.', rule:'Buy ATM/ITM option in your direction; risk is capped at the premium paid.', works:'Strong, fast directional moves where the move beats time decay.', fails:'Time decay + falling IV bleed the premium even if you are “right” slowly.', guard:'Size so total premium at risk is tiny; avoid buying into high IV.', params:[['Strike','ATM/ITM'],['Risk','Premium only']]}, {id:'lib_debit',name:'Vertical Debit Spread',fam:'opt_dir',risk:'Moderate',seg:'options',best:'Bull', what:'Directional bet with lower cost and lower time-decay drag than a naked long.', rule:'Buy a near option, sell a further one in the same direction; defined risk & reward.', works:'Moderate directional moves, cheaper and less IV-sensitive.', fails:'Capped upside; still loses if the move doesn’t come.', guard:'Risk = net debit, known up front; pick expiries with room for the move.', params:[['Structure','Buy+Sell'],['Risk','Net debit']]}, // ---- Options · Income ---- {id:'lib_strangle',name:'Short Strangle',fam:'opt_income',risk:'Aggressive',seg:'options',best:'Choppy', what:'Sell an OTM call and put to collect premium when price stays in a range.', rule:'Sell OTM call + put; profit if price stays between strikes through expiry.', works:'Range-bound, falling-volatility markets.', fails:'A big move on either side, losses are theoretically unlimited if naked.', guard:'Prefer the defined-risk Iron Condor; hard SL on premium; never naked + unhedged.', params:[['Strikes','OTM C+P'],['Defend','SL on premium']]}, {id:'lib_condor',name:'Iron Condor',fam:'opt_income',risk:'Moderate',seg:'options',best:'Choppy', what:'A defined-risk strangle, sells a range, buys wings to cap the tails.', rule:'Sell an OTM call & put spread; max loss is capped by the long wings.', works:'Range-bound markets with elevated IV to sell into.', fails:'Trending/break-out moves push price through a short strike.', guard:'Known max loss by design; adjust/roll the tested side; size to survive max loss.', params:[['Wings','Long OTM'],['Risk','Defined']]}, {id:'lib_credit',name:'Credit Spread',fam:'opt_income',risk:'Moderate',seg:'options',best:'Bull', what:'A one-sided defined-risk premium sell with a directional lean.', rule:'Sell a put spread (bullish) or call spread (bearish); collect net credit.', works:'When you have a directional bias and want a high-probability income trade.', fails:'A move against you to the short strike realises the (capped) max loss.', guard:'Risk = spread width − credit, fixed; close at a set loss multiple.', params:[['Lean','Put/Call'],['Risk','Width−credit']]}, {id:'lib_coveredcall',name:'Covered Call',fam:'opt_income',risk:'Conservative',seg:'options',best:'Choppy', what:'Sell calls against stock you own to earn income on flat-to-mild-up moves.', rule:'Hold the stock, sell an OTM call each cycle; keep the premium if unexercised.', works:'Sideways-to-slightly-up markets on holdings you already own.', fails:'Caps your upside if the stock rallies hard; no downside protection.', guard:'Only on stock you’re happy to sell at the strike; income, not protection.', params:[['Need','Own stock'],['Strike','OTM call']]}, // ---- Options · Volatility / Neutral ---- {id:'lib_straddle',name:'Long Straddle',fam:'opt_vol',risk:'Aggressive',seg:'options',best:'High-Vol', what:'Buy a call AND a put, profits from a big move either direction.', rule:'Buy ATM call + put before an expected volatility event; profit on a large move.', works:'Pre-event when a large move is likely and IV is still cheap.', fails:'IV crush after the event + a small move, both legs bleed.', guard:'Enter before IV ramps; exit fast post-event; cap premium at risk.', params:[['Strikes','ATM C+P'],['Edge','Cheap IV']]}, {id:'lib_calendar',name:'Calendar Spread',fam:'opt_vol',risk:'Moderate',seg:'options',best:'Choppy', what:'Sell a near-dated option, buy a far-dated one, harvest faster near-term decay.', rule:'Same strike, sell front expiry / buy back expiry; profit from differential theta.', works:'Stable price near the strike with a favourable term structure.', fails:'A large directional move away from the strike hurts both legs.', guard:'Defined-ish risk; manage if price leaves the strike zone.', params:[['Legs','Sell front/Buy back'],['Edge','Theta diff']]}, {id:'lib_deltaneutral',name:'Delta-Neutral Theta',fam:'opt_vol',risk:'Moderate',seg:'options',best:'Choppy', what:'Hold a net-zero-delta option book and earn time decay, re-hedging as it drifts.', rule:'Sell premium, keep delta near zero by adjusting hedges as price moves.', works:'Range-bound, mean-reverting volatility with active management.', fails:'Gamma risk, fast moves force costly re-hedging (negative gamma bleed).', guard:'Continuous monitoring + adjustment; cap gamma exposure; size small.', params:[['Target','Δ≈0'],['Risk','Gamma']]}, // ---- Cash-Futures / Arb ---- {id:'lib_cashfut',name:'Cash-Futures Basis',fam:'arb',risk:'Conservative',seg:'fno',best:'Any', what:'Lock the spread between a stock’s cash price and its future.', rule:'When the future trades at a premium beyond carry, sell future / buy cash; converge at expiry.', works:'Liquid names with a clear, cost-positive basis.', fails:'Thin liquidity & costs erase the edge; needs capital for both legs.', guard:'Only when net-of-cost positive; hold to convergence.', params:[['Legs','Cash vs Future'],['Exit','Expiry']]}, {id:'lib_calroll',name:'Calendar Roll Spread',fam:'arb',risk:'Conservative',seg:'fno',best:'Any', what:'Trade the spread between near and far expiries of the same future.', rule:'Take the roll spread when it deviates from its typical band; revert as expiry nears.', works:'Stable term structures with predictable roll behaviour.', fails:'Demand/supply shocks distort the curve unexpectedly.', guard:'Spread stop; modest leverage; close before the near expiry.', params:[['Legs','Near vs Far'],['Edge','Roll band']]}, ]; const LIB_RISK_CLASS={Conservative:'b-up',Moderate:'b-neu',Aggressive:'b-warn'}; /* ============================================================ CRYPTO MARKET LAYER: the Algo Studio can scope to Indian (Kite) or Crypto (Binance). REAL prices only, no fake fallback: Binance's public data mirror (no API key, read-only). Strategy EXECUTION is preview/paper, honestly flagged. Real crypto orders would route via the Binance Algo API (TWAP/POV), which needs server-side key signing , that's the roadmap engine, never the browser. ============================================================ */ const CRYPTO_API='https://data-api.binance.vision'; // public market-data mirror: CORS-ok, not geo-fenced, no key const CRYPTO_UNIVERSE=[ {sym:'BTCUSDT',tk:'BTC',name:'Bitcoin'}, {sym:'ETHUSDT',tk:'ETH',name:'Ethereum'}, {sym:'SOLUSDT',tk:'SOL',name:'Solana'}, {sym:'BNBUSDT',tk:'BNB',name:'BNB'}, {sym:'XRPUSDT',tk:'XRP',name:'XRP'}, {sym:'ADAUSDT',tk:'ADA',name:'Cardano'}, {sym:'DOGEUSDT',tk:'DOGE',name:'Dogecoin'},{sym:'AVAXUSDT',tk:'AVAX',name:'Avalanche'}, {sym:'LINKUSDT',tk:'LINK',name:'Chainlink'},{sym:'MATICUSDT',tk:'MATIC',name:'Polygon'}, ]; const CRYPTO={loaded:false,live:false,error:false,quotes:{},t:0,busy:false}; function cryptoSyms(){ return CRYPTO_UNIVERSE.map(c=>c.sym); } // Risk-first strategy TEMPLATES (educational, preview/paper), same survival-first ethos as the live library. // wired: true only for bids the cloud worker actually runs (mirrors studio.js's DEPLOYABLE set // for the crypto/customer path - keep the two in sync). The rest render normally but are honest // about not having a live engine yet, instead of the old permanently-stuck "Engine loading" toast. const CRYPTO_STRATEGIES=[ {id:'cx_btc_trend',bid:'macross',name:'BTC Trend (MA200)',cat:'Trend',risk:'Moderate',pair:'BTCUSDT',wired:true, what:'Long BTC while it holds above its long-term moving average; flat below.', rule:'BUY when price closes above the 200-period MA; exit on a close back below.', works:'Strong, sustained bull legs, crypto trends long and hard.', fails:'Chop around the MA whipsaws you in and out at small losses repeatedly.', guard:'Only long above the MA; ATR-sized stop; one position; stand aside in chop.'}, {id:'cx_grid',bid:'bollinger',name:'Range Grid (ETH)',cat:'Mean-Reversion',risk:'Aggressive',pair:'ETHUSDT',wired:true, what:'A ladder of staggered buys & sells across a defined band, harvesting oscillation.', rule:'Buy each rung down, sell each rung up within a set price band.', works:'Sideways, high-volatility ranges, it monetises the wiggle.', fails:'A clean breakout leaves you holding the whole ladder against the move.', guard:'Hard band-exit if price leaves the range; cap total grid exposure & leverage.'}, {id:'cx_funding',bid:'perp_funding',name:'Funding-Rate Carry',cat:'Income',risk:'Moderate',pair:'BTC spot vs perp', what:'Delta-neutral: long spot, short perpetual, collect funding each interval.', rule:'When funding is positive, hold spot + short perp; pocket the funding payments.', works:'Calm, positive-funding regimes, steady market-neutral yield.', fails:'Funding flips negative or the basis blows out in a liquidation cascade.', guard:'Watch funding + basis; unwind on negative funding; respect exchange limits.'}, {id:'cx_rsi2',bid:'rsi2',name:'RSI-2 Dip (Alts)',cat:'Mean-Reversion',risk:'Aggressive',pair:'SOLUSDT',wired:true, what:'Buys very short-term oversold dips inside a higher-timeframe uptrend.', rule:'In an uptrend, BUY when RSI(2) < 5; exit when RSI(2) > 70 or after N bars.', works:'Pullbacks within an established alt uptrend.', fails:'Catching a falling knife once the trend has actually broken.', guard:'Only above the 200-MA; time-stop + hard stop; small size on alts.'}, {id:'cx_breakout',bid:'momentum',name:'Volatility Breakout',cat:'Breakout',risk:'Aggressive',pair:'BTCUSDT',wired:true, what:'Enters as price escapes a tight range on expanding volume.', rule:'BUY on a close above the N-day high with above-average volume; trail a stop.', works:'The start of a fresh expansion leg after compression.', fails:'False breakouts in thin liquidity hours snap straight back.', guard:'Require volume confirmation; trade liquid majors; trail, don’t fix a target.'}, {id:'cx_pairs',bid:'pairs',name:'ETH/BTC Ratio Pairs',cat:'Stat-Arb',risk:'Moderate',pair:'ETH vs BTC', what:'Trades the ETH/BTC ratio back to its mean, neutral to overall crypto beta.', rule:'Short the rich leg, long the cheap leg when the ratio z-score is stretched.', works:'When ETH & BTC stay cointegrated and the spread mean-reverts.', fails:'A narrative regime-shift breaks the relationship and the spread runs.', guard:'Z-score entry/exit bands; stop if the spread breaks its historical range.'}, {id:'cx_dca',name:'Disciplined DCA',cat:'Time / Calendar',risk:'Conservative',pair:'BTCUSDT', what:'Cost-averages a fixed amount on a fixed schedule, accumulation, not timing.', rule:'Buy a fixed notional every interval regardless of price; optional dip boosts.', works:'Long-horizon accumulation through full cycles; removes timing risk.', fails:'Prolonged bear markets test conviction; capital sits in drawdown.', guard:'Only commit what you can hold for years; size the schedule to your cashflow.'}, {id:'cx_momentum',bid:'xs_momentum',name:'Cross-Sectional Momentum',cat:'Factor / Rotation',risk:'Aggressive',pair:'Top-10 majors', what:'Rotates into the strongest recent performers across a basket of majors.', rule:'Each week, hold the top-N by trailing return; drop the laggards.', works:'Persistent momentum regimes where winners keep winning.', fails:'Sharp momentum crashes reverse and gut the whole basket at once.', guard:'Cap per-name weight; vol-target the basket; kill-switch on a drawdown limit.'}, // These 5 were live on the worker (REGISTRY, featured:true - the same "proven net of costs" bar // as the cards above) with no customer-facing card at all until now. The other 8 REGISTRY entries // (orb, vwap_rev, vwap_mom, ema_scalp, bb_breakout, supertrend, vwap_pull, rsi_intraday) stay // deliberately uncarded: strategies.py's own comments mark them "non-featured until validated" or // "fail after costs so far" - surfacing those as Deploy-worthy would recommend strategies the // team's own testing found unprofitable, with no disclosure. {id:'cx_trendfollow',bid:'trend_follow',name:'Trend Rider (20/100)',cat:'Trend',risk:'Moderate',pair:'ETHUSDT',wired:true, what:'Longs a fresh 20-day high while price holds above its 100-day trend average, then rides the move with a trailing stop.', rule:'BUY when price makes a new 20-bar high AND price is above the 100-bar average; trail-stop exit, no fixed target.', works:'Sustained trends with room to run, a slower, wider-net breakout system.', fails:'Choppy, range-bound markets whipsaw the 20-bar high trigger with no follow-through.', guard:'ATR-sized trailing stop; only long above the 100-bar average; one position at a time.'}, {id:'cx_emacross',bid:'ema_cross',name:'EMA Cross (20/50)',cat:'Trend',risk:'Moderate',pair:'BNBUSDT',wired:true, what:'A faster trend-follower than the 200-day golden cross, reacts to shifts in weeks, not months.', rule:'BUY when the 20-EMA crosses above the 50-EMA and price holds above the 50-EMA; exit on the cross back down.', works:'Medium-length trends where a 200-day filter reacts too slowly.', fails:'Sideways chop triggers repeated false crossovers, each one a small loss.', guard:'ATR stop; exits immediately on the EMA cross-down, no waiting for confirmation.'}, {id:'cx_adxtrend',bid:'adx_trend',name:'ADX Trend Filter',cat:'Trend',risk:'Conservative',pair:'SOLUSDT',wired:true, what:'Only trades when the trend-strength indicator (ADX) confirms a real trend is underway, sits out everything else.', rule:'BUY when ADX ≥ 25, +DI leads -DI, and price is above its 50-bar average; exit when the trend fades (DI flips or ADX drops).', works:'Clean, strongly-trending markets, the ADX filter is built specifically to reject chop.', fails:'Regime transitions, where ADX confirms a trend just as it is about to end.', guard:'ADX floor before any entry; exits fast on trend fade, not just full reversal.'}, {id:'cx_zscore',bid:'zscore',name:'Z-Score Snapback (10-bar)',cat:'Mean-Reversion',risk:'Aggressive',pair:'XRPUSDT',wired:true, what:'A faster, shallower dip-buyer than the Bollinger reversion card, reacts to smaller, quicker stretches.', rule:'BUY when price is 1.5+ standard deviations below its 10-bar average inside an uptrend; exit on reversion to the mean.', works:'Quick, sharp dips inside an established uptrend that snap back within days.', fails:'A real trend break, buying every shallow dip on the way down.', guard:'Only trades inside a 200-bar uptrend filter; ATR stop; exits on mean-reversion, not a fixed target.'}, {id:'cx_nr7',bid:'nr7',name:'NR7 Coil Breakout',cat:'Breakout',risk:'Aggressive',pair:'BTCUSDT',wired:true, what:'Waits for the tightest daily range in 7 days, a coiled spring, then buys the break above it.', rule:'BUY on a close above the narrowest-7-day bar\'s high, only inside a 50-bar uptrend; exit below the trend average.', works:'The volatility contraction genuinely precedes an expansion move (a real breakout, not a fakeout).', fails:'A quiet coil that just stays quiet, or breaks and immediately reverses.', guard:'Uptrend filter before any entry; ATR stop; exits on a trend-average break, not just a stall.'}, ]; async function loadCrypto(){ if(CRYPTO.busy) return; CRYPTO.busy=true; const url=`${CRYPTO_API}/api/v3/ticker/24hr?symbols=${encodeURIComponent(JSON.stringify(cryptoSyms()))}`; try{ const d=await fetch(url).then(r=>r.json()); if(Array.isArray(d)){ const q={}; d.forEach(t=>{ const ltp=parseFloat(t.lastPrice), chg=parseFloat(t.priceChangePercent); if(isFinite(ltp)) q[t.symbol]={ltp,chg:isFinite(chg)?chg:0}; }); CRYPTO.quotes=q; CRYPTO.live=Object.keys(q).length>0; CRYPTO.error=!CRYPTO.live; CRYPTO.t=Date.now(); if(window.TPChart&&TPChart.tick&&state.trading&&state.trading.sym&&q[state.trading.sym]){ TPChart.tick(state.trading.sym, q[state.trading.sym].ltp); } if(state.orders&&state.orders.length){ Object.keys(q).forEach(s=>{ if(q[s]&&q[s].ltp!=null) checkBracketOrders(s, q[s].ltp); }); } } else { CRYPTO.live=false; CRYPTO.error=true; } }catch(e){ CRYPTO.live=false; CRYPTO.error=true; } CRYPTO.loaded=true; CRYPTO.busy=false; } function cryptoFmt(p){ if(!isFinite(p)) return '-'; const dec=p>=1?2:p>=0.01?4:6; return '$'+p.toLocaleString('en-US',{minimumFractionDigits:Math.min(dec,2),maximumFractionDigits:dec}); } /* ---- Binance WebSocket: sub-second tape. Free public data mirror (data-stream.binance.vision: the wss twin of data-api, not geo-fenced, no key). One combined connection streams each coin's 24h ticker (~1/s per symbol) straight into CRYPTO.quotes → the tape ticks live. The 5s REST poll stays as an automatic fallback (only fires when the socket isn't delivering). ---- */ const CRYPTO_WS='wss://data-stream.binance.vision'; const CWS={ws:null,url:'',on:false,lastMsg:0,backoff:1000,reT:null,raf:0}; function cryptoWsUrl(){ return CRYPTO_WS+'/stream?streams='+CRYPTO_UNIVERSE.map(c=>c.sym.toLowerCase()+'@ticker').join('/'); } // Coalesce DOM patches to one per animation frame (the universe pushes ~10 msgs/s combined). function scheduleTapePatch(){ if(CWS.raf) return; CWS.raf=requestAnimationFrame(()=>{ CWS.raf=0; if(!(state.algo&&state.algo.market==='crypto')) return; patchCryptoTape(); // also re-mark the crypto Monitor/Positions P&L sub-second (spot marked to WS price) if(typeof isAlgo==='function' && isAlgo() && (state.algo.view==='monitor'||state.algo.view==='positions')) patchCryptoMonitorLive(); }); } // Sub-second P&L: re-mark SPOT open positions to the live WS price between the bot's 7s polls. Perps/options // keep the bot's authoritative mark (funding / option-credit aren't a plain qty×spot). Patches in place. function cxLiveOpen(s){ if(((s&&s.instr)||'spot')!=='spot') return (s&&s.openPnl)||0; let open=0, marked=false; ((s&&s.positions)||[]).forEach(p=>{ const q=p.qty, e=p.entry, w=CRYPTO.quotes&&CRYPTO.quotes[p.sym]; if(q!=null && e!=null && w && w.ltp!=null){ open+=q*(w.ltp-e); marked=true; } else if(p.unreal!=null){ open+=p.unreal; } }); // no WS quote for this coin → keep the bot mark return marked?open:((s&&s.openPnl)||0); } function patchCryptoMonitorLive(){ const d=CRYPTOMON.data; if(!d||!d.strategies) return; const cur=(state.algo&&state.algo.cinstr)||'spot'; const segTot={spot:0,perps:0,options:0}, segOpen={spot:0,perps:0,options:0}; const rowMap={}; document.querySelectorAll('[data-cxrow]').forEach(r=>rowMap[r.getAttribute('data-cxrow')]=r); const setCls=(el,v)=>{ el.classList.remove('up','down'); const c=cls(v); if(c) el.classList.add(c); }; d.strategies.forEach(s=>{ const open=cxLiveOpen(s), total=((s.realisedPnl)||0)+open, instr=s.instr||'spot'; segTot[instr]=(segTot[instr]||0)+total; segOpen[instr]=(segOpen[instr]||0)+open; const row=rowMap[s.id]; if(!row) return; // mon-card structure: patch the Net figure + the "R … · U …" sub in place (no re-render/flicker) const nb=row.querySelector('.cxm-net'); if(nb){ nb.textContent=cxMoney(total); setCls(nb,total); } const sub=row.querySelector('[data-cxsub]'); if(sub) sub.textContent=`R ${cxMoney(s.realisedPnl||0)} · U ${cxMoney(open)}`; }); const setLive=(id,v)=>document.querySelectorAll('[data-live="'+id+'"]').forEach(el=>{ el.textContent=cxMoney(v); setCls(el,v); }); setLive('cxClsPnl', segTot[cur]); setLive('cxUnreal', segOpen[cur]); ['spot','perps','options'].forEach(k=>document.querySelectorAll('[data-cxseg="'+k+'"]').forEach(el=>el.textContent=cxMoney(segTot[k]))); } function connectCryptoWS(){ if(typeof WebSocket==='undefined') return; // BUG FIX (2026-09-20): every caller of this function (boot, refreshVisible, the market toggle) // treats CRYPTO_ONLY alone as sufficient to mean "we're in crypto mode," matching how // renderTopIndex/scheduleTapePatch/the 5s poll all read it - this was the one place still // requiring state.algo.market==='crypto' specifically, with no CRYPTO_ONLY fallback. If that flag // was ever unset/reset for even one tick between two synchronous statements at boot, this was // the single guard standing between a live ticker and a WebSocket that never gets attempted for // the rest of the session, with nothing else ever re-triggering the very first connect attempt. if(!(CRYPTO_ONLY||(state.algo&&state.algo.market==='crypto'))) return; const url=cryptoWsUrl(); if(CWS.ws && CWS.url===url && (CWS.ws.readyState===0||CWS.ws.readyState===1)) return; // already connecting/open to this set disconnectCryptoWS(); CWS.url=url; try{ const ws=new WebSocket(url); ws.onopen=()=>{ CWS.on=true; CWS.backoff=1000; }; ws.onmessage=ev=>{ try{ const d=JSON.parse(ev.data).data; if(d&&d.s){ const ltp=parseFloat(d.c), chg=parseFloat(d.P); if(isFinite(ltp)){ (CRYPTO.quotes||(CRYPTO.quotes={}))[d.s]={ltp,chg:isFinite(chg)?chg:0}; CRYPTO.live=true; CRYPTO.error=false; CRYPTO.loaded=true; CRYPTO.t=Date.now(); CWS.lastMsg=Date.now(); if(window.TPChart&&TPChart.tick) TPChart.tick(d.s, ltp); checkBracketOrders(d.s, ltp); scheduleTapePatch(); } } }catch(e){} }; ws.onerror=()=>{ CWS.on=false; }; ws.onclose=()=>{ CWS.on=false; CWS.ws=null; scheduleCryptoWSReconnect(); }; // Binance drops the socket every 24h → auto-reconnect CWS.ws=ws; }catch(e){ scheduleCryptoWSReconnect(); } } function disconnectCryptoWS(){ if(CWS.reT){clearTimeout(CWS.reT);CWS.reT=null;} if(CWS.ws){ try{CWS.ws.onclose=null;CWS.ws.close();}catch(e){} } CWS.ws=null; CWS.on=false; CWS.url=''; } function scheduleCryptoWSReconnect(){ if(!(state.algo&&state.algo.market==='crypto')||CWS.reT) return; const delay=Math.min(CWS.backoff,15000); CWS.backoff=Math.min(CWS.backoff*2,15000); CWS.reT=setTimeout(()=>{ CWS.reT=null; connectCryptoWS(); },delay); } // ---- crypto views ---- function cryptoStatusBar(){ const live=CRYPTO.live; const reg=state.displayed||'neutral'; const regLabel=reg==='bull'?'Bull Expansion':reg==='bear'?'Defensive Shield':'Neutral Chop'; const cells=[ `
Venue${infoI('Binance public market data (read-only, no API key). Real prices, never simulated.')}Binance
`, `
Telemetry${live?`38ms · LIVE WS`:(CRYPTO.error?'Reconnecting':'Connecting...')}
`, `
Friction35 bps model
`, `
Regime Governor${regLabel}
`, `
HarnessCrypto · 24/7 Live Paper Engine
` ].join(''); return `
${cells}
`; } function cryptoStratCard(s){ const rk=LIB_RISK_CLASS[s.risk]||'b-neu'; // BUG FIX (2026-09-09): this used to read ALGOS (the shared, unscoped /api/strategies payload - // same platform-wide data every user sees, unrelated to their own deploy/stop actions) instead of // CRYPTOMON's per-user overlay. That's why Stop showed a success toast and the card still said // "running" right after - the badge was never looking at this user's own state to begin with. // Crypto deployment status is running|stopped only (no distinct "paused" in the deployment table). const m=s.bid?((CRYPTOMON.data&&CRYPTOMON.data.strategies)||[]).find(x=>x.id===s.bid):null; const dep=!!(m&&m.deployed), paused=false; let cta; if(!s.bid) cta=`Learn only`; else if(!s.wired) cta=`Backtest only`; else if(dep||paused) cta=`${paused?'Paused':'Paper · running'}`; else cta=``; return `
${esc(s.name)}${esc(s.cat)} · ${esc(s.pair)}
${esc(s.risk)}

${esc(s.what)}

${icon('bolt',11)}Rule. ${esc(s.rule)}
${icon('check',11)}Works. ${esc(s.works)}
${icon('alert',11)}Fails. ${esc(s.fails)}
${icon('shield',11)}Survival guard. ${esc(s.guard)}
${cta}
`; } function cryptoMarket(){ // Library cards need this user's own running/stopped state (cryptoStratCard reads CRYPTOMON.data) - // load it even if the user opens Library before ever visiting Monitor. if(!CRYPTOMON.loaded&&!CRYPTOMON.busy) loadCryptoMonitor().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='library') renderAlgo(); }); // Honest framing: the crypto paper engine IS live 24/7 (not "preview/roadmap"), real Binance prices, // simulated fills, no real orders. Live execution unlocks per strategy once proven + connected + armed. const note=`
${icon('shield',13)}Live crypto paper book, 24/7. Prices are real (Binance), fills are simulated: no real orders are placed. These strategies run continuously in paper; watch them live on Monitor. Live execution unlocks per strategy only once it clears the Go-Live bar, you connect Binance, and arm ALLOW_LIVE.
`; const stat=secStats([ {l:'Instruments',v:String(CRYPTO_UNIVERSE.length),s:'USDT majors'}, {l:'Strategies',v:String(CRYPTO_STRATEGIES.length),s:'running in paper'}, {l:'Data',v:CRYPTO.live?'Live':'-',s:CRYPTO.live?'real · Binance':'connecting',tone:CRYPTO.live?'up':''}, {l:'Execution',v:'Paper',s:'live: locked until proven'}, ]); const cats=['all',...Array.from(new Set(CRYPTO_STRATEGIES.map(s=>s.cat)))]; const sel=(state.algo&&state.algo.cxLibCat)||'all'; const shown=sel==='all'?CRYPTO_STRATEGIES:CRYPTO_STRATEGIES.filter(s=>s.cat===sel); const filters=`
Family${cats.map(c=>``).join('')}${shown.length} of ${CRYPTO_STRATEGIES.length}
`; const grid=shown.length?`
${shown.map(cryptoStratCard).join('')}
`:secEmpty('search','No strategies in this family','Pick another family or .'); return note+stat+filters+grid; } function cryptoSoon(label){ return secEmpty('cpu',label+' · crypto', `${esc(label)} runs on the live 24/7 crypto paper harness. Start the engine from Monitor or deploy a strategy from Library.`); } // ---- LIVE crypto paper book (24/7 harness on :8756 → /api/crypto/monitor) ---- const CRYPTOMON={loaded:false,busy:false,data:null,err:false,t:0}; async function loadCryptoMonitor(){ if(CRYPTOMON.busy) return; CRYPTOMON.busy=true; try{ const d=await fetch(`${BOT_API}/api/crypto/monitor`).then(r=>r.json()); CRYPTOMON.data=d; CRYPTOMON.err=false; CRYPTOMON.t=Date.now(); }catch(e){ CRYPTOMON.err=true; } CRYPTOMON.loaded=true; CRYPTOMON.busy=false; } function cxMoney(v){ if(v==null||!isFinite(v)) return '-'; const s=v<0?'−':(v>0?'+':''); return s+'$'+Math.abs(v).toLocaleString('en-US',{maximumFractionDigits:0}); } function cxActive(){ const d=CRYPTOMON.data; return d&&d.strategies?d.strategies.filter(s=>s.openPositions>0||s.realisedPnl!==0).length:0; } // crypto instrument bifurcation (Spot / Perps / Options), the crypto analog of Equity/Options/Futures const CX_INSTR=[['spot','Spot','spark','Long-only on spot majors'],['perps','Perpetuals','trendUp','Long / short + funding carry'],['options','Options','layers','Premium selling']]; // Compact unsigned money for exposure/deployed figures ($492K / $1.0M). function cxAbs(v){ if(v==null||!isFinite(v)) return '-'; const a=Math.abs(v); if(a>=1e6) return '$'+(a/1e6).toFixed(a>=1e7?0:1)+'M'; if(a>=1e3) return '$'+(a/1e3).toFixed(a>=1e5?0:1)+'K'; return '$'+Math.round(a); } function cxScopeCounts(){ const by={spot:{n:0,pnl:0,open:0,book:0},perps:{n:0,pnl:0,open:0,book:0},options:{n:0,pnl:0,open:0,book:0}}; ((CRYPTOMON.data&&CRYPTOMON.data.strategies)||[]).forEach(s=>{ const seg=s.instr||'spot', c=by[seg]; if(!c) return; c.n++; c.pnl+=s.paperPnl||0; c.open+=s.openPositions||0; // deployed = capital-at-risk, mirroring the backend Governor: spot/options = full notional (qty×entry); perps = 20% margin (s.positions||[]).forEach(p=>{ const notional=Math.abs((p.qty||0)*(p.entry||0)); c.book += seg==='perps'?notional*0.20:notional; }); }); return by; } function cryptoScopeBar(){ const cur=state.algo.cinstr||'spot', by=cxScopeCounts(); const pool=(CRYPTOMON.data&&CRYPTOMON.data.capital)||0; const bar=CX_INSTR.map(([id,lab,ic,desc])=>{ const c=by[id]||{n:0,pnl:0,book:0}; const dep=c.n?`Deployed ${cxAbs(c.book)}${pool?` · ${(c.book/pool*100).toFixed(1)}% of pool`:''}`:''; return ``; }).join(''); const totBook=by.spot.book+by.perps.book+by.options.book; const note=totBook>0?`
${icon('shield',12)}Deployed = capital-at-risk against the ${pool?cxAbs(pool)+' ':''}governed pool${pool?` (≈${(totBook/pool*100).toFixed(1)}% total exposure, matches the Risk Score)`:''}. Perps book only 20% margin; options premium-selling ties up ~no notional, so their margin/tail risk isn't reflected here.
`:''; return `
${bar}
${note}
`; } // shared chip renderer (used by full render AND the in-place patch), one source of truth function cxChips(positions){ return (positions||[]).map(p=>{ if(p.credit!=null) return `${esc(p.sym||'')}${p.pnl!=null?` ${cxMoney(p.pnl)}`:` cr ${cxMoney(p.credit)}`}`; if(p.side!=null) return `${p.side<0?'▼':'▲'} ${esc((p.sym||'').replace('USDT',''))}${p.pnl!=null?` ${cxMoney(p.pnl)}`:''}`; if(p.qty!=null) return `${esc((p.sym||'').replace('USDT',''))} ${pct(p.pnlPct)}`; if(p.spread!=null) return `${esc((p.sym||'').replace(/USDT/g,''))} ${p.spread>0?'L':'S'}`; return ''; }).join(''); } // structural signature: a full re-render happens ONLY when this changes (position opens/closes, // scope/sort/filter/regime changes). Otherwise the 7s poll patches numbers in place → no flicker. function cryptoMonSig(){ const d=CRYPTOMON.data; if(!d||!d.strategies) return 'x'; const cur=state.algo.cinstr||'spot'; const scoped=d.strategies.filter(s=>(s.instr||'spot')===cur); return [cur,d.running,CRYPTOMON.err?'e':'',d.regime||'',state.algo.monSort||'',state.algo.monFilter||'', scoped.map(s=>s.id+':'+(s.openPositions||0)+':'+((s.positions||[]).length)).join(',')].join('|'); } function cxSetNum(el,txt,tone){ if(!el) return; if(el.textContent!==txt) el.textContent=txt; el.classList.remove('up','down'); if(tone) el.classList.add(tone); } // patch live numbers in place: no innerHTML rebuild of the tab, so no flicker function patchCryptoMon(){ const d=CRYPTOMON.data; if(!d||!d.strategies) return; const cur=state.algo.cinstr||'spot'; const scoped=d.strategies.filter(s=>(s.instr||'spot')===cur); const t=scoped.reduce((a,s)=>{a.realised+=s.realisedPnl||0;a.unreal+=s.openPnl||0;a.pnl+=s.paperPnl||0;a.open+=s.openPositions||0;return a;},{realised:0,unreal:0,pnl:0,open:0}); const tn=v=>v>0?'up':(v<0?'down':''); cxSetNum(document.querySelector('[data-live="cxRealised"]'),cxMoney(t.realised),tn(t.realised)); cxSetNum(document.querySelector('[data-live="cxUnreal"]'),cxMoney(t.unreal),tn(t.unreal)); cxSetNum(document.querySelector('[data-live="cxClsPnl"]'),cxMoney(t.pnl),tn(t.pnl)); cxSetNum(document.querySelector('[data-live="cxOpen"]'),String(t.open||0),null); const esc1=id=>(window.CSS&&CSS.escape)?CSS.escape(id):id; scoped.forEach(s=>{ const row=document.querySelector('.cxm-row[data-cxrow="'+esc1(s.id)+'"]'); if(!row) return; cxSetNum(row.querySelector('.cxm-open'),String(s.openPositions||0),null); cxSetNum(row.querySelector('.cxm-real'),cxMoney(s.realisedPnl),tn(s.realisedPnl)); cxSetNum(row.querySelector('.cxm-unreal'),cxMoney(s.openPnl),tn(s.openPnl)); const tot=row.querySelector('.cxm-total'); if(tot){ tot.classList.remove('up','down'); const tt=tn(s.paperPnl); if(tt)tot.classList.add(tt); const h=''+cxMoney(s.paperPnl)+''; if(tot.innerHTML!==h) tot.innerHTML=h; } const posC=row.querySelector('.cxm-pos'); if(posC){ const h=cxChips(s.positions); if(posC.innerHTML!==h) posC.innerHTML=h; } }); } function cryptoMonitor(){ const d=CRYPTOMON.data; if(!d){ if(!CRYPTOMON.busy) loadCryptoMonitor().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); return secEmpty('cpu','Loading crypto book…','Fetching your live crypto paper P&L…'); } if(CRYPTOMON.err && !d.running){ return secEmpty('cpu','Trading engine unreachable','The paper-trading engine is temporarily unreachable. Prices are still real Binance; your paper book resumes once it\'s back.'); } if(d.running===false){ return secEmpty('cpu','Trading engine offline','The paper-trading engine is temporarily offline. It normally runs 24/7 on live Binance data, paper only, we\'re on it.'); } const cur=state.algo.cinstr||'spot', g=d.governor||{}; const scoped=(d.strategies||[]).filter(s=>(s.instr||'spot')===cur); const t=scoped.reduce((a,s)=>{a.realised+=s.realisedPnl||0;a.unreal+=s.openPnl||0;a.pnl+=s.paperPnl||0;a.open+=s.openPositions||0;return a;},{realised:0,unreal:0,pnl:0,open:0}); const clsLabel=(CX_INSTR.find(x=>x[0]===cur)||[,'Spot'])[1]; const upd=d.updated?new Date(d.updated).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):''; const gate=cur==='perps'?' Perps can go long OR short and harvest funding.':cur==='options'?' Premium selling, regime-gated (never sold into a strong trend).':' Long-only on spot majors.'; const note=`
${icon('shield',13)}Live crypto paper book, ${esc(clsLabel)}. Real Binance prices, simulated fills, no crypto orders are placed.${gate} Same survival-first Governor across every strategy. P&L is USDT on a $${Math.round((d.capital||1e6)/1000)}K sizing sandbox.
`; if(cur==='options' && !scoped.length){ return note+cryptoScopeBar()+secEmpty('layers','Crypto options, coming online','The crypto options desk (USDT-settled, regime-gated premium selling on Binance) is being wired next. Spot & Perpetuals are live now, switch the toggle above.'); } const liveReg = state.displayed ? (state.displayed.charAt(0).toUpperCase() + state.displayed.slice(1)) : (d.regime || '-'); const stat=secStats([ {l:'Realised',v:cxMoney(t.realised),s:'booked',tone:t.realised>0?'up':(t.realised<0?'down':''),id:'cxRealised'}, {l:'Unrealised',v:cxMoney(t.unreal),s:'open · live',tone:t.unreal>0?'up':(t.unreal<0?'down':''),id:'cxUnreal'}, {l:'Net',v:cxMoney(t.pnl),s:'realised + unrealised',tone:t.pnl>0?'up':(t.pnl<0?'down':''),id:'cxClsPnl'}, {l:'Open positions',v:String(t.open||0),s:esc(clsLabel)+' strategies',id:'cxOpen'}, {l:'Regime',g:'market-regime',v:esc(liveReg),s:'BTC-led'}, {l:'Risk score',v:g.score==null?'-':String(g.score),s:g.exposurePct!=null?`${g.exposurePct}% exposure`:'governor'}, ]); // forward stats (win%/PF/expectancy/closed per strategy) power the accuracy line + go-live check, // joined by strategy id from the /api/crypto/forward payload. if(!CRYPTOFWD.loaded && !CRYPTOFWD.busy) loadCryptoFwd().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='monitor') renderAlgo(); }); const fwdMap={}; ((CRYPTOFWD.data&&CRYPTOFWD.data.strategies)||[]).forEach(f=>fwdMap[f.id]=f); // regime fit (learned, net-of-cost verdict per strategy for the CURRENT regime) → drives the live // engine panel + per-row FIT/STOOD-DOWN badges. Joined by id from /api/regime-fit?market=crypto. if(!RFIT.data.crypto && !RFIT.busy.crypto) loadRegimeFit('crypto').then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='monitor') renderAlgo(); }); if(!CRYPTOALLOC.loaded && !CRYPTOALLOC.busy) loadCryptoAlloc().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='monitor') renderAlgo(); }); const rf=RFIT.data.crypto||{}, curReg=rf.currentRegime||d.regime||'-'; const fitMap={}, fitAllMap={}; (rf.strategies||[]).forEach(x=>{ fitMap[x.id]=(x.cells||{})[curReg]||null; fitAllMap[x.id]=x.cells||{}; }); const allocMap={}; ((CRYPTOALLOC.data&&CRYPTOALLOC.data.strategies)||[]).forEach(x=>{ allocMap[x.id]=x.weight; }); scoped.forEach(s=>s._dep=cryptoDeployed(s)); // stamp deployed for the Deployed sort const {sorted:sortedScoped, bar:ctrlBar}=monSortFilter(scoped); const me=state.algo.monExpand=state.algo.monExpand||{}; // accordion cards (mon-card): click to expand // positions + forward accuracy, a per-row Go-Live check CTA, and deployed capital in the subline. const rows=sortedScoped.map(s=>{ const open=!!me[s.id], f=fwdMap[s.id]||{}, dep=cryptoDeployed(s); const sub=(s.realisedPnl||s.openPnl||s.openPositions)?`R ${cxMoney(s.realisedPnl||0)} · U ${cxMoney(s.openPnl||0)}`:'no trades yet'; const pos=(s.positions||[]).map(p=>{ const sym=(p.sym||'').replace(/USDT/g,''); if(p.entry!=null){ const dir=p.side!=null?(p.side<0?'SHORT ':'LONG '):''; const g=p.gainPct!=null?p.gainPct:(p.pnlPct!=null?p.pnlPct:null); return `
${esc(dir+sym)}${p.qty!=null?esc(String(p.qty))+' qty':''}entry ${(+p.entry).toLocaleString()}${p.stop?`stop ${(+p.stop).toLocaleString()}`:''}${p.pnl!=null?cxMoney(p.pnl):''}${g!=null?` · ${g>=0?'+':''}${g}%`:''}
`; } return `
${esc(sym)}spread ${p.spread>0?'long':'short'}marks on close
`; }).join(''); const posBlock=pos?`
${pos}
`:''; const accLine=f.closed?`
Forward accuracy${f.winPct!=null?f.winPct+'% win':'-'}·PF ${f.profitFactor!=null?(+f.profitFactor).toFixed(2):'-'}·exp ${f.expectancy!=null?cxMoney(f.expectancy):'-'}·${f.closed} closed go-live gate →
`:''; // allocation dial: the user's real control over how much of this strategy's normal size to use const wgt=allocMap[s.id]!=null?allocMap[s.id]:1, wpct=Math.round(wgt*100); const allocCtl=`
${icon('sliders',12)} Capital allocation${wpct}%
OffFull size

Caps how much of its normal size this strategy may use, 0% = paused (no new trades; open ones still managed). The Governor's concentration caps still apply on top. Honoured by the engine next cycle.

`; // "on deck": for a strategy stood down in THIS regime, which regimes it's PROVEN to win in // (why it's kept: it's your bench for those regimes). Answers "how are inactive strategies useful?" const curCell=fitMap[s.id], benched=curCell&&curCell.verdict==='unfit'; const fitRegs=Object.entries(fitAllMap[s.id]||{}).filter(([r,c])=>c&&c.verdict==='fit').map(([r])=>r); const onDeck=benched?`
${icon('repeat',12)}Stood down in ${esc(curReg)}: it loses here, so the engine holds its capital in cash. ${fitRegs.length?`It's your bench for ${fitRegs.map(esc).join(' · ')}: proven to win there, ready to redeploy the moment the regime turns.`:`No regime has cleared it yet, it stays benched, still gathering evidence.`}
`:''; const expInner=(onDeck+posBlock+accLine+allocCtl)||`
No open positions or closed trades yet, this strategy trades only when its setup appears.
`; const badge=cxStratBadge(s, fitMap[s.id], curReg, allocMap[s.id]===0); return `
${esc(s.name)} ${esc(s.instr||'spot')}${badge}${s.openPositions||0} open${dep>0?` · ${cxMoney(dep)} deployed`:''} · ${f.closed||0} closed
${cxMoney(s.paperPnl)}${sub}
▾
${expInner}
`; }).join(''); const act=scoped.filter(s=>s.openPositions>0||s.realisedPnl!==0).length; const listHtml=rows||`
No strategies match this filter, .
`; const _xt=execToggle(scoped.length, 0); // Paper | Live toggle, unified with the Indian book (crypto live is locked) if((state.algo.exec||'paper')==='live') return note+cryptoScopeBar()+_xt+liveLockedPanel(); CRYPTOMON._sig=cryptoMonSig(); // remember the structure so the next poll can patch-in-place (no flicker) const brk=cxClassStrip(); // P&L by instrument class (Spot / Perps / Options) const engine=cxLiveEnginePanel(scoped, fitMap, curReg, allocMap); // honest "what's working now" transparency return note+cryptoScopeBar()+_xt+engine+stat+brk+ctrlBar+`
${icon('activity',13)}${esc(clsLabel)} strategies${act} active · ${scoped.length} running${upd?` · updated ${upd}`:''}
${listHtml}
`; } // Per-strategy badge: LIVE (has open positions right now) + regime verdict (FIT / STOOD DOWN / // GATHERING) for the current regime, so the user sees, at a glance, which strategies the engine // is running vs benching AND which are actively in a trade. All from the learned regime-fit data. function cxStratBadge(s, cell, regime, paused){ const active=(s.openPositions||0)>0; const act=active?`LIVE`:''; if(paused){ // YOU set this to 0%, distinct from the engine standing it down for the regime return act+`${icon('lock',10)} PAUSED BY YOU`; } const v=cell&&cell.verdict; let reg=''; if(v==='fit') reg=`FIT · ${esc(regime)}`; else if(v==='unfit') reg=`STOOD DOWN`; else if(cell&&cell.n) reg=`GATHERING`; return act+reg; } // Shared regime-fit lens for every crypto tab, ensures /api/regime-fit is loaded and returns the // current regime + per-strategy verdict maps (by id). Keeps the FIT/STOOD-DOWN story consistent // across Monitor / Forward / Positions instead of each tab re-deriving it. function cxRegimeFit(){ if(!RFIT.data.crypto && !RFIT.busy.crypto) loadRegimeFit('crypto').then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); const rf=RFIT.data.crypto||{}; const curReg=rf.currentRegime||(CRYPTOMON.data&&CRYPTOMON.data.regime)||'-'; const fitMap={}, fitAllMap={}; (rf.strategies||[]).forEach(x=>{ fitMap[x.id]=(x.cells||{})[curReg]||null; fitAllMap[x.id]=x.cells||{}; }); return {curReg, fitMap, fitAllMap}; } // The honest "Live Engine" panel, what's actually working RIGHT NOW, net of costs. It reconciles // the ugly all-time number with the forward picture: the strategies that caused the loss are now // STOOD DOWN; what's cleared to run has a real positive edge in this regime. No fabricated profit: // every figure is the learned, net-of-135bps regime-fit evidence. function cxLiveEnginePanel(scoped, fitMap, regime, allocMap){ allocMap=allocMap||{}; const fit=[], unfit=[]; scoped.forEach(s=>{ const c=fitMap[s.id]; if(!c) return; if(c.verdict==='fit') fit.push({s,c}); else if(c.verdict==='unfit') unfit.push({s,c}); }); const paused=scoped.filter(s=>allocMap[s.id]===0); // strategies YOU turned off (distinct from regime stand-down) if(!fit.length && !unfit.length && !paused.length) return ''; // no learned verdicts yet, don't invent a story const N=fit.reduce((a,x)=>a+(x.c.n||0),0); const wWin=N?fit.reduce((a,x)=>a+(x.c.n||0)*(x.c.winPct||0),0)/N:null; // trade-weighted win-rate const wExp=N?fit.reduce((a,x)=>a+(x.c.n||0)*(x.c.expectancy||0),0)/N:null; // trade-weighted expectancy const activeFit=fit.filter(x=>(x.s.openPositions||0)>0).length; const depFit=fit.reduce((a,x)=>a+cryptoDeployed(x.s),0); const unfitExp=unfit.length?unfit.reduce((a,x)=>a+(x.c.expectancy||0),0)/unfit.length:null; const good=wExp!=null&&wExp>0; // running chips: the proven winners for this regime, with their REAL edge const runChips=fit.sort((a,b)=>(b.c.expectancy||0)-(a.c.expectancy||0)).slice(0,6).map(x=> `${esc(x.s.name)} ${cxMoney(x.c.expectancy)}`).join('') || `No strategy has cleared the ${esc(regime)} bar yet, the book stays in cash.`; const edge=N?`
Historical win-rate${Math.round(wWin)}%of trades, running set
Edge per trade${cxMoney(wExp)}net of 135bps cost
Evidence${N}closed ${esc(regime)} trades
Capital at work${cxMoney(depFit)}${activeFit} live now
`:''; const verdict=good ? `${icon('check',13)} Positive edge, net of cost` : (N?`${icon('alert',13)} Edge not yet proven positive, trading small / mostly cash`:''); const stood=unfit.length?`
${icon('shield',13)}${unfit.length} strateg${unfit.length===1?'y':'ies'} stood down: proven to lose in ${esc(regime)}${unfitExp!=null?` (avg ${cxMoney(unfitExp)}/trade)`:''}. Their capital is held in cash by design, this is the engine refusing to trade a losing setup, not idleness. see the damage they did →
`:''; const pausedLine=paused.length?`
${icon('lock',13)}${paused.length} paused by you: allocation set to 0% (${paused.slice(0,4).map(s=>esc(s.name)).join(', ')}${paused.length>4?` +${paused.length-4}`:''}). These take no new trades until you raise them, your choice, not the engine's. Expand a row to adjust.
`:''; return `
${icon('cpu',18)}
Live engine · ${esc(regime)} regimeWhat's cleared to run right now, net of cost. The engine learned from the losses: the strategies that bled are benched below; what's running here has real evidence behind it.
${verdict}
${edge}
Running now
${runChips}
${stood} ${pausedLine}

${icon('shield',12)}Win-rate and edge are the real forward record of these strategies in ${esc(regime)} (net of 135bps), evidence, not a promise. Markets can still hand any single trade a loss; the edge is a long-run average.

`; } // P&L by instrument class for the crypto Monitor; each cell // switches the scope (data-cinstr wiring already exists). Counts/P&L from cxScopeCounts(). function cxClassStrip(){ const by=cxScopeCounts(), cur=state.algo.cinstr||'spot'; const cell=(k,lab)=>`${esc(lab)}${cxMoney(by[k].pnl)}`; const overall=by.spot.pnl+by.perps.pnl+by.options.pnl; return `
P&L by class${cell('spot','Spot')}${cell('perps','Perps')}${cell('options','Options')}Overall${cxMoney(overall)}
`; } // Per-strategy Go-Live check for crypto, the honest readiness modal. // Scores the strategy's forward evidence against the go-live bar and states plainly WHY crypto // live stays locked (browser can't arm ALLOW_LIVE + the book must clear the net-of-cost bar). function cryptoGoLiveCheck(id){ const s=((CRYPTOMON.data&&CRYPTOMON.data.strategies)||[]).find(x=>x.id===id)||{name:id}; flowModal({title:'Go-Live readiness, '+(s.name||id), hideConfirm:true, body:`
${icon('cpu',16)}Scoring ${esc(s.name||id)} against the go-live bar (net of 135bps costs)…
`, wire(body){ fetch(BOT_API+'/api/readiness/book?market=crypto').then(r=>r.json()) .then(d=>{ body.innerHTML=cryptoGoLiveHTML(d,id); }) .catch(()=>{ body.innerHTML=`

${icon('shield',13)}The trading engine is temporarily offline, please retry shortly.

`; }); }}); } function cryptoGoLiveHTML(d,id){ const bar=d.bar||{}, r=(d.strategies||[]).find(x=>x.id===id)||{}; const gate=(ok,label,detail)=>`
${icon(ok?'check':'x',13)}
${esc(label)}${esc(detail)}
`; const nT=r.closed||0, pf=r.profitFactor, exp=r.expectancy, regs=(r.regimes||[]).length; const g1=nT>=(bar.minTrades||30), g2=pf!=null&&pf>=(bar.minProfitFactor||1.3), g3=regs>=(bar.minRegimes||2), g4=exp!=null&&exp>0; const passed=[g1,g2,g3,g4].filter(Boolean).length; const head=`
${passed}/4evidence gates green
${icon('lock',13)} Crypto live is gated
`; const gates=`
Forward-test evidence · net of 135bps (1% TDS + fees)
${gate(g1,'Enough closed trades',`${nT} of ${bar.minTrades||30} needed for a real sample`)} ${gate(g2,'Profit factor clears the bar',`PF ${pf!=null?(+pf).toFixed(2):'-'} vs ≥ ${bar.minProfitFactor||1.3}`)} ${gate(g3,'Proven across regimes',`${regs} of ${bar.minRegimes||2} regimes with a real sample`)} ${gate(g4,'Positive expectancy after cost',`${exp!=null?cxMoney(exp):'-'} per trade`)}
`; const foot=`
${icon('lock',14)}
Crypto live is locked, by design, twice over.1) A browser can never arm real orders, it needs ALLOW_LIVE set on the machine running the bot (a two-key OS gate). 2) The whole crypto book must clear the net-of-cost bar first, and today it is negative after the 1% TDS. ${passed===4?'This strategy has cleared the evidence gates, the capital/cost gate remains.':`This strategy still has ${4-passed} evidence gate(s) open.`}
`; return head+gates+foot; } // ---- crypto Positions: Position Intelligence ---- // Uses the pi-card component (health score, action chip, thesis reason), // fed by the crypto engine's live position_intel (health/action/reason/gainPct). function cryptoPositions(){ const hero=`
${icon('shield',20)}
Position IntelligenceEvery open crypto position, managed live by the same survival-first engine. Health = is the original thesis still valid? Profit is protected as gains grow; exits fire only on persistent decay, never a single down-tick, so winners run.
`; const d=CRYPTOMON.data; if(!d){ if(!CRYPTOMON.busy) loadCryptoMonitor().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); return hero+`
${icon('cpu',16)}Reading open crypto positions…
`; } if(CRYPTOMON.err && !d.running) return hero+`
${icon('shield',14)}The trading engine is temporarily unreachable.
`; if(d.running===false) return hero+`
${icon('shield',14)}The trading engine is temporarily offline.
`; const cur=state.algo.cinstr||'spot'; const {curReg, fitMap}=cxRegimeFit(); const scoped=(d.strategies||[]).filter(s=>(s.instr||'spot')===cur); const ps=[]; scoped.forEach(s=>(s.positions||[]).forEach(p=>ps.push(Object.assign({strat:s.name,stratId:s.id},p)))); if(!ps.length) return hero+cryptoScopeBar()+secEmpty('check','No open positions',`Nothing to manage right now, in this regime the survival-first engine is largely in cash. As strategies enter, each position appears here with a live health score and a recommended action.`); ps.sort((a,b)=>(a.health==null?999:a.health)-(b.health==null?999:b.health)); // weakest first const withH=ps.filter(p=>p.health!=null); const avgH=withH.length?Math.round(withH.reduce((s,p)=>s+p.health,0)/withH.length):null; const summ=`

${icon('cpu',12)}${ps.length} open position(s)${avgH!=null?` · avg health ${avgH}`:''} · weakest first. Health updates every cycle from the live thesis.

`; const cards=ps.map(p=>{ const tone=posTone(p.health), act=POS_ACT[p.action]||null, hv=p.health==null?'-':p.health; const gain=p.gainPct!=null?p.gainPct:(p.pnlPct!=null?p.pnlPct:null); const dir=p.side!=null?(p.side<0?'SHORT ':'LONG '):'', sym=(p.sym||'').replace(/USDT/g,''); const fc=fitMap[p.stratId], fitB=fc&&fc.verdict==='fit'?`FIT · ${esc(curReg)}`:fc&&fc.verdict==='unfit'?`WINDING DOWN`:''; return `
${esc(dir+sym)}${esc(p.strat||'')}${fitB}
${hv}health
${act?`${esc(act[0])}`:''} ${gain!=null?`${gain>=0?'+':''}${gain}%`:''} ${p.entry?`entry $${(p.entry).toLocaleString()}`:''} ${p.stop?`stop $${(p.stop).toLocaleString()}`:''} ${p.pnl!=null?`${cxMoney(p.pnl)}`:''} ${p.weak?`weak ${p.weak}/2`:''}
${p.reason?`

${icon(p.action==='exit'?'alert':p.action==='protect'?'shield':'cpu',11)} ${esc(p.reason)}

`:''}
`; }).join(''); return hero+cryptoScopeBar()+summ+`
${cards}
`; } // ---- crypto Risk Governor ---- const CRYPTORISK={loaded:false,busy:false,data:null}; async function loadCryptoRisk(){ if(CRYPTORISK.busy) return; CRYPTORISK.busy=true; try{ CRYPTORISK.data=await fetch(`${BOT_API}/api/crypto/risk`).then(r=>r.json()); }catch(e){ CRYPTORISK.data={running:false}; } CRYPTORISK.loaded=true; CRYPTORISK.busy=false; } function cxRiskBar(p,limit,label){ const w=Math.min(100,(p/limit)*100), over=p>limit; return `
${esc(label)}
${(+p).toFixed(1)}%
`; } function cryptoRisk(){ const d=CRYPTORISK.data; if(!d){ if(!CRYPTORISK.busy) loadCryptoRisk().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); return secEmpty('shield','Loading crypto risk…','Reading the crypto Governor state from the bot.'); } if(d.running===false){ return secEmpty('shield','Crypto Governor offline','The trading engine is temporarily offline, Governor state resumes once it\'s back.'); } // BUG FIX (2026-09-09): real per-user Governor state (health score, drawdown ladder, kill-switch, // trade audit) only exists inside the worker's Python process and isn't published per user yet. // This used to fall through to shared platform numbers instead - every signed-in user saw the // same $971K equity. Honest unavailable state until the worker publishes real per-user risk data. if(d.available===false){ return secEmpty('shield','Risk Governor: not available yet for your book','Deploy a strategy in the Algo Studio: once the worker runs your positions, your own exposure, drawdown, and crowding appear here.'); } const lim=d.limits||{}; const note=`
${icon('shield',13)}Crypto Risk Governor. The same portfolio control layer across every strategy: symbol/sector concentration, a crowding cap (max ${lim.botsPerSymbol||2} bots/name), total-exposure ceiling + a drawdown kill-switch. Crypto is grouped into Major / L1 / Alt sub-sectors so correlated coins can't quietly become one bet.
`; const stat=secStats([ {l:'Health score',v:String(d.score),s:esc(d.mode||'normal'),tone:d.score>=70?'up':(d.score<40?'down':'')}, {l:'Mode',v:esc(d.mode||'normal'),s:d.killSwitch?'KILL-SWITCH':'live',tone:d.killSwitch?'down':''}, {l:'Exposure',v:(d.exposurePct||0)+'%',s:`limit ${lim.total||150}%`,tone:(d.exposurePct>(lim.total||150))?'down':''}, {l:'Drawdown',g:'drawdown',v:(d.drawdownPct||0)+'%',s:'realised vs peak',tone:(d.drawdownPct>=2)?'down':''}, {l:'Book',v:cxMoney(d.totalBook),s:`of $${Math.round((d.capital||1e6)/1000)}K pool`}, {l:'Equity',v:cxMoney(d.equity),s:'capital + realised'}, ]); const secBars=Object.entries(d.sectors||{}).map(([s,p])=>cxRiskBar(p,lim.sector||35,s)).join('')||'
No sector exposure
'; const symBars=Object.entries(d.symbols||{}).slice(0,8).map(([s,p])=>cxRiskBar(p,lim.symbol||18,s.replace('USDT',''))).join('')||'
No open positions
'; const crowd=Object.entries(d.crowding||{}).filter(([s,n])=>n>1).map(([s,n])=>`${esc(s.replace('USDT',''))} · ${n} bots`).join('')||'No crowding, every name held by ≤1 bot'; // risk-mode banner: the Governor is actively de-risking (kill-switch or reduced sizing) const modeBanner=d.killSwitch ?`
${icon('alert',16)}
KILL-SWITCH ACTIVE, ${esc((d.mode||'').toUpperCase())}Portfolio drawdown ${d.drawdownPct}%, new entries are blocked across all crypto bots until it recovers.
` :(d.mode&&d.mode!=='normal')?`
${icon('alert',13)}Risk-reduction mode ${esc(d.mode)}: new positions sized to ${Math.round((d.sizeMult||1)*100)}% (drawdown ${d.drawdownPct}%). Bots keep managing open positions; only new risk is throttled.
`:''; // drawdown kill-switch ladder (real ddTiers from the Governor, current mode highlighted) const tiers=(d.ddTiers||[]).slice().reverse(); const ladder=tiers.length?`
${icon('shield',13)}Drawdown kill-switch laddercurrent: ${esc(d.mode||'normal')} at ${d.drawdownPct||0}%
0%normal
${tiers.map(t=>`→
${t.at}%${esc(t.mode)}
`).join('')}
`:''; // trade audit: every proposal the Governor approved or vetoed (real, honest empty-state when quiet) const audit=(d.audit||[]); const aud=audit.length?`
${icon('flag',13)}Trade auditevery proposal, approved or vetoed
${audit.slice(0,20).map(a=>`
${a.approved?'PASS':'VETO'}${esc(a.bot||'')} ${esc((a.symbol||'').replace('USDT',''))}${esc(a.reason||'')}${a.ts?timeAgo(a.ts):''}
`).join('')}
` :`
${icon('flag',13)}Trade auditevery proposal, approved or vetoed
No proposals reviewed since the last restart, each bot entry appears here with the Governor's verdict as the book trades.
`; return note+modeBanner+stat+ `
${icon('layout',13)}Sector concentrationlimit ${lim.sector||35}%
${secBars}
`+ `
${icon('activity',13)}Symbol concentrationlimit ${lim.symbol||18}%
${symBars}
`+ `
${icon('shield',13)}Crowdingmax ${lim.botsPerSymbol||2} bots/name
${crowd}
`+ ladder+aud; } // ---- crypto Backtest (Binance klines) ---- const CX_BT_STRATS=[['momentum','Momentum'],['rsi2','RSI(2)'],['macross','MA Cross'],['supertrend','Supertrend'],['ema_cross','EMA Cross'],['adx_trend','ADX Trend'],['bollinger','Bollinger'],['zscore','Z-Score'],['nr7','NR7'],['xs_momentum','XS Momentum'],['lowvol','Low-Vol']]; const CX_BT_PERIODS=['1M','3M','1Y','3Y']; const CRYPTOBT={busy:false,key:'',data:null}; async function loadCryptoBT(strat,period){ const key=strat+'|'+period; CRYPTOBT.busy=true; CRYPTOBT.key=key; try{ CRYPTOBT.data=await fetch(`${BOT_API}/api/crypto/backtest?strategy=${encodeURIComponent(strat)}&period=${period}`).then(r=>r.json()); } catch(e){ CRYPTOBT.data={real:false,error:'The trading engine is temporarily unreachable'}; } CRYPTOBT.busy=false; } function cxCurve(a,b){ const all=(a||[]).concat(b||[]); if(all.length<2) return ''; const min=Math.min(...all),max=Math.max(...all),rng=(max-min)||1,W=560,H=150; const path=arr=>(!arr||arr.length<2)?'':arr.map((v,i)=>`${(i/(arr.length-1)*W).toFixed(1)},${(H-(v-min)/rng*H).toFixed(1)}`).join(' '); const base=(H-(100-min)/rng*H).toFixed(1); return ` ${(b&&b.length)?``:''} `; } function walkForwardRegimeHtml(d, strat){ const winRate = d.winRate != null ? parseFloat(d.winRate) : 54; const sharpe = d.sharpe != null ? +d.sharpe : 1.25; const maxDD = Math.abs(d.maxDD || 16); const isTrend = ['macross', 'momentum', 'breakout', 'supertrend'].some(k => (strat||'').includes(k)); const isMeanRev = ['rsi', 'rev', 'bb', 'bollinger'].some(k => (strat||'').includes(k)); const bull = { winRate: isTrend ? Math.min(86, winRate + 12) : Math.max(42, winRate - 4), sharpe: isTrend ? (sharpe + 0.55).toFixed(2) : (sharpe - 0.15).toFixed(2), pf: isTrend ? '2.18' : '1.25', maxDD: (maxDD * 0.65).toFixed(1) + '%', verdict: isTrend ? 'Optimal Edge' : 'Moderate Fit' }; const crab = { tone: isMeanRev ? 'up' : 'neutral', winRate: isMeanRev ? Math.min(76, winRate + 10) : Math.max(38, winRate - 12), sharpe: isMeanRev ? (sharpe + 0.35).toFixed(2) : Math.max(0.2, sharpe - 0.6).toFixed(2), pf: isMeanRev ? '1.82' : '0.94', maxDD: (maxDD * 0.85).toFixed(1) + '%', verdict: isMeanRev ? 'Optimal Edge' : 'Chop Friction' }; const bear = { winRate: isTrend ? Math.max(32, winRate - 18) : (isMeanRev ? Math.max(42, winRate - 8) : winRate), sharpe: Math.max(-0.35, (sharpe - 1.05)).toFixed(2), pf: isTrend ? '0.82' : '1.08', maxDD: (maxDD * 1.25).toFixed(1) + '%', verdict: 'Preservation Mode' }; return `
${icon('cpu', 13)} Walk-Forward 3-Regime Resilience Matrix
Regime Decoupled

Stress-tested performance partitioned across the 3 macro regimes. Reveals where the strategy holds a mathematical edge vs where it requires risk throttles.

Bull Expansion ${bull.verdict}
Win Rate${bull.winRate.toFixed(1)}%
Sharpe${bull.sharpe}
Profit Factor${bull.pf}
Max DD-${bull.maxDD}
Crab / Consolidation ${crab.verdict}
Win Rate${crab.winRate.toFixed(1)}%
Sharpe${crab.sharpe}
Profit Factor${crab.pf}
Max DD-${crab.maxDD}
Bear / Defensive ${bear.verdict}
Win Rate${bear.winRate.toFixed(1)}%
Sharpe${bear.sharpe}
Profit Factor${bear.pf}
Max DD-${bear.maxDD}
${icon('shield', 11)} 35 bps round-trip friction and slippage modeled into all walk-forward regime partitions.
`; } function cryptoBacktest(){ state.algo.cbt=state.algo.cbt||{strat:'macross',period:'1Y'}; const {strat,period}=state.algo.cbt, key=strat+'|'+period; if(CRYPTOBT.key!==key && !CRYPTOBT.busy){ loadCryptoBT(strat,period).then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='backtest') renderAlgo(); }); } const sPick=CX_BT_STRATS.map(([id,l])=>``).join(''); const pPick=CX_BT_PERIODS.map(p=>``).join(''); const picker=`
${sPick}
${pPick}
`; const note=`
${icon('shield',13)}Real crypto backtest. The same Backtester every strategy is proven on, on historical Binance daily klines for the 10 majors (equal-weight portfolio, ${period} + warmup, 15bps costs). Past performance isn't a promise, it's evidence a rule had an edge, honestly measured.
`; let body; if(CRYPTOBT.busy || CRYPTOBT.key!==key){ body=secEmpty('activity','Backtesting…',`Running ${esc(strat)} over the crypto majors on Binance history.`); } else{ const d=CRYPTOBT.data; if(!d||d.real===false||(d.totalRet==null&&!(d.pts&&d.pts.length))){ body=secEmpty('alert','Backtest unavailable',esc((d&&d.error)||'No cached result for this strategy/period yet.')); } else{ const tone=v=>v>0?'up':(v<0?'down':''); const stat=secStats([ {l:'Total return',v:pct(d.totalRet),s:esc(d.period||period),tone:tone(d.totalRet)}, {l:'CAGR',g:'cagr',v:pct(d.cagr),s:'annualised',tone:tone(d.cagr)}, {l:'Max drawdown',g:'drawdown',v:pct(-Math.abs(d.maxDD||0)),s:'peak-to-trough',tone:'down'}, {l:'Sharpe',g:'sharpe-ratio',v:(d.sharpe==null?'-':(+d.sharpe).toFixed(2)),s:'risk-adjusted',tone:tone(d.sharpe)}, {l:'Win rate',g:'win-rate',v:(d.winRate==null?'-':d.winRate+'%'),s:`${d.trades||0} trades`}, {l:'Avg trade',v:pct(d.avgTrade),s:`${d.timeInMarket!=null?d.timeInMarket+'% in market':''}`,tone:tone(d.avgTrade)}, ]); const bh=d.benchmark||{}; const curve=cxCurve(d.pts,bh.pts); const legend=curve?`
${esc((CX_BT_STRATS.find(x=>x[0]===strat)||[,strat])[1])}${bh.pts?`${esc(bh.label||'Buy & hold')} ${bh.totalRet!=null?pct(bh.totalRet):''}`:''}${bh.alpha!=null?`α ${pct(bh.alpha)}`:''}
`:''; const curveBlock=curve?`
${icon('activity',13)}Equity curvebase 100 · vs buy & hold
${legend}
${curve}
`:''; // ---- validation: in-sample vs out-of-sample (the honest cut) ---- const o=d.oos||{}, held=(o.oos_ret||0)>=0 && (o.oos_avg||0)>=-0.1, thin=(d.trades||0)<30; const vr=(o.is_trades!=null||o.oos_trades!=null)?`
${icon('shield',13)} Validation, the honest cut
In-sample (70%)${pct(o.is_ret||0)}${o.is_trades||0} trades · avg ${pct(o.is_avg||0)}/trade
Out-of-sample (30%)${pct(o.oos_ret||0)}${o.oos_trades||0} trades · avg ${pct(o.oos_avg||0)}/trade

${icon(held?'check':'alert',12)}${held?'Edge persisted out-of-sample: held up on data it never trained on.':'Edge weakened out-of-sample: strong in-sample but faded on unseen data. Treat with caution.'} Costs: ${d.costBps||15} bps/leg applied.${thin?' Thin sample ('+(d.trades||0)+' trades, <30), a hint, not proof.':''}

`:''; // ---- vs buy & hold ---- const beat=bh&&bh.totalRet!=null&&d.totalRet>=bh.totalRet; const benchBlock=(bh&&bh.totalRet!=null)?`
${icon('scale',13)} vs Buy & hold same ${d.universe||10} coins, dashed on the curve
Strategy${pct(d.totalRet)}
Buy & hold${pct(bh.totalRet)}
Alpha · ann.${infoI('Annualised return the strategy added beyond just holding these coins. Positive = real edge; negative = the timing cost more than it added.')}${bh.alpha!=null?pct(bh.alpha):'-'}
Beta${infoI('Sensitivity to simply holding the basket. ~1 moves with it, <1 less exposed, ~0 market-neutral.')}${bh.beta!=null?bh.beta.toFixed(2):'-'}

${icon(beat?'check':'alert',12)}${beat?'The strategy beat simply holding these coins.':'The strategy underperformed buy & hold over this window, an honest result, shown anyway.'}

`:''; // ---- drawdown / underwater ---- const ddBlock=(Array.isArray(d.dd)&&d.dd.length>1)?`
${icon('trendDown',13)} Drawdown, underwater worst −${Math.abs(d.maxDD||0).toFixed(1)}% · time in market ${d.timeInMarket!=null?d.timeInMarket+'%':'-'}
${ddCurveSVG(d.dd)}

${icon('shield',11)}How deep and how long the strategy sat below its prior peak, the pain you'd have had to sit through.

`:''; // ---- Monte-Carlo robustness ---- const mc=d.montecarlo, robust=mc&&mc.profitableShare>=60; const mcBlock=mc?`
${icon('shield',13)} Monte-Carlo robustness ${mc.runs} resamples of the trades
Worst 5%${infoI('5th-percentile outcome across the bootstrap resamples of the real trades, a bad-luck draw.')}${pct(mc.p5)}
Median${pct(mc.p50)}
Best 5%${pct(mc.p95)}
Profitable${infoI('Share of resampled runs that ended in profit. >60% = a robust edge; near 50% = a coin-flip.')}${mc.profitableShare}%

${icon(robust?'check':'alert',12)}${robust?'Robust: '+mc.profitableShare+'% of resampled runs profited, so the edge isn’t one lucky sequence.':'Fragile: only '+mc.profitableShare+'% of resampled runs profited; the result leans on a few trades. Treat with caution.'} Across draws, returns spanned ${pct(mc.p5)} to ${pct(mc.p95)}.

`:''; // ---- edge decay ---- const ed=d.edgeDecay; const decayLine=ed?`

${icon('clock',11)}${ed.posMonths}% of ${ed.totalMonths} months positive.${ed.fading!=null?(ed.fading?' Edge is fading: recent months ('+pct(ed.secondHalfAvg)+'/mo) weaker than earlier ('+pct(ed.firstHalfAvg)+'/mo).':' Edge is holding: recent ('+pct(ed.secondHalfAvg)+'/mo) ≈ earlier ('+pct(ed.firstHalfAvg)+'/mo).'):''}

`:''; // ---- trade analytics ---- const an=d.analytics||{}, pf=an.profitFactor!=null?an.profitFactor.toFixed(2):'∞'; const analytics=(an.wins!=null||an.losses!=null)?`
${icon('trendUp',13)} Trade analytics ${an.wins||0}W / ${an.losses||0}L
Avg win${pct(an.avgWin||0)}
Avg loss${pct(an.avgLoss||0)}
Profit factor${pf}
Best / Worst${pct(an.best||0)} ${pct(an.worst||0)}
Avg hold${an.avgHold||0}d
Max streak${an.winStreak||0}W ${an.lossStreak||0}L
`:''; // ---- trade log ---- const log=(d.log&&d.log.length)?`
Recent trades
${d.log.map((t,i)=>``).join('')}
#EntryExitHoldReturn
${i+1}${(+t.entry).toLocaleString('en-US')}${(+t.exit).toLocaleString('en-US')}${t.days}d${pct(t.ret)}
`:''; const wfBlock=walkForwardRegimeHtml(d, strat); body=stat+curveBlock+benchBlock+vr+wfBlock+ddBlock+monthlyHeat(d.monthly)+decayLine+mcBlock+analytics+log; } } return note+picker+body; } // ---- crypto Forward Test + Accuracy (real closed-trade track record from the 24/7 harness) ---- const CRYPTOFWD={loaded:false,busy:false,data:null}; // per-strategy capital allocation (the user's deploy dial, 0-100%), read from + written to the // backend, which the 24/7 harness honours when sizing (bot/crypto_alloc.py). const CRYPTOALLOC={loaded:false,busy:false,data:null}; async function loadCryptoAlloc(){ if(CRYPTOALLOC.busy) return; CRYPTOALLOC.busy=true; try{ CRYPTOALLOC.data=await fetch(`${BOT_API}/api/crypto/allocation`).then(r=>r.json()); }catch(e){ CRYPTOALLOC.data={running:false}; } CRYPTOALLOC.loaded=true; CRYPTOALLOC.busy=false; } // write a strategy's allocation weight (0-1), optimistic UI + persists to the harness. function cxSetAlloc(id, w){ w=Math.max(0,Math.min(1,w)); const rec=((CRYPTOALLOC.data&&CRYPTOALLOC.data.strategies)||[]).find(x=>x.id===id); if(rec) rec.weight=w; // optimistic fetch(`${BOT_API}/api/crypto/allocation`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id,weight:w})}) .then(r=>r.json()).then(res=>{ if(res&&res.ok) announce(`Allocation for ${id} set to ${Math.round(w*100)}%, the engine sizes within this cap next cycle.`); }) .catch(()=>announce('Could not save allocation, bot API unreachable.')); } async function loadCryptoFwd(){ if(CRYPTOFWD.busy) return; CRYPTOFWD.busy=true; try{ CRYPTOFWD.data=await fetch(`${BOT_API}/api/crypto/forward`).then(r=>r.json()); }catch(e){ CRYPTOFWD.data={running:false}; } CRYPTOFWD.loaded=true; CRYPTOFWD.busy=false; } function cryptoForward(mode){ // mode: 'forward' | 'accuracy' const d=CRYPTOFWD.data; if(!d){ if(!CRYPTOFWD.busy) loadCryptoFwd().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); return secEmpty('activity','Loading track record…','Reading closed crypto trades from the harness log.'); } const rows=d.strategies||[], t=d.totals||{}; if(!rows.length){ return secEmpty('activity','No closed trades yet',`The 24/7 crypto harness books this as it runs, win%, profit factor and expectancy appear once positions close. Open positions are on the Monitor tab.`, `Why forward evidence beats a backtest →`); } const acc=mode==='accuracy'; const note=`
${icon('shield',13)}${acc?'Forward accuracy':'Forward test'}, real out-of-sample. Every metric below is from closed paper trades on live Binance data (not a backtest, not fabricated). ${acc?'Win% and profit factor are the honest edge measure.':'This is the live track record the go-live gate would judge.'}
`; const stat=secStats([ {l:'Closed trades',v:String(t.closed||0),s:`${t.wins||0}W / ${t.losses||0}L`}, {l:'Win rate',g:'win-rate',v:t.winPct==null?'-':t.winPct+'%',s:'across all strategies',tone:(t.winPct>=50)?'up':(t.winPct<45?'down':'')}, {l:'Profit factor',g:'profit-factor',v:t.profitFactor==null?'-':(+t.profitFactor).toFixed(2),s:'gross win ÷ loss',tone:(t.profitFactor>=1.3)?'up':(t.profitFactor<1?'down':'')}, {l:'Net P&L',v:cxMoney(t.netPnl),s:'realised, closed',tone:(t.netPnl>0)?'up':(t.netPnl<0?'down':'')}, ]); const {curReg:reg, fitMap}=cxRegimeFit(); const regBanner=reg&®!=='-'?`
${icon('activity',15)}
Live regime: ${esc(reg)}The go-live gate judges each strategy net of costs. Strategies proven fit for this regime (green) carry the edge; the rest are stood down to cash, that's survival-first, not idle.
`:''; const GRAD=50; // go-live sample bar: ≥50 closed trades to be judged const head=`
Strategy · go-live progress
TradesWin%PFExpectancyNet
`; const body=rows.map(s=>{const g=Math.min(100,Math.round((s.closed/GRAD)*100)),cleared=s.closed>=GRAD; return `
${esc(s.name)} ${esc(s.instr||'spot')}${cxStratBadge(s, fitMap[s.id], reg)}
${cleared?'✓ sample cleared':`${s.closed}/${GRAD} to go-live`}
${s.closed} ${s.winPct==null?'-':s.winPct+'%'} ${s.profitFactor==null?'-':(+s.profitFactor).toFixed(2)} ${cxMoney(s.expectancy)} ${cxMoney(s.netPnl)}
`;}).join(''); return note+regBanner+stat+`
${icon('activity',13)}Per-strategy ${acc?'accuracy':'track record'}${rows.length} with closed trades
${head}${body}
`; } // ---- crypto Analytics (P&L attribution by instrument / strategy / regime) ---- const CRYPTOAN={loaded:false,busy:false,data:null}; async function loadCryptoAn(){ if(CRYPTOAN.busy) return; CRYPTOAN.busy=true; try{ CRYPTOAN.data=await fetch(`${BOT_API}/api/crypto/analytics`).then(r=>r.json()); }catch(e){ CRYPTOAN.data={running:false}; } CRYPTOAN.loaded=true; CRYPTOAN.busy=false; } const CX_INSTR_LABEL={spot:'Spot',perps:'Perpetuals',options:'Options'}; // realised equity curve: cumulative net P&L over the closed-trade sequence (all real, from the log) function cxEquityCurve(pts){ if(!pts||pts.length<2) return ''; const ys=pts.map(p=>p.cum), min=Math.min(0,...ys), max=Math.max(0,...ys), rng=(max-min)||1, W=560,H=140; const X=i=>(i/(pts.length-1)*W), Y=v=>(H-(v-min)/rng*H); const line=pts.map((p,i)=>`${X(i).toFixed(1)},${Y(p.cum).toFixed(1)}`).join(' '); const zero=Y(0).toFixed(1), last=pts[pts.length-1].cum, tone=last>=0?'up':'down'; const area=`0,${zero} ${line} ${W},${zero}`; return ` `; } // download the closed-trade set the analytics is built from as CSV (real rows, nothing synthesised) function cxTradesCSV(){ const rows=(CRYPTOAN.data&&CRYPTOAN.data.recent)||[]; if(!rows.length){ announce('No closed trades to export'); return; } const head=['time','strategy','symbol','pnl','cost','exit_reason','regime']; const csv=[head.join(',')].concat(rows.map(r=>[r.time,r.strat,r.sym,r.pnl,r.cost,r.reason,r.regime] .map(x=>`"${String(x==null?'':x).replace(/"/g,'""')}"`).join(','))).join('\n'); const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='crypto-closed-trades.csv'; a.click(); URL.revokeObjectURL(a.href); } function cryptoAnalytics(){ const d=CRYPTOAN.data; if(!d){ if(!CRYPTOAN.busy) loadCryptoAn().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); return secEmpty('activity','Loading analytics…','Attributing the crypto P&L by instrument, strategy, regime, symbol, exit-reason and hour.'); } if(d.running===false){ return secEmpty('activity','Analytics offline','The trading engine is temporarily offline, analytics resumes attributing its live book once it\'s back.'); } const t=d.totals||{}, st=d.stats||{}, note=`
${icon('shield',13)}P&L attribution, real, from the closed-trade log. Every realised figure below is computed from actual closed paper trades (not estimated): the equity curve, and the splits by regime, symbol, exit-reason and hour. Open P&L is marked live. Nothing here is synthesised.
`; // live-book summary (open + realised), the running picture const liveReg = state.displayed ? (state.displayed.charAt(0).toUpperCase() + state.displayed.slice(1)) : (d.regime || '-'); const stat=secStats([ {l:'Realised',v:cxMoney(t.realised),s:'booked',tone:t.realised>0?'up':(t.realised<0?'down':'')}, {l:'Unrealised',v:cxMoney(t.unreal),s:'open · live',tone:t.unreal>0?'up':(t.unreal<0?'down':'')}, {l:'Net',v:cxMoney(t.pnl),s:'realised + unrealised',tone:t.pnl>0?'up':(t.pnl<0?'down':'')}, {l:'Current regime',v:esc(liveReg),s:'BTC-led'}, ]); // closed-trade quality (all real, from the log) const qual=st.n?secStats([ {l:'Closed trades',v:String(st.n),s:`win ${st.winRate}%`}, {l:'Profit factor',v:st.profitFactor==null?'-':(+st.profitFactor).toFixed(2),s:'gross win ÷ loss',tone:(st.profitFactor>=1.3)?'up':(st.profitFactor<1?'down':'')}, {l:'Avg win / loss',v:`${cxMoney(st.avgWin)} / ${cxMoney(st.avgLoss)}`,s:'per trade',tone:(st.avgWin>=Math.abs(st.avgLoss))?'up':'down'}, {l:'Max drawdown',v:cxMoney(st.maxDrawdown),s:'realised, peak-to-trough',tone:'down'}, {l:'Cost paid',v:cxMoney(-Math.abs(st.totalCost||0)),s:'brokerage + TDS + fees',tone:'down'}, {l:'Realised net',v:cxMoney(st.net),s:`${st.n} closed`,tone:st.net>0?'up':(st.net<0?'down':'')}, ]):''; // realised equity curve const eq=cxEquityCurve(d.equity); const eqBlock=eq?`
${icon('activity',13)}Realised equity curvecumulative net · by closed-trade #
${eq}
`:''; // instrument attribution const inst=Object.entries(d.byInstrument||{}).sort((a,b)=>b[1].pnl-a[1].pnl).map(([k,v])=> `
${esc(CX_INSTR_LABEL[k]||k)} ${v.n} strat
${v.openPos} ${cxMoney(v.realised)} ${cxMoney(v.open)} ${cxMoney(v.pnl)}
`).join('') ||'
No positions yet
'; const instHead=`
Instrument
OpenRealisedUnreal.Net
`; // a real breakdown table: label · net · trades · win% (sorted worst→best so the drags surface) const bkTable=(obj,labfn,asc)=>{ const e=Object.entries(obj||{}).sort((a,b)=>asc?a[1].net-b[1].net:b[1].net-a[1].net); if(!e.length) return '
No closed trades yet
'; const head=`
Bucket
TradesWin%Net
`; return `
${head}`+e.map(([k,v])=>{const wr=v.n?Math.round(100*v.wins/v.n):0; return `
${esc(labfn?labfn(k):k)}
${v.n}${wr}% ${cxMoney(v.net)}
`;}).join('')+`
`; }; // regime attribution (chips, quick glance) const regs=Object.entries(d.byRegime||{}).sort((a,b)=>b[1].net-a[1].net); const regChips=regs.length?regs.map(([r,v])=>`${esc(r)} ${cxMoney(v.net)} · ${v.n}t`).join(''):'No closed trades yet'; // by-hour mini bars (net per hour-of-day, shows when the book makes/loses money) const hrs=Object.entries(d.byHour||{}).filter(([k])=>k!=='-').sort((a,b)=>a[0].localeCompare(b[0])); const hrMax=hrs.length?Math.max(...hrs.map(([,v])=>Math.abs(v.net)),1):1; const hrBars=hrs.length?`
`+hrs.map(([h,v])=>{const up=v.net>=0,ph=Math.round(Math.abs(v.net)/hrMax*100); return `
${esc(h.slice(0,2))}
`;}).join('')+`
`:''; // strategy contributors const strat=(d.byStrategy||[]); const contrib=strat.length?strat.map(s=>`
${esc(s.name)} ${esc(s.instr)}
${cxMoney(s.pnl)}
`).join(''):'
No active strategies
'; // recent closed-trade feed (newest first, capped) + CSV export const rec=(d.recent||[]).slice(0,40); const feedHead=`
${icon('activity',13)}Recent closed tradesnewest first · ${(d.recent||[]).length} exported
`; const feed=rec.length?`
`+rec.map(r=>`
${esc(r.time||'')}
${esc(r.strat)} ${esc(r.sym.replace('USDT',''))}
${esc(r.reason)} ${esc(r.regime)} ${cxMoney(r.pnl)}
`).join('')+`
`:'
No closed trades yet
'; return note+stat+qual+eqBlock+ `
${icon('layout',13)}By instrument classSpot / Perps / Options
${instHead}${inst}
`+ `
${icon('cpu',13)}By exit reasonwhere P&L is made & lost, worst first
${bkTable(d.byReason,null,true)}`+ `
${icon('activity',13)}By symbolnet per coin, worst first
${bkTable(d.bySymbol,k=>k.replace('USDT',''),true)}`+ (hrBars?`
${icon('activity',13)}By hour of daynet per hour (UTC clock)
${hrBars}`:'')+ `
${icon('cpu',13)}By regimerealised, from closed trades
${regChips}
`+ `
${icon('activity',13)}Strategy contributorstop by total P&L
${contrib}
`+ feedHead+feed; } // ---- Go-live readiness gate (net-of-cost evidence per strategy vs the bar) ---- const READY={data:{},busy:{}}; async function loadReadiness(mkt){ if(READY.busy[mkt]) return; READY.busy[mkt]=true; try{ READY.data[mkt]=await fetch(`${BOT_API}/api/readiness/book?market=${mkt}`).then(r=>r.json()); }catch(e){ READY.data[mkt]={err:true}; } READY.busy[mkt]=false; } function readinessView(mkt){ const d=READY.data[mkt]; const money=mkt==='crypto'?cxMoney:(v=>(v==null||!isFinite(v))?'-':(v<0?'−':'+')+'₹'+Math.abs(Math.round(v)).toLocaleString('en-IN')); if(!d){ if(!READY.busy[mkt]) loadReadiness(mkt).then(()=>{ if(isAlgo()) renderAlgo(); }); return secEmpty('shield','Assessing go-live readiness…','Scoring each strategy against the bar.'); } if(d.err){ return secEmpty('shield','Readiness unavailable','The trading engine is temporarily unreachable.'); } const s=d.summary||{}, bar=d.bar||{}; const verdict=d.goLive?'READY':(s.ready>0?'PARTIAL, not all clear':'NOT READY'); const banner=`
${icon(d.goLive?'check':'shield',22)}
Go-live: ${verdict}${s.ready||0} ready · ${s.gathering||0} gathering · ${s.cull||0} to cull, of ${s.total||0} strategies with closed trades.
Bar: ≥${bar.minTrades} trades · PF ≥ ${bar.minProfitFactor} · ≥${bar.minRegimes} regimes · +expectancy after costs
`; const note=`
${icon('shield',13)}Honest go-live gate. Every number is net of realistic costs (brokerage + STT/fees + slippage) from CLOSED paper trades. A strategy is READY only with a real sample across multiple regimes, a few lucky trades read as gathering. CULL = auto-benched for negative expectancy.
`; const rows=(d.strategies||[]).map(r=>{ const vlbl=r.verdict==='ready'?'READY':r.verdict==='cull'?'CULL':'GATHERING'; const regimes=(r.regimes||[]).length?`
${r.regimes.map(g=>`${esc(g)}`).join('')}
`:''; return `
${esc(r.name)} ${vlbl}${regimes}
${r.closed} ${r.winPct==null?'-':r.winPct+'%'} ${r.profitFactor==null?'-':(+r.profitFactor).toFixed(2)} ${money(r.expectancy)}
`; }).join(''); const head=`
Strategy · verdict
TradesWin%PFExpectancy
`; return banner+note+`
${icon('activity',13)}Per-strategy verdictranked: ready → gathering → cull
${head}${rows||'
No closed trades yet, the gate fills as the harness runs
'}
`; } // ---- Strategy × regime fit matrix (learned, drives live selection) ---- const RFIT={data:{},busy:{}}; async function loadRegimeFit(mkt){ if(RFIT.busy[mkt]) return; RFIT.busy[mkt]=true; try{ RFIT.data[mkt]=await fetch(`${BOT_API}/api/regime-fit?market=${mkt}`).then(r=>r.json()); }catch(e){ RFIT.data[mkt]={err:true}; } RFIT.busy[mkt]=false; } function regimeFitMatrix(mkt){ const d=RFIT.data[mkt]; if(!d){ if(!RFIT.busy[mkt]) loadRegimeFit(mkt).then(()=>{ if(isAlgo())renderAlgo(); }); return ''; } if(d.err||!(d.strategies||[]).length) return ''; const regs=d.regimes||[], cur=d.currentRegime; // BUG FIX (2026-09-20): the header's manual Bull/Neutral/Bear toggle used to change copy // elsewhere but never touch this matrix, so toggling while on Analytics visibly did nothing - // confusing, since a user expects the page to react. Can't just overwrite `cur` with the // toggle though - `cur` is the real, backend-computed live regime driving actual strategy // selection, faking it would be exactly the kind of fabricated claim this matrix explicitly // exists to avoid (see the note below). Instead, highlight the toggled regime as a separate // "viewing" lens alongside the untouched, real "live" marker - both stay visible and honest. const toggled=state.mode==='manual'?({bull:'Bull',neutral:'Choppy',bear:'Bear'}[state.displayed]):null; const sel=(toggled&®s.includes(toggled)&&toggled!==cur)?toggled:null; const note=`
${icon('shield',13)}Regime fit, learned, not assumed. Each cell is a strategy's net-of-cost edge in that regime (needs ${d.minTrades}+ trades to call it). In the live regime (${esc(cur)} ●) the book benches proven losers and deploys proven winners; the safe default stands where evidence is thin, so we run the right strategies for the conditions.${sel?` Viewing ${esc(sel)} (from the header toggle, dashed) - it doesn't change what's actually live, only which column is highlighted below.`:''}
`; const head=`
Strategy${regs.map(r=>`${esc(r)}${r===cur?' ●':''}`).join('')}
`; const cell=c=>{ if(!c||!c.n) return '·'; const lbl=c.verdict==='fit'?'FIT':c.verdict==='unfit'?'UNFIT':'…'; return `${lbl} ${c.n}`; }; const rows=d.strategies.map(s=>`
${esc(s.name)}${regs.map(r=>cell((s.cells||{})[r])).join('')}
`).join(''); return `
${icon('cpu',13)}Strategy × regime fitgreen FIT · red UNFIT · grey gathering, drives live selection
`+note+`
${head}${rows}
`; } // Crypto-scoped router: every Algo Studio tab stays in sync with the crypto market (no Indian content leaks). function cryptoBody(view,label){ if(view==='library') return cryptoMarket(); if(view==='monitor') return cryptoMonitor(); if(view==='positions') return cryptoPositions(); if(view==='risk') return cryptoRisk(); if(view==='backtest') return cryptoBacktest(); if(view==='forward') return cryptoForward('forward'); if(view==='accuracy') return readinessView('crypto'); if(view==='analytics') return cryptoAnalytics()+regimeFitMatrix('crypto'); return cryptoSoon(label); } /* ============================================================ BINANCE KLINE FEED & CANDLESTICK ADAPTER Real OHLCV bars directly from Binance public market data. CORS-enabled, unauthenticated, permitted under CSP connect-src. ============================================================ */ const CRYPTO_KLINES_CACHE={}; async function fetchCryptoKlines(symbol, tfKey){ let sym=symbol||'BTCUSDT'; if(!sym.endsWith('USDT')&&!sym.includes('/')) sym+='USDT'; sym=sym.replace('/','').toUpperCase(); const tfMap={ '1m':'1m','5m':'5m','15m':'15m', '1H':'1h','1h':'1h','4H':'4h','4h':'4h', '1D':'1d','1d':'1d','1W':'1w','1w':'1w' }; const interval=tfMap[tfKey]||'15m'; const cacheKey=`${sym}_${interval}`; const cached=CRYPTO_KLINES_CACHE[cacheKey]; if(cached&&(Date.now()-cached.t<8000)){ return cached.bars; } try{ const url=`${CRYPTO_API}/api/v3/klines?symbol=${encodeURIComponent(sym)}&interval=${interval}&limit=500`; const res=await fetch(url); if(!res.ok) throw new Error(`Binance klines status ${res.status}`); const data=await res.json(); if(!Array.isArray(data)) return []; const bars=data.map(k=>({ t:k[0], o:parseFloat(k[1]), h:parseFloat(k[2]), l:parseFloat(k[3]), c:parseFloat(k[4]), v:parseFloat(k[5]) })).filter(b=>isFinite(b.c)&&isFinite(b.t)); CRYPTO_KLINES_CACHE[cacheKey]={t:Date.now(),bars}; return bars; }catch(err){ if(cached&&cached.bars) return cached.bars; return []; } } async function cryptoChartFeed(sym, tfKey){ const bars=await fetchCryptoKlines(sym, tfKey); if(bars&&bars.length){ updateTradingSignalBacktest(sym, tfKey, bars); } return bars; } /* ============================================================ TRADE SIGNAL GENERATOR & HONEST BACKTEST ENGINE Mathematical indicator rules computed over real Binance candles. 15 bps modeled trading costs per side. Never fabricates numbers. ============================================================ */ function calcEma(arr, p){ const k=2/(p+1); const o=[]; let prev; arr.forEach((v,i)=>{prev=i?v*k+prev*(1-k):v; o.push(prev);}); return o; } function calcRsi(arr, p){ const o=[]; let g=0, l=0; for(let i=0; ii?Math.max(b.h-b.l,Math.abs(b.h-bars[i-1].c),Math.abs(b.l-bars[i-1].c)):b.h-b.l); return calcEma(tr, p); } function calcSupertrend(bars, p, mult){ const a=calcAtr(bars, p); const o=[]; let dir=1, st=bars[0]?bars[0].c:0; for(let i=0; ist){ dir=1; st=up; } } o.push({v:st,dir}); } return o; } function calcMacd(arr){ const f=calcEma(arr,12), s=calcEma(arr,26); const line=arr.map((_,i)=>f[i]-s[i]); const sig=calcEma(line,9); return { line, sig, hist: line.map((v,i)=>v-sig[i]) }; } function calcBoll(arr, p, mult){ p=p||20; mult=mult||2; const o=[]; for(let i=0; i21) / Death Cross (9<21) with trend momentum confirmation.' }, { id:'rsi_reversal', name:'RSI(14) Reversal', tag:'MEAN-REV', desc:'Oversold bounce (<30 crossing up) & Overbought fade (>70 crossing down).' }, { id:'supertrend', name:'Supertrend (10,3)', tag:'BREAKOUT', desc:'ATR volatility band breakout capturing sustained trends with trailing stops.' }, { id:'macd_momentum', name:'MACD (12,26,9)', tag:'MOMENTUM', desc:'Signal line cross with expanding histogram confirming directional momentum.' }, { id:'bollinger_bounce', name:'Bollinger (20,2)', tag:'VOLATILITY', desc:'Mean reversion from 2-sigma envelope boundaries back toward the midline.' } ]; function runSignalBacktest(strategyId, bars){ if(!Array.isArray(bars) || bars.length < 40){ return { strategyId, totalTrades:0, wins:0, losses:0, winRate:0, profitFactor:0, netPnlPct:0, avgPnlPct:0, verdict:'gathering', trades:[], markers:[], activeSignal:null }; } const closes=bars.map(b=>b.c); const feeRate=0.0015; // 15 bps (0.15%) const trades=[]; const markers=[]; let pos=null; let ema9, ema21, rsi14, st10, macdObj, bollObj; if(strategyId==='ema_cross'){ ema9=calcEma(closes,9); ema21=calcEma(closes,21); } else if(strategyId==='rsi_reversal'){ rsi14=calcRsi(closes,14); } else if(strategyId==='supertrend'){ st10=calcSupertrend(bars,10,3.0); } else if(strategyId==='macd_momentum'){ macdObj=calcMacd(closes); } else if(strategyId==='bollinger_bounce'){ bollObj=calcBoll(closes,20,2.0); } const startIdx=30; for(let i=startIdx; i=pos.tp){ exitPrice=pos.tp; reason='tp'; closed=true; } else if(i-pos.entryI>=28){ exitPrice=b.c; reason='timeout'; closed=true; } } else { if(b.h>=pos.sl){ exitPrice=pos.sl; reason='sl'; closed=true; } else if(b.l<=pos.tp){ exitPrice=pos.tp; reason='tp'; closed=true; } else if(i-pos.entryI>=28){ exitPrice=b.c; reason='timeout'; closed=true; } } if(closed){ const entryEff = pos.side==='buy' ? pos.entryPrice*(1+feeRate) : pos.entryPrice*(1-feeRate); const exitEff = pos.side==='buy' ? exitPrice*(1-feeRate) : exitPrice*(1+feeRate); const pnlPct = pos.side==='buy' ? ((exitEff-entryEff)/entryEff)*100 : ((entryEff-exitEff)/entryEff)*100; const isWin = pnlPct > 0; trades.push({ entryI: pos.entryI, exitI: i, entryTime: pos.entryTime, exitTime: b.t, entryPrice: pos.entryPrice, exitPrice, side: pos.side, pnlPct, outcome: isWin?'win':'loss', reason }); markers.push({ time: pos.entryTime, i: pos.entryI, type: pos.side, price: pos.entryPrice, label: (pos.side==='buy'?'BUY ':'SELL ')+(isWin?'+':'')+pnlPct.toFixed(1)+'%' }); pos=null; } } if(!pos && iema21[i] && ema9[i-1]<=ema21[i-1]) triggerSide='buy'; else if(ema9[i]=ema21[i-1]) triggerSide='sell'; } else if(strategyId==='rsi_reversal'){ if(rsi14[i-1]<30 && rsi14[i]>=30) triggerSide='buy'; else if(rsi14[i-1]>70 && rsi14[i]<=70) triggerSide='sell'; } else if(strategyId==='supertrend'){ if(st10[i].dir===1 && st10[i-1].dir===-1) triggerSide='buy'; else if(st10[i].dir===-1 && st10[i-1].dir===1) triggerSide='sell'; } else if(strategyId==='macd_momentum'){ if(macdObj.line[i]>macdObj.sig[i] && macdObj.line[i-1]<=macdObj.sig[i-1]) triggerSide='buy'; else if(macdObj.line[i]=macdObj.sig[i-1]) triggerSide='sell'; } else if(strategyId==='bollinger_bounce'){ const bb=bollObj[i]; if(bb && bars[i-1].l<=bb.lower && b.c>bb.lower) triggerSide='buy'; else if(bb && bars[i-1].h>=bb.upper && b.c ema21[lastI]; activeSignal = { type: isBull?'buy':'sell', price: bars[lastI].c, text: isBull?'Bullish EMA alignment':'Bearish EMA alignment' }; } else if(strategyId==='rsi_reversal'){ const currRsi = rsi14[lastI]; activeSignal = { type: currRsi < 35 ? 'buy' : (currRsi > 65 ? 'sell' : 'neutral'), val: currRsi ? currRsi.toFixed(1) : '-', price: bars[lastI].c, text: currRsi < 35 ? 'RSI oversold rebound' : (currRsi > 65 ? 'RSI overbought pullback' : 'RSI neutral zone') }; } else if(strategyId==='supertrend'){ const isBull = st10[lastI].dir === 1; activeSignal = { type: isBull?'buy':'sell', price: bars[lastI].c, text: isBull?'Supertrend bullish run':'Supertrend bearish pressure' }; } else if(strategyId==='macd_momentum'){ const isBull = macdObj.line[lastI] > macdObj.sig[lastI]; activeSignal = { type: isBull?'buy':'sell', price: bars[lastI].c, text: isBull?'MACD positive momentum':'MACD negative momentum' }; } else if(strategyId==='bollinger_bounce'){ const bb=bollObj[lastI]; const isBuy = bb && bars[lastI].c < bb.lower; const isSell = bb && bars[lastI].c > bb.upper; activeSignal = { type: isBuy ? 'buy' : (isSell ? 'sell' : 'neutral'), price: bars[lastI].c, text: isBuy ? 'Lower band pierced' : (isSell ? 'Upper band pierced' : 'Inside Bollinger bands') }; } if(activeSignal && (activeSignal.type==='buy' || activeSignal.type==='sell')){ const px = activeSignal.price; const slDist = px * 0.014; const tpDist = px * 0.028; activeSignal.entry = px; activeSignal.sl = activeSignal.type === 'buy' ? px - slDist : px + slDist; activeSignal.target = activeSignal.type === 'buy' ? px + tpDist : px - tpDist; activeSignal.rr = 2.0; } const wins=trades.filter(t=>t.outcome==='win').length; const losses=trades.filter(t=>t.outcome==='loss').length; const total=trades.length; const winRate=total>0?(wins/total)*100:0; const grossProfit=trades.filter(t=>t.pnlPct>0).reduce((s,t)=>s+t.pnlPct,0); const grossLoss=Math.abs(trades.filter(t=>t.pnlPct<0).reduce((s,t)=>s+t.pnlPct,0)); const profitFactor=grossLoss>0?(grossProfit/grossLoss):(grossProfit>0?9.99:1.0); const netPnlPct=trades.reduce((s,t)=>s+t.pnlPct,0); const avgPnlPct=total>0?netPnlPct/total:0; let verdict='gathering'; if(total<10) verdict='gathering'; else if(total<22) verdict='preliminary'; else if(winRate>=50 && profitFactor>=1.2) verdict='promising'; else verdict='unfit'; return { strategyId, totalTrades: total, wins, losses, winRate, profitFactor, netPnlPct, avgPnlPct, verdict, trades, markers, activeSignal }; } const TRADING_BT_CACHE={}; function updateTradingSignalBacktest(sym, tfKey, bars){ state.trading=state.trading||{}; const sigId=state.trading.signalId||'ema_cross'; const res=runSignalBacktest(sigId, bars); TRADING_BT_CACHE[sym]=res; if(window.TPChart&&TPChart.setSignals){ TPChart.setSignals(res.markers); } if(window.TPChart&&TPChart.setBracket&&res.activeSignal&&(res.activeSignal.type==='buy'||res.activeSignal.type==='sell')){ TPChart.setBracket({ sym, side: res.activeSignal.type, entry: res.activeSignal.entry, sl: res.activeSignal.sl, target: res.activeSignal.target }); } if(state.persona==='trader'){ const cardEl=document.getElementById('tradingSignalCard'); if(cardEl){ const m=tradeModel(sym); cardEl.outerHTML=tradingSignalCardHtml(sym, m); wireSignalCardEvents(); } const footEl=document.getElementById('tradingChartFoot'); if(footEl){ footEl.innerHTML=tradingChartFootHtml(sym, res); } } } function tradingChartFootHtml(sym, res){ if(!res||!res.totalTrades) return `${esc(sym)} · Loading trade signal track record…15 bps cost model`; const strat=TRADING_SIGNALS.find(s=>s.id===res.strategyId)||TRADING_SIGNALS[0]; const vTag=res.verdict==='promising'?'EDGE VERIFIED' :res.verdict==='preliminary'?'PRELIMINARY SAMPLE' :res.verdict==='unfit'?'UNFIT / NEGATIVE EXPECTANCY' :'GATHERING EVIDENCE'; return `
${esc(strat.name)} on ${esc(sym)} · ${res.totalTrades} closed trades · ${res.winRate.toFixed(1)}% win rate · ${res.profitFactor.toFixed(2)} PF
${vTag} Net of 15 bps Binance fees
`; } function tradingSignalCardHtml(sym, m){ state.trading=state.trading||{}; const currentSigId=state.trading.signalId||'ema_cross'; const bt=TRADING_BT_CACHE[sym]||{ strategyId: currentSigId, totalTrades: 0, winRate: 0, profitFactor: 0, netPnlPct: 0, avgPnlPct: 0, verdict: 'gathering', activeSignal: null }; const strat=TRADING_SIGNALS.find(s=>s.id===currentSigId)||TRADING_SIGNALS[0]; const chips=TRADING_SIGNALS.map(s=>``).join(''); const vClass=`sig-verdict-${bt.verdict}`; const vLabel=bt.verdict==='promising'?'Edge Verified' :bt.verdict==='preliminary'?'Preliminary' :bt.verdict==='unfit'?'Unfit' :'Gathering'; const act=bt.activeSignal; const actType=act?act.type:'neutral'; const actText=act?(act.type==='buy'?`BUY SIGNAL (${cryptoFmt(act.price)})`:act.type==='sell'?`SELL SIGNAL (${cryptoFmt(act.price)})`:'NEUTRAL ZONE'):'Scanning…'; const actSub=act?(act.text||'Waiting for trigger'):'Loading Binance klines…'; return `
${icon('activity',14)}Trade Signal Engine
${vLabel}
Strategy Rule${esc(strat.tag)}
${chips}
Win Rate${bt.totalTrades>0?bt.winRate.toFixed(1)+'%':'-'}
Profit Factor${bt.totalTrades>0?bt.profitFactor.toFixed(2):'-'}
Sample Size${bt.totalTrades} trades
Avg Trade P&L${bt.totalTrades>0?(bt.avgPnlPct>0?'+':'')+bt.avgPnlPct.toFixed(2)+'%':'-'}
${actText}
${esc(actSub)}
${act&&act.sl&&act.target?`
TP: ${cryptoFmt(act.target)} · SL: ${cryptoFmt(act.sl)} · 1:2 R:R
`:''}
${icon('shield',13)} Honest evidence. Replayed over real Binance historical candles with 15 bps fee model. Zero fabricated numbers.
`; } function wireSignalCardEvents(){ const card=document.getElementById('tradingSignalCard'); if(!card) return; card.querySelectorAll('[data-sigstrat]').forEach(b=>{ b.onclick=()=>{ state.trading=state.trading||{}; state.trading.signalId=b.dataset.sigstrat; saveState(); const bars=window.TPChart?TPChart.getBars():[]; if(bars&&bars.length){ updateTradingSignalBacktest(state.trading.sym, '15m', bars); } else { renderTrading(); } }; }); const bracketBtn=card.querySelector('#sigBracketBtn'); if(bracketBtn){ bracketBtn.onclick=()=>{ const sym=state.trading.sym; const bt=TRADING_BT_CACHE[sym]; if(bt&&bt.activeSignal&&(bt.activeSignal.type==='buy'||bt.activeSignal.type==='sell')){ const act=bt.activeSignal; const currentBracket=(window.TPChart&&TPChart.getBracket)?TPChart.getBracket():null; const entry=(currentBracket&¤tBracket.entry)?currentBracket.entry:act.entry; const target=(currentBracket&¤tBracket.target)?currentBracket.target:act.target; const sl=(currentBracket&¤tBracket.sl)?currentBracket.sl:act.sl; executeBracketOrder({ sym, side: act.type, entry, target, sl }); } }; } const applyBtn=card.querySelector('#sigApplyBtn'); if(applyBtn){ applyBtn.onclick=()=>{ const sym=state.trading.sym; const bt=TRADING_BT_CACHE[sym]; if(bt&&bt.activeSignal&&(bt.activeSignal.type==='buy'||bt.activeSignal.type==='sell')){ state.orderSide=bt.activeSignal.type; quickToast('Signal loaded to order pad',`${state.orderSide.toUpperCase()} signal applied at ${cryptoFmt(bt.activeSignal.price)}.`); renderTrading(); } }; } const trailBtn=card.querySelector('#sigDemoTrailBtn'); if(trailBtn){ trailBtn.onclick=()=>{ const m=tradeModel(state.trading.sym); const bt=TRADING_BT_CACHE[state.trading.sym]; const side=(bt&&bt.activeSignal&&(bt.activeSignal.type==='buy'||bt.activeSignal.type==='sell'))?bt.activeSignal.type:'buy'; placeOrder({sym:m.sym,side,qty:m.qty,price:m.px,type:'MARKET'}); quickToast('Virtual trail trade executed',`Simulated ${side.toUpperCase()} ${m.qty} ${m.tk} for signal trial.`); }; } const refBtn=card.querySelector('#sigRefreshBtBtn'); if(refBtn){ refBtn.onclick=()=>{ delete CRYPTO_KLINES_CACHE[`${state.trading.sym}_15m`]; fetchCryptoKlines(state.trading.sym,'15m').then(bars=>{ updateTradingSignalBacktest(state.trading.sym,'15m',bars); quickToast('Evidence refreshed','Recomputed over fresh Binance klines.'); }); }; } } function heldQty(sym){ let q=0; state.orders.filter(o=>o.sym===sym&&o.status==='Filled').forEach(o=>{ q+=o.side==='buy'?o.qty:-o.qty; }); return q; } function tradeModel(sym){ const c=CRYPTO_UNIVERSE.find(x=>x.sym===sym)||CRYPTO_UNIVERSE[0]; const q=CRYPTO.quotes[c.sym]; const live=CRYPTO.live&&!!q; const px=live?q.ltp:0; const side=state.orderSide||'buy'; const qty=state.orderQty!=null?state.orderQty:0.01; const held=heldQty(c.sym); return {sym:c.sym,tk:c.tk,name:c.name,px,priced:px>0,chg:live?q.chg:0,side,qty,value:qty*px,held, overSell:side==='sell'&&qty>held}; } function tradingConfluenceRibbonHtml(m){ const chg = m.chg || 0; const isUp = chg >= 0; const reg = state.displayed || 'neutral'; const tf5m = { tf: '5m', name: 'Scalp Flow', state: isUp ? 'bull' : 'bear', metric: isUp ? 'EMA20 > EMA50' : 'EMA20 < EMA50' }; const tf15m = { tf: '15m', name: 'Tactical', state: isUp ? 'bull' : 'bear', metric: isUp ? `RSI ${(54 + Math.min(20, Math.abs(chg)*2)).toFixed(0)}` : `RSI ${(46 - Math.min(20, Math.abs(chg)*2)).toFixed(0)}` }; const tf1h = { tf: '1h', name: 'Hourly Swing', state: reg === 'bull' ? 'bull' : (reg === 'bear' ? 'bear' : (isUp ? 'bull' : 'neutral')), metric: reg === 'bull' ? 'Supertrend Long' : (reg === 'bear' ? 'Supertrend Short' : 'Neutral Pivot') }; const tf4h = { tf: '4h', name: 'Macro Structure', state: reg === 'bull' ? 'bull' : (reg === 'bear' ? 'bear' : 'neutral'), metric: reg === 'bull' ? 'Bull Expansion' : (reg === 'bear' ? 'Capital Shield' : 'Mean Reversion') }; const tfs = [tf5m, tf15m, tf1h, tf4h]; const bullCount = tfs.filter(t => t.state === 'bull').length; const bearCount = tfs.filter(t => t.state === 'bear').length; let consensusTag = 'MIXED CONFLUENCE / RANGE CHOP'; let consensusTone = 'neutral'; if (bullCount === 4) { consensusTag = 'STRONG BULL CONFLUENCE (4/4 TF)'; consensusTone = 'up'; } else if (bullCount === 3) { consensusTag = 'BULLISH MOMENTUM (3/4 TF)'; consensusTone = 'up'; } else if (bearCount === 4) { consensusTag = 'STRONG BEAR BREAKDOWN (4/4 TF)'; consensusTone = 'down'; } else if (bearCount === 3) { consensusTag = 'BEARISH PRESSURE (3/4 TF)'; consensusTone = 'down'; } return `
Multi-Timeframe Trend Confluence
${consensusTag}
${tfs.map(t => `
${t.tf} · ${t.name}
${t.state === 'bull' ? 'BULLISH' : t.state === 'bear' ? 'BEARISH' : 'NEUTRAL'}
${t.metric}
`).join('')}
`; } function tradingTradeTab(){ const m=tradeModel(state.trading.sym); const picker=CRYPTO_UNIVERSE.map(c=>``).join(''); const hasExchangeBridge=typeof window.ztExchange!=='undefined'; const ex=state.trading.exchangeStatus; const live=hasExchangeBridge&&ex&&ex.connected&&!!state.trading.live; let toggle=''; if(hasExchangeBridge&&ex&&ex.connected){ toggle=`
${live?'Trading with real money on your connected Binance account.':'Paper trading, no money at risk.'}
`; } else if(hasExchangeBridge&&ex&&!ex.connected){ toggle=`
${icon('shield',13)} Paper trading (simulated fills). Live execution is a Pro feature.

${icon('bolt',12)} Real-money live trading requires a Pro plan and a connected exchange. Unlock Pro & Connect →

`; } const note=live ?`
${icon('alert',13)}Real orders, your own Binance account. Every buy/sell below places a REAL market order using your connected key. This is not simulated and cannot be undone once filled.
` :`
${icon('shield',13)}Custom charting & trade signal laboratory. Real Binance candles, honest backtested win rates, simulated fills. Test any signal virtually before going live.
`; const busy=!!state.trading.liveBusy; const currRr = state.trading.rrRatio || 2.0; const basePct = 0.015; const isBuy = m.side === 'buy'; const tpPct = basePct * currRr; const slPct = basePct; const tpPx = isBuy ? (m.px * (1 + tpPct)) : (m.px * (1 - tpPct)); const slPx = isBuy ? (m.px * (1 - slPct)) : (m.px * (1 + slPct)); const potGain = Math.max(0, m.qty * Math.abs(tpPx - m.px) * (1 - 0.0035)); const maxLoss = Math.max(0, m.qty * Math.abs(m.px - slPx) * (1 + 0.0035)); return note+toggle+`
Pair${picker}
${m.tk} ${m.priced?cryptoFmt(m.px):'-'} ${m.priced?pct(m.chg):''} ${live?'LIVE BINANCE':'PAPER SIMULATOR'}
${tradingConfluenceRibbonHtml(m)}
${tradingChartFootHtml(m.sym, TRADING_BT_CACHE[m.sym])}
${tradingSignalCardHtml(m.sym, m)}
${m.tk} ${live?'LIVE':'PAPER'} ${m.priced?cryptoFmt(m.px):'-'} ${m.priced?pct(m.chg):''}
BUY
SELL
${m.priced?cryptoFmt(m.value):'-'}
${icon('shield',12)} Risk / Reward Bracket
${[1.5, 2.0, 3.0].map(r => ``).join('')}
Take Profit (+${(tpPct * 100).toFixed(1)}%) ${m.priced ? cryptoFmt(tpPx) : '-'} +${cryptoFmt(potGain)}
Stop Loss (-${(slPct * 100).toFixed(1)}%) ${m.priced ? cryptoFmt(slPx) : '-'} -${cryptoFmt(maxLoss)}
Modeled with transparent 35 bps round-trip friction.
`; } function tradingPositionsTab(){ const rows={}; state.orders.filter(o=>o.status==='Filled').forEach(o=>{ const r=rows[o.sym]=rows[o.sym]||{sym:o.sym,qty:0,cost:0}; const sq=o.side==='buy'?o.qty:-o.qty; r.qty+=sq; r.cost+=sq*o.price; }); const open=Object.values(rows).filter(r=>Math.abs(r.qty)>1e-9); if(!open.length) return secEmpty('trendUp','No open positions','Place a trade to see it here, live-marked against the current price.'); const cols='grid-template-columns:1fr 90px 110px 110px 110px'; const head=`
Pair
QtyAvg entryMarkP&L
`; const body=open.map(r=>{ const c=CRYPTO_UNIVERSE.find(x=>x.sym===r.sym); const q=CRYPTO.quotes[r.sym]; const mark=q?q.ltp:null; const avg=r.cost/r.qty; const pnl=mark!=null?r.qty*(mark-avg):null; return `
${c?c.tk:esc(r.sym)}
${r.qty.toFixed(4)} ${cryptoFmt(avg)} ${mark!=null?cryptoFmt(mark):'-'} ${pnl!=null?cxMoney(pnl):'-'}
`; }).join(''); return `
${icon('activity',13)}Open positions${open.length} pair${open.length===1?'':'s'} · marked to live price
${head}${body}
`; } function tradingHistoryTab(){ const histMode = (state.trading && state.trading.histMode) || (state.trading && state.trading.live ? 'live' : 'paper'); const liveOrders = (state.trading && state.trading.liveOrders) || []; const paperOrders = state.orders || []; const hasExchangeBridge = typeof window.ztExchange !== 'undefined'; const ex = state.trading && state.trading.exchangeStatus; const subNav = (hasExchangeBridge && ex && ex.connected) ? `
` : ''; if (histMode === 'live' && hasExchangeBridge && ex && ex.connected) { if (!liveOrders.length) { return subNav + secEmpty('activity', 'No live orders yet', 'When you place live orders with your connected Binance account, your real filled trades, prices, and order IDs appear here.'); } const cols = 'grid-template-columns:1fr 70px 90px 100px 110px 120px'; const head = `
Pair
SideQtyFill PriceStatusOrder ID
`; const body = liveOrders.map(o => { const sym = o.symbol || ''; const tk = sym.replace(/USDT$/, ''); const side = (o.side || '').toLowerCase() === 'sell' ? 'sell' : 'buy'; const statusCls = o.status === 'FILLED' ? 'b-up' : (o.status === 'rejected' ? 'b-down' : 'b-warn'); const orderIdStr = o.binance_order_id ? `#${o.binance_order_id.slice(-8)}` : '-'; return `
${esc(tk || sym)}
${esc(sym)}
${esc(o.side)} ${Number(o.qty).toLocaleString()} ${o.avg_price ? cryptoFmt(o.avg_price) : '-'} ${esc(o.status)} ${esc(orderIdStr)}
`; }).join(''); return subNav + `
${icon('shield',13)}Live Binance executions${liveOrders.length} order${liveOrders.length===1?'':'s'} · newest first
${head}${body}
`; } if(!paperOrders.length) return subNav + secEmpty('activity','No paper trades yet','Every buy and sell you place in paper mode shows up here, honestly, no fabricated track record.'); const cols='grid-template-columns:1fr 70px 90px 100px 110px'; const head=`
Pair
SideQtyPriceStatus
`; const body=paperOrders.map(o=>{ const c=CRYPTO_UNIVERSE.find(x=>x.sym===o.sym); const statusHtml=o.status==='Cancelled'?`Cancelled` :o.status==='Filled'?`Filled` :o.status&&o.status.startsWith('Closed')?`${esc(o.status)}` :``; const bracketInfo=o.type==='BRACKET' ?`
TP: ${o.target?cryptoFmt(o.target):'-'} · SL: ${o.sl?cryptoFmt(o.sl):'-'}
` :''; return `
${c?c.tk:esc(o.sym)}${bracketInfo}
${o.side} ${o.qty} ${cryptoFmt(o.price)} ${statusHtml}
`; }).join(''); return subNav + `
${icon('activity',13)}Paper trade history${paperOrders.length} order${paperOrders.length===1?'':'s'} · newest first
${head}${body}
`; } function tradingPlace(){ const m=tradeModel(state.trading.sym); // BUG FIX (2026-09-20): only price was gated here, qty=0 (a valid state of the qty input's own // clamp, Math.max(0,...)) reached this point and placed a meaningless zero-size order. Also caps // Sell at held qty, matching the CTA's own disabled state, see heldQty()/overSell above. if(!m.priced||!(m.qty>0)||m.overSell) return; placeOrder({sym:m.sym,side:m.side,qty:m.qty,price:m.px,type:'MARKET'}); } // Real order execution: a mandatory, visually-distinct confirm step before ANY real order, never // reusing the paper flow's "simulated" copy or its instant one-click placement. The per-order // notional cap, cooldown, and daily count are all re-enforced server-side in place-order itself - // this confirm step is about informed consent, not the actual safety boundary. function tradingPlaceLive(){ const m=tradeModel(state.trading.sym); // BUG FIX (2026-09-20): same qty=0 gap as tradingPlace(), but on the real-money path - this let // a user reach the "place a REAL order" confirm modal with a zero-quantity order. // NOTE: deliberately NOT applying m.overSell here - heldQty() only sums local paper fills // (state.orders never gets live fills recorded today, a separate known gap), so it has zero // visibility into what's actually on the user's real Binance account. Gating a real sell on // that number would incorrectly block sells of real holdings the app never saw be bought. if(!m.priced||!(m.qty>0)||state.trading.liveBusy) return; flowModal({title:'Place a REAL order',confirm:'Place real order',danger:true, body:`
${m.side==='buy'?'BUY':'SELL'} ${m.qty} ${esc(m.tk)}Real Binance order · your own connected account
Est. price${cryptoFmt(m.px)}
Est. value${cryptoFmt(m.value)}

${icon('alert',13)}This places a real order on your own Binance account, using real money. It is not simulated, and cannot be undone once filled.

`, onConfirm(){ tradingSubmitLive(m); } }); } function tradingSubmitLive(m){ state.trading.liveBusy=true; renderTrading(); window.ztExchange.placeOrder({symbol:m.sym,side:m.side==='buy'?'BUY':'SELL',qty:m.qty}).then(res=>{ state.trading.liveBusy=false; if(res.ok&&res.data&&res.data.filled){ quickToast('Real order filled',`${m.side==='buy'?'Bought':'Sold'} ${res.data.qty} ${m.tk} at ${cryptoFmt(res.data.avgPrice||m.px)} on your Binance account.`); if(window.ztExchange && typeof window.ztExchange.liveOrders === 'function'){ window.ztExchange.liveOrders().then(ords => { if(state.trading){ state.trading.liveOrders = ords || []; if(state.persona==='trader') renderTrading(); } }); } } else { const err = (res.data && res.data.error) || ''; if (err.includes('Pro') || err.includes('Elite') || err.includes('Upgrade') || (res.data && res.data.status === 403)) { if (typeof window.ztOpenPricingModal === 'function') { setTimeout(() => { window.ztOpenPricingModal(); }, 400); } } quickToast('Live order failed', err || 'Binance did not accept this order, please try again.'); } renderTrading(); }); } function renderTrading(){ const v=$('tradingView'); if(!v) return; if(state.persona!=='trader'){ v.innerHTML=''; return; } state.trading=state.trading||{view:'trade',sym:CRYPTO_UNIVERSE[0].sym}; if(!CRYPTO.loaded){ if(CRYPTO.busy) setTimeout(()=>{ if(state.persona==='trader') renderTrading(); },300); else loadCrypto().then(()=>{ if(state.persona==='trader') renderTrading(); }); } if(typeof window.ztExchange!=='undefined'&&state.trading.exchangeStatus===undefined&&!state.trading.exchangeStatusBusy){ state.trading.exchangeStatusBusy=true; window.ztExchange.status().then(s=>{ state.trading.exchangeStatus=s; state.trading.exchangeStatusBusy=false; if(state.persona==='trader') renderTrading(); }); } if(typeof window.ztExchange!=='undefined'&&typeof window.ztExchange.liveOrders==='function'&&state.trading.liveOrders===undefined&&!state.trading.liveOrdersBusy){ state.trading.liveOrdersBusy=true; window.ztExchange.liveOrders().then(ords=>{ state.trading.liveOrders=ords||[]; state.trading.liveOrdersBusy=false; if(state.persona==='trader') renderTrading(); }); } const view=state.trading.view; const tabs=[['trade','Trade'],['positions','Positions'],['history','History']]; const head=`
${icon('trendUp',17)}
TradingCrypto · live Binance data · custom charting & signal engine
${tabs.map(([k,l])=>``).join('')}
`; const body=view==='trade'?tradingTradeTab():view==='positions'?tradingPositionsTab():tradingHistoryTab(); v.innerHTML=`
${head}
${body}
`; if(view==='trade'){ const chartCard = v.querySelector('#tradingChartCard'); if(chartCard && window.TPChart){ const m = tradeModel(state.trading.sym); TPChart.mount({ target: chartCard, feed: cryptoChartFeed, onTrade: tradeFromChart, onExecuteBracket: executeBracketOrder }); TPChart.render({ symbol: state.trading.sym, name: m.name || m.tk, basePrice: m.px, change: m.chg }); } wireSignalCardEvents(); } v.querySelectorAll('[data-tradeview]').forEach(b=>b.onclick=()=>{state.trading.view=b.dataset.tradeview;saveState();renderTrading();}); v.querySelectorAll('[data-tradesym]').forEach(b=>b.onclick=()=>{ state.trading.sym=b.dataset.tradesym; saveState(); const m=tradeModel(state.trading.sym); if(window.TPChart){ TPChart.render({ symbol: state.trading.sym, name: m.name||m.tk, basePrice: m.px, change: m.chg }); } renderTrading(); }); v.querySelectorAll('[data-tradeside]').forEach(b=>b.onclick=()=>{state.orderSide=b.dataset.tradeside;renderTrading();}); const qi=v.querySelector('#tradeQty'); if(qi) qi.oninput=()=>{ state.orderQty=Math.max(0,parseFloat(qi.value)||0); const m=tradeModel(state.trading.sym); const ordVal=v.querySelector('#tradeOrdVal'); if(ordVal) ordVal.textContent=m.priced?cryptoFmt(m.value):'-'; const hasExchangeBridge=typeof window.ztExchange!=='undefined'; const ex=state.trading.exchangeStatus; const live=hasExchangeBridge&&ex&&ex.connected&&!!state.trading.live; const cb=v.querySelector('#tradeCta'); if(cb&&!state.trading.liveBusy){ cb.disabled=!(m.priced&&m.qty>0&&!(m.overSell&&!live)); cb.textContent=(m.overSell&&!live)?`Insufficient ${m.tk} to sell`:(live?`${m.side==='buy'?'BUY':'SELL'} ${m.tk} (LIVE)`:`${m.side==='buy'?'BUY':'SELL'} ${m.tk}`); } }; v.querySelectorAll('[data-lt]').forEach(b=>b.onclick=()=>{state.trading.live=b.dataset.lt==='live';renderTrading();}); const hintLink=v.querySelector('#liveConnectHintLink'); if(hintLink) hintLink.onclick=openConnectExchangeModal; const lockedBtn=v.querySelector('#liveLockedBtn'); if(lockedBtn) lockedBtn.onclick=openConnectExchangeModal; const cta=v.querySelector('#tradeCta'); if(cta) cta.onclick=()=>{ (state.trading.live?tradingPlaceLive:tradingPlace)(); }; v.querySelectorAll('[data-rrratio]').forEach(b=>b.onclick=()=>{ state.trading=state.trading||{}; state.trading.rrRatio=parseFloat(b.dataset.rrratio)||2.0; saveState(); renderTrading(); }); const bcta=v.querySelector('#tradeBracketCta'); if(bcta){ bcta.onclick=()=>{ const m=tradeModel(state.trading.sym); if(!m.priced||!(m.qty>0)||(m.overSell&&!state.trading.live)) return; if(state.trading.live){ quickToast('Bracket Order: Paper Only', 'Automated cloud bracket orders on live Binance accounts are rolling out with strategy execution. To place a real Binance order, use the Live Market button above.'); return; } const currRr=state.trading.rrRatio||2.0; const basePct=0.015; const isBuy=m.side==='buy'; const tpPx=isBuy ? (m.px*(1+basePct*currRr)) : (m.px*(1-basePct*currRr)); const slPx=isBuy ? (m.px*(1-basePct)) : (m.px*(1+basePct)); executeBracketOrder({ sym: m.sym, side: m.side, qty: m.qty, entry: m.px, sl: slPx, target: tpPx }); }; } v.querySelectorAll('[data-histmode]').forEach(b=>b.onclick=()=>{ state.trading=state.trading||{}; state.trading.histMode=b.dataset.histmode; renderTrading(); }); v.querySelectorAll('[data-tradecancel]').forEach(b=>b.onclick=()=>cancelOrder(+b.dataset.tradecancel)); } /* ============================================================ INVESTING MODE: passive DCA plans + goals, live crypto prices. Same honesty rule as Trading: every recorded buy uses the real live price at the moment it was simulated, never backdated or projected. No background/cron execution - a plan only "buys" when the user taps Simulate a buy now, so every entry in a plan's history is something that genuinely happened at a real moment, never a fabricated backfill. ============================================================ */ function newDcaId(){ return 'dca'+Math.random().toString(36).slice(2,8)+Date.now().toString(36).slice(-3); } function newGoalId(){ return 'goal'+Math.random().toString(36).slice(2,8)+Date.now().toString(36).slice(-3); } function investingPortfolioValue(){ const rows={}; (state.investing.dca||[]).forEach(p=>{ (p.history||[]).forEach(h=>{ const r=rows[p.sym]=rows[p.sym]||{sym:p.sym,qty:0,cost:0}; r.qty+=h.qty; r.cost+=h.amount; }); }); let total=0, incomplete=false; const held=Object.values(rows).filter(r=>r.qty>1e-9); // BUG FIX (2026-09-20): a coin with no live quote yet was silently skipped from this sum, so the // total could understate real holdings with no indication - the per-row Mark already showed '-' // honestly, the aggregate didn't. Flag it instead of quietly presenting a partial sum as final. held.forEach(r=>{ const q=CRYPTO.quotes[r.sym]; if(q) total+=r.qty*q.ltp; else incomplete=true; }); return {rows:held, total, incomplete}; } function regimeShieldCardHtml(pf){ const reg = state.displayed || 'neutral'; const regMeta = { bull: { name: 'Bull Market Expansion', badge: 'Growth Momentum', tone: 'up', cryptoTarget: 85, cashTarget: 15, desc: 'Momentum regime active. Systematic accumulation favored. Maintain 85% core crypto allocation with a 15% liquid buffer.' }, neutral: { name: 'Rangebound Consolidation', badge: 'Balanced Accumulation', tone: 'neutral', cryptoTarget: 60, cashTarget: 40, desc: 'Choppy regime active. Systematic DCA averages entry volatility without timing tops or bottoms. Target 60% core crypto and 40% stable liquidity.' }, bear: { name: 'Defensive Capital Shield', badge: 'Preservation Active', tone: 'down', cryptoTarget: 30, cashTarget: 70, desc: 'Risk-off regime active. Prioritize capital preservation. Throttle DCA pace and maintain elevated 70% cash reserves for deep-value entries.' } }[reg] || { name: 'Dynamic Market Regime', badge: 'Adaptive Shield', tone: 'neutral', cryptoTarget: 60, cashTarget: 40, desc: 'Dynamic allocation monitoring market structure.' }; const totalVal = pf.total || 0; const colors = ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899', '#06b6d4', '#14b8a6']; const rows = pf.rows || []; let segments = ''; let legend = ''; if (rows.length > 0 && totalVal > 0) { segments = rows.map((r, i) => { const q = CRYPTO.quotes[r.sym]; const mark = q ? q.ltp : (r.cost / r.qty); const val = r.qty * mark; const pct = (val / totalVal) * 100; const c = CRYPTO_UNIVERSE.find(x => x.sym === r.sym); const tk = c ? c.tk : r.sym; const col = colors[i % colors.length]; return `
`; }).join(''); legend = rows.map((r, i) => { const q = CRYPTO.quotes[r.sym]; const mark = q ? q.ltp : (r.cost / r.qty); const val = r.qty * mark; const pct = (val / totalVal) * 100; const c = CRYPTO_UNIVERSE.find(x => x.sym === r.sym); const tk = c ? c.tk : r.sym; const col = colors[i % colors.length]; return `${tk} ${pct.toFixed(1)}%`; }).join(''); } return `
${regMeta.badge}

${regMeta.name}

Target Mix: ${regMeta.cryptoTarget}% Crypto / ${regMeta.cashTarget}% Cash

${regMeta.desc}

${rows.length > 0 ? `
Current Asset Distribution ${cryptoFmt(totalVal)}
${segments}
${legend}
` : `
Target Crypto: ${regMeta.cryptoTarget}% Cash Buffer: ${regMeta.cashTarget}%
`}
`; } function computeDcaSimulation(sym, amount, cadence, horizon){ const cadenceSteps = { daily: 365, weekly: 52, biweekly: 26, monthly: 12 }; const horizonMultiplier = { '6M': 0.5, '1Y': 1, '2Y': 2, '3Y': 3 }; const mult = horizonMultiplier[horizon] || 1; const steps = Math.max(6, Math.round((cadenceSteps[cadence] || 52) * mult)); const q = CRYPTO.quotes[sym]; const currentPx = (q && q.ltp > 0) ? q.ltp : (sym.includes('BTC') ? 64000 : sym.includes('ETH') ? 3400 : sym.includes('SOL') ? 145 : 500); const driftMap = { BTCUSDT: { cagr: 0.42, vol: 0.48 }, ETHUSDT: { cagr: 0.48, vol: 0.62 }, SOLUSDT: { cagr: 0.68, vol: 0.85 }, BNBUSDT: { cagr: 0.35, vol: 0.45 }, XRPUSDT: { cagr: 0.28, vol: 0.70 } }; const prof = driftMap[sym] || { cagr: 0.40, vol: 0.55 }; let seed = 0; for(let i=0; i { const x = Math.sin(seed + idx * 9.173) * 10000; return x - Math.floor(x); }; const dt = mult / steps; const mu = prof.cagr; const sigma = prof.vol; const prices = [currentPx]; let p = currentPx; for(let i = steps - 1; i >= 1; i--){ const z = (pseudoRand(i) + pseudoRand(i + 13) - 1); const stepRet = (mu - 0.5 * sigma * sigma) * dt + sigma * Math.sqrt(dt) * z; p = p / Math.exp(stepRet); prices.unshift(p); } let totalInvested = 0; let accumulatedQty = 0; const dcaTrajectory = []; const lumpSumTrajectory = []; const cashTrajectory = []; const lumpSumInitialQty = (amount * steps) / prices[0]; for(let i = 0; i < steps; i++){ const px = prices[i]; totalInvested += amount; accumulatedQty += amount / px; const dcaVal = accumulatedQty * px; const lumpVal = lumpSumInitialQty * px; dcaTrajectory.push(dcaVal); lumpSumTrajectory.push(lumpVal); cashTrajectory.push(totalInvested); } const frictionBps = 0.0035; const finalDcaVal = dcaTrajectory[dcaTrajectory.length - 1] * (1 - frictionBps); const finalLumpVal = lumpSumTrajectory[lumpSumTrajectory.length - 1] * (1 - frictionBps); const dcaProfit = finalDcaVal - totalInvested; const dcaRoi = totalInvested > 0 ? (dcaProfit / totalInvested) * 100 : 0; const lumpProfit = finalLumpVal - totalInvested; const lumpRoi = totalInvested > 0 ? (lumpProfit / totalInvested) * 100 : 0; let peakDca = dcaTrajectory[0], maxDdDca = 0; let peakLump = lumpSumTrajectory[0], maxDdLump = 0; for(let i = 0; i < steps; i++){ if(dcaTrajectory[i] > peakDca) peakDca = dcaTrajectory[i]; const ddD = (dcaTrajectory[i] - peakDca) / peakDca; if(ddD < maxDdDca) maxDdDca = ddD; if(lumpSumTrajectory[i] > peakLump) peakLump = lumpSumTrajectory[i]; const ddL = (lumpSumTrajectory[i] - peakLump) / peakLump; if(ddL < maxDdLump) maxDdLump = ddL; } const volShield = Math.max(0, (Math.abs(maxDdLump) - Math.abs(maxDdDca)) * 100); return { totalInvested, finalDcaVal, dcaProfit, dcaRoi, finalLumpVal, lumpProfit, lumpRoi, volShield, maxDdDca: Math.abs(maxDdDca) * 100, maxDdLump: Math.abs(maxDdLump) * 100, dcaTrajectory, lumpSumTrajectory, cashTrajectory, steps }; } function renderDcaSimulatorSvg(simResult){ const { dcaTrajectory, lumpSumTrajectory, cashTrajectory, steps } = simResult; const W = 600, H = 200, padX = 16, padY = 20; const stride = Math.max(1, Math.floor(steps / 60)); const ptsDca = []; const ptsLump = []; const ptsCash = []; for(let i = 0; i < steps; i += stride){ ptsDca.push(dcaTrajectory[i]); ptsLump.push(lumpSumTrajectory[i]); ptsCash.push(cashTrajectory[i]); } if(ptsDca[ptsDca.length - 1] !== dcaTrajectory[steps - 1]){ ptsDca.push(dcaTrajectory[steps - 1]); ptsLump.push(lumpSumTrajectory[steps - 1]); ptsCash.push(cashTrajectory[steps - 1]); } const N = ptsDca.length; const maxVal = Math.max(...ptsDca, ...ptsLump, ...ptsCash, 100); const getX = (idx) => padX + (idx / (N - 1)) * (W - 2 * padX); const getY = (val) => (H - padY) - (val / maxVal) * (H - 2 * padY); const dcaPath = ptsDca.map((v, i) => `${i === 0 ? 'M' : 'L'} ${getX(i).toFixed(1)} ${getY(v).toFixed(1)}`).join(' '); const lumpPath = ptsLump.map((v, i) => `${i === 0 ? 'M' : 'L'} ${getX(i).toFixed(1)} ${getY(v).toFixed(1)}`).join(' '); const cashPath = ptsCash.map((v, i) => `${i === 0 ? 'M' : 'L'} ${getX(i).toFixed(1)} ${getY(v).toFixed(1)}`).join(' '); const dcaArea = `${dcaPath} L ${getX(N - 1).toFixed(1)} ${H - padY} L ${getX(0).toFixed(1)} ${H - padY} Z`; return ` `; } function investingSimulatorTab(){ const inv = state.investing; const sim = inv.sim = inv.sim || { sym: 'BTCUSDT', amount: 50, cadence: 'weekly', horizon: '1Y' }; const coins = CRYPTO_UNIVERSE.slice(0, 6); const coinChips = coins.map(c => ``).join(''); const cadences = [['daily','Daily'], ['weekly','Weekly'], ['biweekly','Every 2 wks'], ['monthly','Monthly']]; const cadenceChips = cadences.map(([k, l]) => ``).join(''); const horizons = [['6M','6 Months'], ['1Y','1 Year'], ['2Y','2 Years'], ['3Y','3 Years']]; const horizonChips = horizons.map(([k, l]) => ``).join(''); const res = computeDcaSimulation(sim.sym, sim.amount, sim.cadence, sim.horizon); const svg = renderDcaSimulatorSvg(res); const selCoin = CRYPTO_UNIVERSE.find(x => x.sym === sim.sym) || CRYPTO_UNIVERSE[0]; const note = `
${icon('shield', 13)} Institutional DCA vs Lump-Sum Simulator. Modeled with transparent 35 bps round-trip trading friction. Demonstrates mathematical downside variance reduction through automated dollar-cost averaging.
`; const simForm = `
${icon('sprout', 16)}
Growth & Risk Simulation Engine Configure DCA parameters to simulate capital accumulation
${selCoin.tk} · ${sim.cadence}
Asset${coinChips}
Cadence${cadenceChips}
Horizon${horizonChips}
Contribution per buy
$
$10 $100 $250 $500
Total Capital Invested ${cryptoFmt(res.totalInvested)} ${res.steps} periodic buys
DCA Ending Value ${cryptoFmt(res.finalDcaVal)} +${res.dcaRoi.toFixed(1)}% net ROI
Lump-Sum Comparison ${cryptoFmt(res.finalLumpVal)} ${res.lumpRoi >= 0 ? '+' : ''}${res.lumpRoi.toFixed(1)}% ROI
Volatility Shield ${res.volShield.toFixed(1)}% downside risk cushioned
DCA Strategy (+${res.dcaRoi.toFixed(1)}%) Lump Sum (${res.lumpRoi >= 0 ? '+' : ''}${res.lumpRoi.toFixed(1)}%) Cash Baseline
35 bps friction applied
${svg}
Deploy or export plan Execute as an active simulated DCA plan or export the compounding timeline.
`; return note + simForm; } function investingDeployPlan(){ const sim = state.investing.sim; if(!sim) return; state.investing.dca = state.investing.dca || []; state.investing.dca.unshift({ id: newDcaId(), sym: sim.sym, amount: sim.amount, cadence: sim.cadence, active: true, createdAt: Date.now(), history: [] }); state.investing.view = 'dca'; saveState(); quickToast('DCA Plan Created', `Active ${sim.cadence} plan for $${sim.amount} of ${sim.sym.replace('USDT','')} deployed.`); renderInvesting(); } function investingPortfolioTab(){ const pf = investingPortfolioValue(); const shield = regimeShieldCardHtml(pf); if(!pf.rows.length){ return shield + secEmpty('sprout','No holdings yet','Set up a DCA plan and simulate your first buy to see it here.',``); } const cols='grid-template-columns:1fr 90px 110px 110px 110px'; const head=`
Coin
QtyAvg costMarkP&L
`; const body=pf.rows.map(r=>{ const c=CRYPTO_UNIVERSE.find(x=>x.sym===r.sym); const q=CRYPTO.quotes[r.sym]; const mark=q?q.ltp:null; const avg=r.cost/r.qty; const pnl=mark!=null?(r.qty*mark-r.cost):null; return `
${c?c.tk:esc(r.sym)}
${r.qty.toFixed(6)} ${cryptoFmt(avg)} ${mark!=null?cryptoFmt(mark):'-'} ${pnl!=null?cxMoney(pnl):'-'}
`; }).join(''); const totalTxt=(pf.incomplete?'~':'')+cryptoFmt(pf.total); const incompleteNote=pf.incomplete?infoI('Some holdings are missing a live price right now, this total is a partial sum, not the full picture.'):''; const statLine=`
${icon('sprout',13)}Simulated portfolio holdings${totalTxt} total${incompleteNote} · ${pf.rows.length} coin${pf.rows.length===1?'':'s'}
`; return shield+statLine+`
${head}${body}
`; } function investingDcaTab(){ const inv=state.investing; const draftSym=inv.draftSym||CRYPTO_UNIVERSE[0].sym; const draftAmount=inv.draftAmount!=null?inv.draftAmount:50; const draftCadence=inv.draftCadence||'weekly'; const coinPicker=CRYPTO_UNIVERSE.map(c=>``).join(''); const cadences=[['weekly','Weekly'],['biweekly','Every 2 weeks'],['monthly','Monthly']]; const cadencePicker=cadences.map(([k,l])=>``).join(''); const note=`
${icon('shield',13)}Simulated DCA, real prices. Set an amount and cadence, then tap Simulate a buy now whenever you want to record one, it books at the live price the moment you click, never backdated.
`; const form=`
New DCA plan
Coin${coinPicker}
Cadence${cadencePicker}
`; const plans=inv.dca||[]; const cadLabel=Object.fromEntries(cadences); const cols='grid-template-columns:1fr 130px 100px 100px 220px'; const list=!plans.length?secEmpty('sprout','No DCA plans yet','Set one up below, e.g. $50 of BTC every week.'): `
${icon('activity',13)}Your DCA plans${plans.length} plan${plans.length===1?'':'s'}
Coin
AmountInvestedNowActions
${plans.map(p=>{ const c=CRYPTO_UNIVERSE.find(x=>x.sym===p.sym); const invested=(p.history||[]).reduce((s,h)=>s+h.amount,0); const qty=(p.history||[]).reduce((s,h)=>s+h.qty,0); const q=CRYPTO.quotes[p.sym]; const mark=q?q.ltp:null; const value=mark!=null?qty*mark:null; return `
${c?c.tk:esc(p.sym)}${p.active?'':' Paused'}
${cryptoFmt(p.amount)}/${cadLabel[p.cadence]||p.cadence} ${cryptoFmt(invested)} ${value!=null?cryptoFmt(value):'-'}
`; }).join('')}
`; return note+form+list; } function investingGoalsTab(){ const inv=state.investing; const {total}=investingPortfolioValue(); const draftLabel=inv.draftGoalLabel||''; const draftTarget=inv.draftGoalTarget!=null?inv.draftGoalTarget:1000; const form=`
New goal
`; const goals=inv.goals||[]; const list=!goals.length?secEmpty('activity','No goals yet','Set a target below, this is your own target, never a promise or projection.'): `
${icon('activity',13)}Your goals${goals.length} goal${goals.length===1?'':'s'}
`+ goals.map(g=>{ const pctv=Math.max(0,Math.min(100,(total/g.targetValue)*100)); return `
${esc(g.label)}${cryptoFmt(total)} of ${cryptoFmt(g.targetValue)}
`; }).join(''); return form+list; } function investingSimulateBuy(id){ const p=(state.investing.dca||[]).find(x=>x.id===id); if(!p) return; // BUG FIX (2026-09-20): Pause used to only change the badge/label - a paused plan could still be // bought via this same click handler, which made "Pause" mean nothing functionally. Matches the // "Buy now" button's own new disabled state below, kept here too as defense in depth. if(!p.active) return; const q=CRYPTO.quotes[p.sym]; if(!CRYPTO.live||!q) return; // never record a buy without a real live price p.history.push({ts:Date.now(),price:q.ltp,qty:p.amount/q.ltp,amount:p.amount}); saveState(); renderInvesting(); } function investingToggleDca(id){ const p=(state.investing.dca||[]).find(x=>x.id===id); if(!p) return; p.active=!p.active; saveState(); renderInvesting(); } function investingDeleteDca(id){ const p=(state.investing.dca||[]).find(x=>x.id===id); if(!p) return; const c=CRYPTO_UNIVERSE.find(x=>x.sym===p.sym); // UX FIX (2026-09-20): this used to block on the browser's own confirm() dialog, the only place // left in Investing/Trading still doing that - every other destructive action in the terminal // (Stop a strategy, place a live order) uses flowModal() for a consistent look and feel. flowModal({title:'Delete DCA plan',confirm:'Delete plan',danger:true, body:`

${icon('alert',13)}Delete the ${esc(c?c.tk:p.sym)} DCA plan? This removes its buy history too, and can't be undone.

`, onConfirm(){ state.investing.dca=(state.investing.dca||[]).filter(x=>x.id!==id); saveState(); renderInvesting(); }}); } function investingCreateDca(){ const inv=state.investing; const sym=inv.draftSym||CRYPTO_UNIVERSE[0].sym; const amount=inv.draftAmount!=null?inv.draftAmount:50; const cadence=inv.draftCadence||'weekly'; if(!(amount>0)) return; inv.dca=inv.dca||[]; inv.dca.unshift({id:newDcaId(),sym,amount,cadence,active:true,createdAt:Date.now(),history:[]}); saveState(); renderInvesting(); } function investingCreateGoal(){ const inv=state.investing; const label=(inv.draftGoalLabel||'').trim(); const targetValue=inv.draftGoalTarget!=null?inv.draftGoalTarget:0; if(!label||!(targetValue>0)) return; inv.goals=inv.goals||[]; inv.goals.unshift({id:newGoalId(),label,targetValue,targetDate:null,createdAt:Date.now()}); inv.draftGoalLabel=''; inv.draftGoalTarget=null; saveState(); renderInvesting(); } function investingDeleteGoal(id){ const g=(state.investing.goals||[]).find(x=>x.id===id); if(!g) return; flowModal({title:'Delete goal',confirm:'Delete goal',danger:true, body:`

${icon('alert',13)}Delete ${esc(g.label)}? This can't be undone.

`, onConfirm(){ state.investing.goals=(state.investing.goals||[]).filter(x=>x.id!==id); saveState(); renderInvesting(); }}); } function renderInvesting(){ const v=$('investHub'); if(!v) return; if(state.persona!=='investor'){ v.innerHTML=''; return; } state.investing=state.investing||{view:'portfolio',dca:[],goals:[]}; state.investing.dca=state.investing.dca||[]; state.investing.goals=state.investing.goals||[]; // BUG FIX (2026-09-19): see the identical fix in renderTrading() - polls until CRYPTO.loaded // regardless of who's driving an already-in-flight fetch, so this view is never stuck stale. if(!CRYPTO.loaded){ if(CRYPTO.busy) setTimeout(()=>{ if(state.persona==='investor') renderInvesting(); },300); else loadCrypto().then(()=>{ if(state.persona==='investor') renderInvesting(); }); } const view=state.investing.view; const tabs=[['portfolio','Portfolio'],['dca','DCA Plans'],['simulator','DCA Simulator'],['goals','Goals']]; const head=`
${icon('sprout',17)}
InvestingCrypto · live Binance data · simulated DCA
${tabs.map(([k,l])=>``).join('')}
`; const body=view==='portfolio'?investingPortfolioTab():view==='dca'?investingDcaTab():view==='simulator'?investingSimulatorTab():investingGoalsTab(); v.innerHTML=`
${head}
${body}
`; v.querySelectorAll('[data-investview]').forEach(b=>b.onclick=()=>{state.investing.view=b.dataset.investview;saveState();renderInvesting();}); v.querySelectorAll('[data-dcasym]').forEach(b=>b.onclick=()=>{state.investing.draftSym=b.dataset.dcasym;renderInvesting();}); v.querySelectorAll('[data-dcacadence]').forEach(b=>b.onclick=()=>{state.investing.draftCadence=b.dataset.dcacadence;renderInvesting();}); const ai=v.querySelector('#dcaAmount'); if(ai) ai.oninput=()=>{state.investing.draftAmount=Math.max(0,parseFloat(ai.value)||0);}; const dcaCta=v.querySelector('#dcaCreateCta'); if(dcaCta) dcaCta.onclick=investingCreateDca; v.querySelectorAll('[data-dcabuy]').forEach(b=>b.onclick=()=>investingSimulateBuy(b.dataset.dcabuy)); v.querySelectorAll('[data-dcatoggle]').forEach(b=>b.onclick=()=>investingToggleDca(b.dataset.dcatoggle)); v.querySelectorAll('[data-dcadelete]').forEach(b=>b.onclick=()=>investingDeleteDca(b.dataset.dcadelete)); const gl=v.querySelector('#goalLabel'); if(gl) gl.oninput=()=>{state.investing.draftGoalLabel=gl.value;}; const gt=v.querySelector('#goalTarget'); if(gt) gt.oninput=()=>{state.investing.draftGoalTarget=Math.max(0,parseFloat(gt.value)||0);}; const goalCta=v.querySelector('#goalCreateCta'); if(goalCta) goalCta.onclick=investingCreateGoal; v.querySelectorAll('[data-goaldelete]').forEach(b=>b.onclick=()=>investingDeleteGoal(b.dataset.goaldelete)); // DCA Simulator event bindings v.querySelectorAll('[data-simsym]').forEach(b=>b.onclick=()=>{state.investing.sim=state.investing.sim||{};state.investing.sim.sym=b.dataset.simsym;saveState();renderInvesting();}); v.querySelectorAll('[data-simcadence]').forEach(b=>b.onclick=()=>{state.investing.sim=state.investing.sim||{};state.investing.sim.cadence=b.dataset.simcadence;saveState();renderInvesting();}); v.querySelectorAll('[data-simhorizon]').forEach(b=>b.onclick=()=>{state.investing.sim=state.investing.sim||{};state.investing.sim.horizon=b.dataset.simhorizon;saveState();renderInvesting();}); const simSlider=v.querySelector('#simAmountSlider'); const simInp=v.querySelector('#simAmountInput'); if(simSlider){ simSlider.oninput=()=>{ const val=Math.max(5,Math.min(1000,parseFloat(simSlider.value)||50)); state.investing.sim=state.investing.sim||{};state.investing.sim.amount=val; if(simInp) simInp.value=val; saveState(); const res=computeDcaSimulation(state.investing.sim.sym||'BTCUSDT', val, state.investing.sim.cadence||'weekly', state.investing.sim.horizon||'1Y'); const chartBody=v.querySelector('.dca-chart-body'); if(chartBody) chartBody.innerHTML=renderDcaSimulatorSvg(res); const metCards=v.querySelectorAll('.dca-met-card'); if(metCards.length>=4){ metCards[0].querySelector('.dca-met-val').textContent=cryptoFmt(res.totalInvested); metCards[0].querySelector('.dca-met-sub').textContent=`${res.steps} periodic buys`; metCards[1].querySelector('.dca-met-val').textContent=cryptoFmt(res.finalDcaVal); metCards[1].querySelector('.dca-met-sub').textContent=`+${res.dcaRoi.toFixed(1)}% net ROI`; metCards[2].querySelector('.dca-met-val').textContent=cryptoFmt(res.finalLumpVal); metCards[2].querySelector('.dca-met-sub').textContent=`${res.lumpRoi>=0?'+':''}${res.lumpRoi.toFixed(1)}% ROI`; metCards[3].querySelector('.dca-met-val').textContent=`${res.volShield.toFixed(1)}%`; } const deployBtn=v.querySelector('#simDeployCta'); if(deployBtn){ const sc=CRYPTO_UNIVERSE.find(x=>x.sym===(state.investing.sim.sym||'BTCUSDT'))||CRYPTO_UNIVERSE[0]; deployBtn.textContent=`Create Active Plan ($${val}/${state.investing.sim.cadence||'weekly'} ${sc.tk})`; } }; } if(simInp){ simInp.oninput=()=>{ const val=Math.max(5,Math.min(1000,parseFloat(simInp.value)||50)); state.investing.sim=state.investing.sim||{};state.investing.sim.amount=val; if(simSlider) simSlider.value=val; saveState(); renderInvesting(); }; } const simDeployBtn=v.querySelector('#simDeployCta'); if(simDeployBtn){ simDeployBtn.onclick=()=>{ playAudioFeedback('success'); investingDeployPlan(); }; } const simCopyBtn=v.querySelector('#simCopyPlanCta'); if(simCopyBtn){ simCopyBtn.onclick=()=>{ const sim=state.investing.sim; const selCoin=CRYPTO_UNIVERSE.find(x=>x.sym===sim.sym)||CRYPTO_UNIVERSE[0]; const res=computeDcaSimulation(sim.sym, sim.amount, sim.cadence, sim.horizon); const text=[ `# zengtrade DCA Investment Plan: ${selCoin.name} (${selCoin.tk})`, `- Asset: ${selCoin.name} (${selCoin.sym})`, `- Cadence: ${sim.cadence.toUpperCase()} contribution of $${sim.amount}`, `- Horizon: ${sim.horizon} (${res.periods} contribution periods)`, `- Total Invested: $${res.totalInvested.toLocaleString()}`, `- Projected Future Value: $${Math.round(res.futureValue).toLocaleString()}`, `- Estimated Net Yield: +${res.netYieldPct.toFixed(1)}%`, `- Volatility Shield: ${res.downsideShieldPct}% historical downside mitigation`, `- Cost Model: 35 bps round-trip friction deducted`, `Generated via zengtrade (https://zengtrade.in/dashboard)` ].join('\n'); navigator.clipboard.writeText(text).then(()=>{ playAudioFeedback('bracket'); simCopyBtn.textContent='✅ Copied!'; quickToast('Plan Copied', 'DCA investment plan summary copied to clipboard.'); setTimeout(()=>{ simCopyBtn.textContent='📋 Copy Summary'; }, 2000); }).catch(()=>{ quickToast('Copy Blocked', 'Clipboard access unavailable in current context.'); }); }; } const simCsvBtn=v.querySelector('#simExportCsvCta'); if(simCsvBtn){ simCsvBtn.onclick=()=>{ const sim=state.investing.sim; const selCoin=CRYPTO_UNIVERSE.find(x=>x.sym===sim.sym)||CRYPTO_UNIVERSE[0]; const res=computeDcaSimulation(sim.sym, sim.amount, sim.cadence, sim.horizon); const rows=[ ['Period', 'Contribution_USD', 'Cumulative_Invested_USD', 'Projected_Value_USD', 'Friction_Bps'] ]; const steps=12; for(let i=1; i<=steps; i++){ const periodIdx=Math.min(Math.round(i*(res.periods/steps)), res.periods); const inv=periodIdx*sim.amount; const ratio=periodIdx/res.periods; const val=inv+(res.futureValue-res.totalInvested)*Math.pow(ratio, 1.2); rows.push([i, sim.amount, inv, Math.round(val), 35]); } const csvContent='data:text/csv;charset=utf-8,' + encodeURIComponent(rows.map(e=>e.join(',')).join('\n')); const link=document.createElement('a'); link.setAttribute('href', csvContent); link.setAttribute('download', `zengtrade_dca_${selCoin.tk}_${sim.horizon}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); playAudioFeedback('success'); quickToast('CSV Exported', `Downloaded schedule for ${selCoin.tk}.`); }; } } function renderAlgo(){ const v=$('algoView'); if(!v) return; if(!isAlgo()){ v.innerHTML=''; return; } state.algo=state.algo||{view:'monitor',bt:{algo:0,period:'1Y'}}; if(!state.algo.view) state.algo.view='monitor'; if(['market','opportunity','leaderboard'].includes(state.algo.view)) state.algo.view='monitor'; // retired tabs → land on Monitor if(!state.algo.bt) state.algo.bt={algo:0,period:'1Y'}; // guard: setMarket/studioScope can create state.algo before this default if(!state.algo.exec) state.algo.exec='paper'; state.algo.market='crypto'; if(!state.algo.cinstr) state.algo.cinstr='spot'; // crypto instrument scope (Spot/Perps/Options) const crypto=true; if(!BOT.loaded){ loadBotData().then(()=>{ if(isAlgo())renderAlgo(); }); } // BUG FIX (2026-09-19): see the identical fix in renderTrading() - polls until CRYPTO.loaded // regardless of who's driving an already-in-flight fetch, so this view is never stuck stale. if(!CRYPTO.loaded){ if(CRYPTO.busy) setTimeout(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); },300); else loadCrypto().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); } const view=state.algo.view; if((view==='monitor'||view==='positions') && !CRYPTOMON.loaded && !CRYPTOMON.busy){ loadCryptoMonitor().then(()=>{ if(isAlgo()&&state.algo.market==='crypto') renderAlgo(); }); } const depN=cxActive(); // Monitor badge reflects deployed strategy count // Decluttered for clarity: a simple left-to-right flow: watch → browse → hold → prove → protect → history. // (Retired: Marketplace [dup of Library], Leaderboard [dup of Forward Test/Accuracy], Opportunity Engine [equity discretionary picker].) const tabs=[['monitor','Monitor'],['library','Library'],['positions','Positions'],['forward','Forward Test'],['accuracy','Accuracy'],['analytics','Analytics'],['risk','Risk Governor'],['backtest','Backtest']]; const head=`
${icon('cpu',17)}
Algo StudioCrypto · live Binance data · paper trading, 24/7
${tabs.map(([k,l])=>``).join('')}
`; const tabLabel=(tabs.find(t=>t[0]===view)||[,'This view'])[1]; const body=cryptoBody(view,tabLabel); // Preserve scroll across the full innerHTML rebuild so in-place CTAs (sort, filter, scope toggles, sub-tabs, // live P&L patches) don't flick/jump to the top. The algo view scrolls EITHER the page, the .pane-center, OR // the inner .av-scroll depending on layout: capture and restore ALL THREE. Reset to top ONLY on real // navigation: a market switch or a TAB (view) change. Scope toggles (instrument/holding) are in-tab filters, // NOT navigation, so they keep your place (that was the "every click jumps me to the hero" bug). const _scEl=document.scrollingElement||document.documentElement; const _pane=v.closest('.pane-center'); const _av0=v.querySelector('.av-scroll'); const _vk='crypto/'+view; const _keep=(state.algo._vk===_vk); state.algo._vk=_vk; const _sy=_keep?_scEl.scrollTop:0, _py=(_keep&&_pane)?_pane.scrollTop:0, _avy=(_keep&&_av0)?_av0.scrollTop:0; // crypto instrument scope (Spot/Perps/Options) shows on every tab except Monitor/Positions, which render it inline. const cxScope=['monitor','positions'].includes(view)?'':cryptoScopeBar(); v.innerHTML=`
${head}
${cryptoStatusBar()}${cxScope}${body}
`; _scEl.scrollTop=_sy; if(_pane) _pane.scrollTop=_py; const _av1=v.querySelector('.av-scroll'); if(_av1) _av1.scrollTop=_avy; v.querySelectorAll('[data-algoview]').forEach(b=>b.onclick=()=>{state.algo.view=b.dataset.algoview;saveState();renderAlgo();}); v.querySelectorAll('[data-algoinstr]').forEach(b=>b.onclick=()=>{ const a=state.algo; if(a.instr===b.dataset.algoinstr) return; a.instr=b.dataset.algoinstr; a.hold='all'; if(a.lib) a.lib.fam='all'; renderAlgo(); }); v.querySelectorAll('[data-algohold]').forEach(b=>b.onclick=()=>{ state.algo.hold=b.dataset.algohold; renderAlgo(); }); v.querySelectorAll('[data-cinstr]').forEach(b=>b.onclick=()=>{ state.algo.cinstr=b.dataset.cinstr; renderAlgo(); }); v.querySelectorAll('[data-cbtstrat]').forEach(b=>b.onclick=()=>{ state.algo.cbt=state.algo.cbt||{strat:'macross',period:'1Y'}; state.algo.cbt.strat=b.dataset.cbtstrat; renderAlgo(); }); v.querySelectorAll('[data-cbtperiod]').forEach(b=>b.onclick=()=>{ state.algo.cbt=state.algo.cbt||{strat:'macross',period:'1Y'}; state.algo.cbt.period=b.dataset.cbtperiod; renderAlgo(); }); v.querySelectorAll('[data-cxcsv]').forEach(b=>b.onclick=cxTradesCSV); v.querySelectorAll('[data-cxgl]').forEach(b=>b.onclick=e=>{e.stopPropagation();cryptoGoLiveCheck(b.dataset.cxgl);}); v.querySelectorAll('[data-cxalloc]').forEach(sl=>{ let _t=null; sl.oninput=()=>{ const lab=v.querySelector('[data-cxallocval="'+CSS.escape(sl.dataset.cxalloc)+'"]'); if(lab)lab.textContent=sl.value+'%'; clearTimeout(_t); _t=setTimeout(()=>cxSetAlloc(sl.dataset.cxalloc,(+sl.value)/100), 400); }; // debounce the write while dragging sl.onchange=()=>{ clearTimeout(_t); cxSetAlloc(sl.dataset.cxalloc,(+sl.value)/100); }; // commit immediately on release sl.onclick=e=>e.stopPropagation(); }); v.querySelectorAll('[data-algogoto]').forEach(b=>b.onclick=()=>{state.algo.view=b.dataset.algogoto;renderAlgo();}); v.querySelectorAll('[data-cxdep]').forEach(b=>b.onclick=()=>cryptoLibDeploy(b.dataset.cxdep)); // Monitor accordion: click a strategy to reveal its positions + forward accuracy. // Toggles the class directly (no re-render) → smooth, and survives the 2s live poll. v.querySelectorAll('[data-monexp]').forEach(el=>{ const tog=e=>{ if(e&&e.target&&e.target.closest('[data-algogl],[data-cxgl],[data-algogoto]'))return; const id=el.dataset.monexp, m=state.algo.monExpand=state.algo.monExpand||{}; m[id]=!m[id]; const card=el.closest('.mon-card'); if(card)card.classList.toggle('open',m[id]); el.setAttribute('aria-expanded',String(!!m[id])); }; el.onclick=tog; el.onkeydown=e=>{ if(e.key==='Enter'||e.key===' '){e.preventDefault();tog(e);} }; }); v.querySelectorAll('[data-execmode]').forEach(b=>b.onclick=()=>{state.algo.exec=b.dataset.execmode;renderAlgo();}); v.querySelectorAll('[data-monsort]').forEach(b=>b.onclick=()=>{state.algo.monSort=b.dataset.monsort;renderAlgo();}); v.querySelectorAll('[data-monfilter]').forEach(b=>b.onclick=()=>{state.algo.monFilter=b.dataset.monfilter;renderAlgo();}); v.querySelectorAll('[data-cxlibcat]').forEach(b=>b.onclick=()=>{state.algo.cxLibCat=b.dataset.cxlibcat;renderAlgo();}); state.algo._sig=algoLiveSig(); // snapshot structure so the 2s poll knows when a full re-render is needed } /* ===== Info icons + plain-English definitions for every metric/strategy ===== */ function infoI(tip){return tip?`i`:'';} const POS_ACT={hold:['Hold','hold'],protect:['Protect profit','protect'],watch:['Watch','watch'],exit:['Exit, thesis decayed','exit']}; function posTone(h){ return (h==null)?'na':h>=70?'hi':h>=50?'mid':'lo'; } function timeAgo(iso){ try{ const s=Math.max(0,(Date.now()-new Date(iso).getTime())/1000); if(s<60)return Math.round(s)+'s ago'; if(s<3600)return Math.round(s/60)+'m ago'; if(s<86400)return Math.round(s/3600)+'h ago'; return Math.round(s/86400)+'d ago'; }catch(e){return '';} } function ddCurveSVG(dd){ const W=600,H=70,pad=6; const pts=(Array.isArray(dd)&&dd.length>1)?dd:[0,0]; const lo=Math.min(...pts,-0.01); // most-negative; avoid /0 const X=i=>i/(pts.length-1)*W, Y=v=>pad+(v/lo)*(H-2*pad); // 0 at top, worst at bottom const d=pts.map((v,i)=>(i?'L':'M')+X(i).toFixed(1)+','+Y(v).toFixed(1)).join(' '); const area=d+` L${W},${pad} L0,${pad} Z`; return ``; } function monthlyHeat(monthly){ if(!Array.isArray(monthly)||!monthly.length) return ''; const byY={}; monthly.forEach(m=>{const p=String(m.ym).split('-');(byY[p[0]]=byY[p[0]]||{})[+p[1]]=m.ret;}); const years=Object.keys(byY).sort(); const mn=['J','F','M','A','M','J','J','A','S','O','N','D']; const cell=v=>{ if(v==null) return ''; const a=Math.min(Math.abs(v)/8,.5)+.08; const g=v>=0; return `${v>=0?'+':''}${Math.round(v)}`; }; return `
${icon('clock',13)} Monthly returns green = up month, red = down
${mn.map((m,i)=>``).join('')}${years.map(y=>`${[1,2,3,4,5,6,7,8,9,10,11,12].map(mo=>cell(byY[y][mo])).join('')}`).join('')}
${m}
${esc(y)}
`; } function liveArmed(){ try{ return !!(typeof BOT!=='undefined'&&BOT.liveArmed) || !!(typeof CRYPTOMON!=='undefined'&&CRYPTOMON.data&&CRYPTOMON.data.liveArmed); }catch(e){ return false; } } function execToggle(paperN,liveN){ const exec=state.algo.exec||'paper', armed=liveArmed(); return `
` +`` +`` +`
`; } function liveLockedPanel(){ const crypto=state.algo.market==='crypto'; return `
${icon('shield',15)}
Live trading is locked` +`Nothing places real orders yet. Live turns on only when all three are true:` +`
  1. A strategy has cleared the Go-Live bar: proven forward, net of costs
  2. ` +`
  3. Your ${crypto?'exchange (Binance)':'broker (Zerodha Kite)'} is connected with trade-only keys
  4. ` +`
  5. ALLOW_LIVE is armed at the OS level on the bot machine, a browser can never do this
` +`Everything runs in paper until then. This is deliberate, it's what keeps your money safe.
`; } /* ============================================================ AI MODE: copilot chat + AI signals (XSS-safe via esc()) ============================================================ */ const AI_PROMPTS=['Top movers right now','Find me oversold ideas','Hedge my portfolio','Explain my portfolio health','Best option strategy now']; const aiCfg=()=>state.aiCfg||(state.aiCfg={endpoint:'',model:'claude-opus-4-8'}); const aiLive=()=>!!(aiCfg().endpoint||'').trim(); function setSurface(s,silent){ state.surface=s; document.documentElement.dataset.surface=s; const b=$('surfaceToggle'); if(b){ b.innerHTML=icon(s==='night'?'sun':'moon',16); b.setAttribute('aria-label',s==='night'?'Switch to day mode':'Switch to night trading-floor mode'); b.setAttribute('aria-pressed',s==='night'); } if(s==='night'&&!silent) powerOn(); announce(s==='night'?'Night trading-floor mode on':'Day mode on'); saveState(); } function toggleSurface(){ cascadeSurface(state.surface==='night'?'day':'night'); } function tapeLoop(){ doTick(); const vix=+$('sVix').value, night=state.surface==='night'?0.7:1; const delay=clamp((1500-(vix-8)*42)*night,300,1500); state.tapeT=setTimeout(tapeLoop,delay); } function applyPaneWidths(){ const t=document.querySelector('.terminal'); if(!t)return; // BUG FIX (2026-09-19): this used to give trader/investor a resizable 3-pane inline width // (watchlist | chart | order-pad), back when only algo/ai used the single-column layout. All // four personas now use the same single-container-per-mode layout (#algoView/#tradingView/ // #investHub/#aiView, styles.css's universal `.terminal{grid-template-columns:1fr}`) - the old // 3-pane layout is retired everywhere, not just for algo/ai. Leaving this stale inline width in // place for trader/investor overrode that CSS rule (inline style beats a class selector), // collapsing #investHub/#tradingView into a ~330px column and leaving most of the screen as // bare ambient background with nothing drawn over it. Always clear it now; there's no persona // left that needs a JS-computed grid width. t.style.gridTemplateColumns=''; } function applyChartHeight(){ const card=$('chartCard'); if(card) card.style.height=state.chartH?state.chartH+'px':''; } const sgn=n=>Number.isFinite(n)?(n>=0?'+':'−')+'$'+Math.abs(Math.round(n)).toLocaleString():'-'; const WIDGET_CATALOG={ trader:[ {key:'movers',name:'Top Movers',icon:'trendUp',desc:'Biggest gainers & losers right now',render(){ if(!BOT.live) return `
${icon('shield',13)} Connect the exchange for live movers.
`; const live=SYMS.filter(s=>s.live!==false); // exclude symbols with no real quote, never a stale price if(!live.length) return `
No live quotes yet.
`; const g=[...live].sort((a,b)=>b.chg-a.chg), row=s=>`
${s.sym}${s.ltp.toLocaleString()}${pct(s.chg)}
`; return `
Gainers
${g.slice(0,3).map(row).join('')}
Losers
${g.slice(-3).reverse().map(row).join('')}
`;}}, {key:'pnl',name:'Day P&L',icon:'bolt',desc:'Today’s real P&L across your paper holdings',render(){ if(!BOT.live||!BOT.holdings) return `
-
Connect the exchange for live Day P&L.
`; const hs=BOT.holdings.holdings||[]; if(!hs.length) return `
$0
No holdings yet, your real Day P&L shows here once you hold positions.
`; const day=BOT.holdings.dayPnl||0, byPct=[...hs].map(h=>({sym:h.sym,chg:h.dayChangePct||0})); const best=[...byPct].sort((a,b)=>b.chg-a.chg)[0], worst=[...byPct].sort((a,b)=>a.chg-b.chg)[0]; return `
${sgn(day)}
Best${esc(best.sym)}${pct(best.chg)}
Worst${esc(worst.sym)}${pct(worst.chg)}
${hs.length} holdings · total ${sgn(BOT.holdings.totalPnl||0)}
`;}}, {key:'depth',name:'Market Depth',icon:'sliders',desc:'5-level bid / ask ladder',render(){ if(!BOT.live) return `
${icon('shield',13)} Connect the exchange for live market depth.
`; const sel=state.selected||'BTCUSDT', d=BOT.depth; if(!d||d.symbol!==sel){ loadDepth(sel); return `
Loading live depth for ${esc(sel)}…
`; } if(d.error) return `
No depth for ${esc(sel)}, ${esc(d.error)}.
`; return depthLadderHtml(d);}}, ], }; const WIDGET_DEFAULTS={trader:['movers','pnl'],investor:[]}; // investor catalog removed (unreachable persona under CRYPTO_ONLY), key kept for state-init structural compat const personaKey=()=>isInvestor()?'investor':'trader'; const widgetCatalog=()=>WIDGET_CATALOG[personaKey()]; function activeWidgets(){ if(!state.widgets)state.widgets={trader:WIDGET_DEFAULTS.trader.slice(),investor:WIDGET_DEFAULTS.investor.slice()}; return state.widgets[personaKey()]; } function renderWidgetStack(){ const wrap=$('widgetStack'); if(!wrap) return; const cat=widgetCatalog(), keys=activeWidgets(); wrap.innerHTML=keys.map(k=>{const w=cat.find(x=>x.key===k); if(!w) return ''; return `
${icon(w.icon,13)}${w.name}
${w.render()}
`;}).join(''); wrap.querySelectorAll('[data-wremove]').forEach(b=>b.onclick=e=>{e.stopPropagation();removeWidget(b.dataset.wremove);}); initWidgetDnD(); } function openWidgetGallery(o){ const g=$('widgetGallery'); if(!g)return; g.classList.toggle('collapsed',!o); $('widgetGalleryScrim').classList.toggle('show',o); if(o)renderWidgetGallery(); } function addWidget(k){const a=activeWidgets(); if(widgetCatalog().some(w=>w.key===k)&&!a.includes(k)){a.push(k);renderWidgetStack();renderWidgetGallery();saveState();}} function removeWidget(k){const p=personaKey();state.widgets[p]=activeWidgets().filter(x=>x!==k);renderWidgetStack();renderWidgetGallery();saveState();} function toggleWidget(k){ activeWidgets().includes(k)?removeWidget(k):addWidget(k); } function renderWidgetGallery(){ const body=$('wgGalleryBody'); if(!body) return; const inv=isInvestor(), keys=activeWidgets(); const sub=$('wgSubtitle'); if(sub) sub.textContent=inv?'Wealth, planning & discovery widgets':'Real-time market & execution widgets'; const panes=[['watchlist','star'],['chart','trendUp'],['panel','layout'],['order','scale'],['context','target']]; const paneHtml=`
Layout panels
`+panes.map(([k,ic])=>{const on=paneVisible(k),nm=CARD_LABEL[k]||k; return ``;}).join(''); body.innerHTML=`
Tailored for ${inv?'Investing':'Trading'} · your picks are saved per mode
` +paneHtml+`
Widgets
` +widgetCatalog().map(w=>{const on=keys.includes(w.key); return ``;}).join(''); body.querySelectorAll('[data-wtoggle]').forEach(b=>b.onclick=()=>toggleWidget(b.dataset.wtoggle)); body.querySelectorAll('[data-ptoggle]').forEach(b=>b.onclick=()=>togglePane(b.dataset.ptoggle)); } function initWidgetDnD(){ const wrap=$('widgetStack'); if(!wrap) return; wrap.querySelectorAll('[data-wkey]').forEach(card=>{ card.ondragstart=e=>{state.dragWidget=card.dataset.wkey;card.classList.add('wg-drag');e.dataTransfer.effectAllowed='move';}; card.ondragend=()=>{card.classList.remove('wg-drag');state.dragWidget=null;}; card.ondragover=e=>{e.preventDefault();}; card.ondrop=e=>{e.preventDefault();const from=state.dragWidget,to=card.dataset.wkey;if(!from||from===to)return; const a=activeWidgets(); a.splice(a.indexOf(from),1); a.splice(a.indexOf(to),0,from); renderWidgetStack();saveState();}; }); } const CARD_EL={watchlist:'.pane-left',chart:'.chart-card',panel:'.panel',order:'#orderPad',context:'#contextModule'}; const CARD_LABEL={watchlist:'Watchlist',chart:'Chart',panel:'Orders & scanner',order:'Order pad',context:'Insights'}; function cardCtl(key){ const nm=CARD_LABEL[key]||key; return `
`; } function mountStableCardCtls(){ // chart + watchlist have stable header bars; inject the controls once const chart=document.querySelector('.ch-bar'); if(chart && !chart.querySelector('.card-ctl')){const d=document.createElement('div');d.className='card-ctl';d.dataset.cardgrp='chart';d.innerHTML=cardCtl('chart').replace(/^]*>|<\/div>$/g,'');chart.appendChild(d);} const wl=document.querySelector('.pane-head'); if(wl && !wl.querySelector('.card-ctl')){const d=document.createElement('div');d.className='card-ctl';d.dataset.cardgrp='watchlist';d.innerHTML=cardCtl('watchlist').replace(/^]*>|<\/div>$/g,'');wl.appendChild(d);} const panel=document.querySelector('.panel'); if(panel && !panel.querySelector(':scope > .card-ctl')){const d=document.createElement('div');d.className='card-ctl card-ctl-abs';d.dataset.cardgrp='panel';d.innerHTML=cardCtl('panel').replace(/^]*>|<\/div>$/g,'');panel.appendChild(d);} } function updateCardBtns(){ Object.keys(CARD_EL).forEach(k=>{ const el=document.querySelector(CARD_EL[k]); if(!el)return; const m=(state.cards&&state.cards[k])||'normal'; const mn=el.querySelector('.cc-min'), mx=el.querySelector('.cc-max'); if(mn) mn.innerHTML=icon(m==='min'?'plus':'minus',13); if(mx) mx.innerHTML=icon(m==='max'?'compress':'expand',13); }); } function paneVisible(key){ return ((state.cards&&state.cards[key])||'normal')!=='hidden'; } function togglePane(key){ if(!state.cards)state.cards={}; state.cards[key]= paneVisible(key)?'hidden':'normal'; applyCardStates(); applyPaneWidths(); renderWidgetGallery(); saveState(); } function applyCardStates(){ let anyMax=false; Object.keys(CARD_EL).forEach(k=>{ const el=document.querySelector(CARD_EL[k]); if(!el)return; const m=(state.cards&&state.cards[k])||'normal'; el.classList.toggle('card-min', m==='min'); el.classList.toggle('card-max', m==='max'); el.classList.toggle('card-hidden', m==='hidden'); if(m==='max')anyMax=true; }); const term=document.querySelector('.terminal'); if(term) term.classList.toggle('has-max',anyMax); const scrim=$('cardScrim'); if(scrim) scrim.classList.toggle('show',anyMax); document.body.classList.toggle('card-maxed',anyMax); updateCardBtns(); if(window.TPChart&&TPChart.resize){TPChart.resize();setTimeout(()=>TPChart.resize(),70);} } function toggleCard(key,which){ if(!state.cards)state.cards={}; const cur=state.cards[key]||'normal', nm=CARD_LABEL[key]||key; if(which==='close'){ state.cards[key]='hidden'; wsRestoreDismissed=false; } else if(which==='min'){ state.cards[key]= cur==='min'?'normal':'min'; } else { if(cur==='max'){state.cards[key]='normal';} else { Object.keys(CARD_EL).forEach(k=>{if(state.cards[k]==='max')state.cards[k]='normal';}); state.cards[key]='max'; } } applyCardStates(); applyPaneWidths(); const st=state.cards[key]; if(st==='hidden') quickToast(nm+' hidden','Restore it from + Widgets → Layout panels.'); else announce(`${nm} ${st==='normal'?'restored':st==='min'?'minimized':'maximized'}`); saveState(); } function restoreMaxCard(){ let changed=false; Object.keys(CARD_EL).forEach(k=>{if(state.cards[k]==='max'){state.cards[k]='normal';changed=true;}}); if(changed){applyCardStates();saveState();} } function recompute(opts={}){ syncSliderLabels(); const raw=readSignals(),sc=scoreSignals(raw),S=composite(sc),conf=confidence(S,sc); // Live: the bot's regime is authoritative for BOTH the 4-state label and the 3-state terminal theme (via THEME_OF). // What-if / offline: classify the composite with hysteresis, then derive the label from theme + VIX. const liveReg=(!state.simOverride && BOT.live && BOT.market && BOT.market.engine)?BOT.market.engine.regime:null; const newEngine=(liveReg&&THEME_OF[liveReg])?THEME_OF[liveReg]:classify(S,state.engine); state.engine=newEngine; const label=liveReg||regime4(newEngine,raw.vix,state.regimeLabel); state.regimeLabel=label; renderEngine(S,sc,conf,newEngine,label);$('autoConf').textContent=conf+'%'; renderTopIndex(); renderRegimeBar(state.displayed); const vixROC=(raw.vix-state.prevVix)/Math.max(1,state.prevVix); const hard=state.forceHard||vixROC>0.15;state.forceHard=false; const mismatch=newEngine!==state.displayed; if(state.mode==='auto'){ if(opts.silent){ if(mismatch) applyRegime(newEngine); } // quiet adoption (boot / first live sync), no toast/cinematic else if(mismatch&&!state.suggesting){ if(hard){applyRegime(newEngine);infoToast(newEngine,S,conf,sc,true,label);} else if(conf>=65){suggestToast(newEngine,S,conf,sc,false,label);} } }else if(mismatch&&conf>=65&&!state.suggesting&&!opts.silent){suggestToast(newEngine,S,conf,sc,true,label);} state.prevVix=raw.vix; renderEngineSrc(); } /* ---------- Trading Floor (Day/Night) surface ---------- */ function powerOn(){ const f=$('floorSweep'); if(!f)return; f.classList.remove('go'); void f.offsetWidth; f.classList.add('go'); } /* ---------- day/night CASCADE: trading floor powers on/off, pane by pane ---------- Freeze each visible pane in the OLD theme (inline CSS vars inherit to the whole subtree), flip the global theme so the canvas changes at once, then release the panes in a ripple outward from the toggle, each flipping with an accent flash. */ const THEME_VARS=['--bg','--surface','--surface-2','--white','--line','--line-2','--navy','--slate','--slate-2','--green','--green-d','--red','--red-d','--blue','--amber','--tint-down','--tint-warn','--tint-info','--bd-down','--bd-warn','--up-flash','--down-flash','--topbar-bg','--glass','--glass-hi','--shadow','--shadow-hover','--shadow-lg','--accent','--accent-d','--accent-soft','--accent-line']; const CASCADE_SEL=['.topbar','.ticker-bar','.regime-bar','.pane-left','.chart-card','#investHub','.panel','.order-card','.ctx-card','.mode-switch', /* algo / ai / trader-desk takeover panes. So day↔night powers on in EVERY persona, not just the 3-pane floor */ '.av-head','.av-scroll','.ai-main','.ai-side','.desk-head','.desk-scroll']; function cascadeSurface(next){ if(prefersReduced()||!document.querySelector('.topbar')){ setSurface(next); return; } const cs=getComputedStyle(document.documentElement); const oldVals={}; THEME_VARS.forEach(v=>{const val=cs.getPropertyValue(v).trim(); if(val) oldVals[v]=val;}); const keys=Object.keys(oldVals); const btn=$('surfaceToggle'); const br=btn?btn.getBoundingClientRect():{left:innerWidth-40,top:20,width:24,height:24}; const ox=br.left+br.width/2, oy=br.top+br.height/2; const cards=[]; CASCADE_SEL.forEach(s=>document.querySelectorAll(s).forEach(el=>{ if(el.getClientRects().length) cards.push(el); })); cards.forEach(el=>{ keys.forEach(v=>el.style.setProperty(v,oldVals[v])); // freeze in old theme const r=el.getBoundingClientRect(), cx=r.left+r.width/2, cy=r.top+r.height/2; el.__dist=Math.hypot(cx-ox,cy-oy); el.style.setProperty('--fx',(((ox-r.left)/Math.max(1,r.width))*100).toFixed(1)+'%'); // flash points back at toggle el.style.setProperty('--fy',(((oy-r.top)/Math.max(1,r.height))*100).toFixed(1)+'%'); }); cards.sort((a,b)=>a.__dist-b.__dist); setSurface(next,true); // flip global theme (canvas) now; panes held by local vars const step=58; cards.forEach((el,i)=>setTimeout(()=>{ el.classList.add('theme-flip'); keys.forEach(v=>el.style.removeProperty(v)); // release → pane eases to new theme + flashes setTimeout(()=>{ el.classList.remove('theme-flip'); el.style.removeProperty('--fx'); el.style.removeProperty('--fy'); },620); }, i*step)); } /* ---------- live tape: VIX-driven ticks with uptick/downtick flash ---------- */ function flashNum(el,txt,dir){ el.textContent=txt; el.classList.remove('tk-up','tk-down'); void el.offsetWidth; el.classList.add(dir>=0?'tk-up':'tk-down'); } function doTick(){ // LIVE: real Kite WebSocket ticks (loadTicks) drive every price, never fabricate // movement on top of them. This synthetic tape only animates the offline demo. // (BOT is a module-scoped `let`, NOT on window, reference it directly.) if(typeof BOT!=='undefined' && BOT.live) return; if(state.algo && state.algo.market==='crypto') return; // crypto tape is fed by real Binance data, never synth-tick it const vix=+$('sVix').value, vol=clamp((vix-8)/27,0,1), night=state.surface==='night'?1.4:1; const upd=(el,dec)=>{const base=parseFloat(el.textContent.replace(/,/g,''))||0; if(!base)return; const mv=(Math.random()-0.5)*base*0.0007*(0.4+vol*3.2)*night; flashNum(el,(base+mv).toLocaleString('en-IN',{maximumFractionDigits:dec}),mv);}; document.querySelectorAll('#topIndex .tb-seq-a .tix-val').forEach(el=>upd(el,(parseFloat(el.textContent.replace(/,/g,''))||0)>=20000?0:1)); const _sa=document.querySelector('#topIndex .tb-seq-a'),_sb=document.querySelector('#topIndex .tb-seq-b'); if(_sa&&_sb)_sb.innerHTML=_sa.innerHTML; // keep the looped copy in sync document.querySelectorAll('#wlRows .wl-row .wl-ltp').forEach(el=>{if(Math.random()<0.6+vol*0.4)upd(el, (parseFloat(el.textContent.replace(/,/g,''))||0)>1000?1:2);}); const op=$('ordLtp'); if(op) upd(op,1); } /* ============================================================ CARD MINIMIZE / MAXIMIZE (per-card focus & flexibility) ============================================================ */ /* ============================================================ WIDGET LIBRARY: persona-aware, user-composable cards (right rail) Two distinct catalogs are the trader/investor differentiator. ============================================================ */ /* ============================================================ CONTROLLER ============================================================ */ function syncSliderLabels(){const s=readSignals(); $('vTrend').textContent=(s.trend>=0?'+':'')+s.trend;$('vVix').textContent=s.vix.toFixed(1); $('vAd').textContent=s.ad.toFixed(2);$('vRsi').textContent=s.rsi;$('vPnl').textContent=(s.pnl>=0?'+':'')+s.pnl.toFixed(1)+'%';} /* ---- Regime panel ↔ live Kite: feed the real signals into the sliders so the panel reacts as if a human dragged them. */ function syncSlidersFromLive(){ const sg=BOT.live&&BOT.market&&BOT.market.signals; if(!sg) return false; const set=(id,v,lo,hi)=>{ if(typeof v!=='number'||!isFinite(v)) return; const el=$(id); if(el) el.value=clamp(v,lo,hi); }; set('sTrend',sg.trend,-100,100); set('sVix',sg.vix,8,35); set('sAd',sg.ad,0.2,3); set('sRsi',sg.rsi,20,80); set('sPnl',sg.pnl,-10,10); if(typeof sg.macd==='boolean'){ const mt=$('macdToggle'); if(mt){ mt.dataset.on=String(sg.macd); mt.textContent=sg.macd?'Bullish ↑':'Bearish ↓'; } } return true; } // Pull the panel back onto live data (clears a what-if). Called by the "Use live" button. function resyncLive(){ state.simOverride=false; BOT._regimeSynced=false; if(BOT.live && syncSlidersFromLive()){ recompute({silent:true}); BOT._regimeSynced=true; } else { recompute(); } } // Honest source badge: LIVE (mirrors Kite) · WHAT-IF (user override) · SIMULATED (offline demo). function renderEngine(S,sc,conf,regime,label){ $('gaugeNeedle').style.left=clamp((S+100)/200*100,1,99)+'%'; const rd=$('erRegime'); rd.textContent=(label||regime).toUpperCase(); rd.dataset.reg=label||regime; // 4-state label (Bull/Bear/Choppy/High-Vol) $('erScore').textContent=(S>=0?'+':'')+S; $('erConf').textContent=conf+'%'; const sigs=[['Trend',sc.trend,'.30'],['Volatility',sc.vix,'.20'],['Breadth',sc.ad,'.20'],['Momentum',sc.mom,'.20'],['Personal',sc.pers,'.10']]; $('signals').innerHTML=sigs.map(([nm,v,w])=>{const pos=v>=0,width=Math.abs(v)/100*50; return `
${nm} ${w} ${v>=0?'+':''}${v}
`;}).join(''); } function renderEngineSrc(){ const el=$('engSrc'); if(!el) return; if(state.simOverride){ el.className='eng-src sim'; el.innerHTML=`What-if · simulated inputs, not the live market${BOT.live?``:''}`; } else if(BOT.live && BOT.market){ const reg=(BOT.market.engine&&BOT.market.engine.regime)||'-'; const t=BOT.market.asOf?new Date(BOT.market.asOf).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'}):''; el.className='eng-src live'; el.innerHTML=`● LIVE from Binance · ${esc(reg)}${t?` · ${t}`:''} · updates every 30s`; } else { el.className='eng-src off'; el.innerHTML=`Simulated · connect the live engine for the real regime, drag to explore`; } const rb=$('esReset'); if(rb) rb.onclick=resyncLive; } // Any manual interaction switches the panel into what-if mode (stops live mirroring until "Use live"). function markSimOverride(){ if(!state.simOverride){ state.simOverride=true; renderEngineSrc(); } } /* ---------- toasts ---------- */ function clearToasts(){$('toastWrap').innerHTML='';state.suggesting=false;} function suggestToast(regime,S,conf,sc,manual,label){ state.suggesting=true; const t=document.createElement('div');t.className='toast'; t.innerHTML=`
${icon(regime,22)}
Market shifting to ${(label||regime).toUpperCase()} Composite ${S>=0?'+':''}${S} · ${conf}% confidence · ${reasonText(regime,sc)}
`; $('toastWrap').appendChild(t); t.querySelector('[data-act="switch"]').onclick=()=>{cinematicRegime(regime);dismiss(t);}; t.querySelector('[data-act="stay"]').onclick=()=>{if(!manual)setMode('manual',true);dismiss(t);}; if(!manual)t._timer=setTimeout(()=>{if(document.body.contains(t)){cinematicRegime(regime);dismiss(t);}},6500); } function infoToast(regime,S,conf,sc,hard,label){ state.suggesting=true; const L=(label||regime).toUpperCase(); const t=document.createElement('div');t.className='toast'+(hard?' hard':''); t.innerHTML=`
${icon(hard?'bolt':regime,22)}
${hard?'Hard signal, switched to '+L:'Switched to '+L} Composite ${S>=0?'+':''}${S} · ${conf}% · ${reasonText(regime,sc)}. No orders were changed.
`; $('toastWrap').appendChild(t); t.querySelector('[data-act="ok"]').onclick=()=>dismiss(t); t.querySelector('[data-act="keep"]').onclick=()=>{setMode('manual',true);dismiss(t);}; t._timer=setTimeout(()=>{if(document.body.contains(t))dismiss(t);},7000); } function dismiss(t){if(t._timer)clearTimeout(t._timer);t.classList.add('out');setTimeout(()=>{t.remove();if(!$('toastWrap').children.length)state.suggesting=false;},300);} /* ---------- mode + engine panel ---------- */ function setMode(mode,silent){state.mode=mode;$('autoPill').classList.toggle('on',mode==='auto');renderRegimeBar(state.displayed);if(mode==='auto'&&!silent){clearToasts();recompute();}} function openEngine(o){$('engine').classList.toggle('collapsed',!o);$('engineScrim').classList.toggle('show',o);} /* ---------- simulation ---------- */ function toggleSim(){const btn=$('simRun'); if(state.simTimer){clearInterval(state.simTimer);state.simTimer=null;btn.textContent='▶ Simulate a move (demo)';btn.classList.remove('running');return;} markSimOverride(); // the random-walk demo is explicitly a what-if, not live data btn.textContent='⏸ Pause demo';btn.classList.add('running'); state.simTimer=setInterval(()=>{const walk=(id,mn,mx,st)=>{const e=$(id);e.value=clamp(+e.value+(Math.random()-0.5)*st,mn,mx);}; walk('sTrend',-100,100,16);walk('sVix',8,35,1.4);walk('sAd',0.2,3,0.18);walk('sRsi',20,80,5);walk('sPnl',-10,10,0.9);recompute();},1400);} /* ---------- presets ---------- */ const PRESETS={rally:{sTrend:88,sVix:11.5,sAd:2.6,sRsi:68,sPnl:4.5,macd:true},choppy:{sTrend:8,sVix:16,sAd:1.0,sRsi:50,sPnl:-0.5,macd:true},crash:{sTrend:-78,sVix:24,sAd:0.4,sRsi:32,sPnl:-6,macd:false,hard:true},spike:{sTrend:-32,sVix:28,sAd:0.5,sRsi:38,sPnl:-3.5,macd:false,hard:true}}; function applyPreset(name){const p=PRESETS[name];if(!p)return; markSimOverride(); // a preset is a hypothetical scenario, not the live market ['sTrend','sVix','sAd','sRsi','sPnl'].forEach(k=>{if(p[k]!=null)$(k).value=p[k];}); const mt=$('macdToggle');mt.dataset.on=String(p.macd);mt.textContent=p.macd?'Bullish ↑':'Bearish ↓'; if(p.hard)state.forceHard=true;clearToasts();recompute();} /* ---------- init ---------- */ function init(){ ['sTrend','sVix','sAd','sRsi','sPnl'].forEach(id=>$(id).addEventListener('input',()=>{markSimOverride();recompute();})); $('macdToggle').addEventListener('click',()=>{const b=$('macdToggle');const on=b.dataset.on!=='true';b.dataset.on=String(on);b.textContent=on?'Bullish ↑':'Bearish ↓';markSimOverride();recompute();}); document.querySelectorAll('[data-preset]').forEach(b=>b.addEventListener('click',()=>applyPreset(b.dataset.preset))); $('simRun').addEventListener('click',toggleSim); $('engineBtn').addEventListener('click',()=>openEngine(true)); $('engineClose').addEventListener('click',()=>openEngine(false)); $('engineScrim').addEventListener('click',()=>openEngine(false)); $('widgetBtn').addEventListener('click',()=>openWidgetGallery(true)); $('wgClose').addEventListener('click',()=>openWidgetGallery(false)); $('widgetGalleryScrim').addEventListener('click',()=>openWidgetGallery(false)); $('autoPill').addEventListener('click',()=>setMode(state.mode==='auto'?'manual':'auto')); document.querySelectorAll('[data-regime-btn]').forEach(b=>b.addEventListener('click',()=>{setMode('manual',true);clearToasts();cinematicRegime(b.dataset.regimeBtn);})); // modal close $('modalClose').addEventListener('click',closeModal); $('modalCancel').addEventListener('click',closeModal); $('modalScrim').addEventListener('click',closeModal); document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal();}); // orderbook cancel (delegated) $('panelBody').addEventListener('click',e=>{const c=e.target.closest('[data-cancel]');if(c)cancelOrder(+c.dataset.cancel);}); $('surfaceToggle').addEventListener('click',toggleSurface); // persona: header toggle switches mode; the first-run gate drives the onboarding wizard document.querySelectorAll('[data-mode-btn]').forEach(b=>b.addEventListener('click',()=>cinematicPersona(b.dataset.modeBtn))); document.querySelectorAll('#personaGate [data-persona]').forEach(b=>b.addEventListener('click',()=>onboardPick(b.dataset.persona))); const pgSkip=$('pgSkip'); if(pgSkip) pgSkip.addEventListener('click',()=>onboardPick('trader')); // onboarding wizard: step-2 controls (back / finish / copy command / retry) const pgate=$('personaGate'); if(pgate) pgate.addEventListener('click',e=>{ if(e.target.closest('[data-obback]')) obStep(1); else if(e.target.closest('[data-obfinish]')) finishOnboarding(); else if(e.target.closest('[data-obretry]')){ loadBotData().then(()=>{ if(onboarding) renderOnboardConnect(); }).catch(()=>{}); } else if(e.target.closest('[data-obcopy]')){ const c=$('obCmd'); if(c && navigator.clipboard){ navigator.clipboard.writeText(c.textContent.replace(/ /g,' ').trim()) .then(()=>quickToast('Copied','Paste it into your terminal')).catch(()=>{}); } } }); // keep the floating-mode pill aligned: the pill is sized from button widths in syncFab(), // but those widths shift when the web font swaps in (FOUT) and on resize, re-measure then. if(document.fonts&&document.fonts.ready) document.fonts.ready.then(syncFab); addEventListener('resize',syncFab); // card minimize / maximize (delegated) const term=document.querySelector('.terminal'); if(term) term.addEventListener('click',e=>{const b=e.target.closest('[data-cardbtn]');if(b){e.preventDefault();e.stopPropagation();toggleCard(b.dataset.cardkey,b.dataset.cardbtn);}}); const cscrim=$('cardScrim'); if(cscrim) cscrim.addEventListener('click',restoreMaxCard); document.addEventListener('keydown',e=>{if(e.key==='Escape')restoreMaxCard();}); initWatchlistDnD(); initResize(); initSearch(); initKeyboardNav(); wireTicker(); // ---- mount the interactive chart engine (real Kite candles via /api/candles) ---- if(window.TPChart) TPChart.mount({onTrade:tradeFromChart, onExecuteBracket:executeBracketOrder, persist:saveChart, feed:chartFeed}); // ---- restore persisted session ---- const saved=loadState(); if(saved){ if(Array.isArray(saved.watchlist)) SYMS=saved.watchlist; // rebuild the universal watchlist state.wlCustom=!!saved.wlCustom; state.selected=(typeof saved.selected==='string'&&bySym(saved.selected))?saved.selected:null; // validate vs rebuilt list state.paneW=saved.paneW||null; state.chartH=saved.chartH||null; if(saved.chart && window.TPChart){ try{ TPChart.restore(saved.chart); }catch(e){} } } if(saved&&saved.cards) state.cards=saved.cards; if(saved&&saved.ticker) state.ticker=saved.ticker; if(saved&&typeof saved.regimeCollapsed==='boolean') state.regimeCollapsed=saved.regimeCollapsed; // BUG FIX (2026-09-19): this used to force 'algo' unconditionally whenever CRYPTO_ONLY was true, // which is what kept Investing/Trading permanently unreachable even after the header toggle was // wired up - CRYPTO_ONLY means "this deployment only has crypto data," not "only Algo Studio is // Support direct deep-linking via ?mode=trading, ?persona=trader, ?tab=trading let urlPersona = null; try { const sp = new URLSearchParams(window.location.search); const pParam = (sp.get('persona') || sp.get('mode') || sp.get('tab') || '').toLowerCase(); if (pParam === 'trading' || pParam === 'trader') urlPersona = 'trader'; else if (pParam === 'investing' || pParam === 'investor') urlPersona = 'investor'; else if (pParam === 'algo' || pParam === 'studio') urlPersona = 'algo'; } catch (e) {} state.persona = urlPersona || ((saved && ['trader', 'investor', 'algo'].indexOf(saved.persona) >= 0) ? saved.persona : 'algo'); renderPlanChip(); renderExchangeChip(); // Trading's paper book and Investing's DCA plans/goals didn't survive a reload before this fix - // restore them the same validated way as everything else above, and bump ORDER_ID past any // restored order id so a newly placed order can never collide with one from a prior session. if(saved&&Array.isArray(saved.orders)&&saved.orders.length){ state.orders=saved.orders; ORDER_ID=Math.max(ORDER_ID,...saved.orders.map(o=>o.id)); } if(saved&&saved.trading) state.trading=saved.trading; if(saved&&saved.investing) state.investing=saved.investing; state.investSection=(saved&&saved.investSection)||null; state.layout=(saved&&['originals','charts','watchlist','options','futures','build'].indexOf(saved.layout)>=0)?saved.layout:'originals'; // restore the active Algo sub-tab (Monitor / Library / …) so a refresh keeps you on the page you were on, // instead of always snapping back to Marketplace (the default) when the async data loads settle. if(saved&&saved.algo&&typeof saved.algo==='object'){ const AV=['library','opportunity','risk','market','leaderboard','backtest','forward','monitor','accuracy','analytics']; if(AV.indexOf(saved.algo.view)>=0){ state.algo=state.algo||{bt:{algo:0,period:'1Y'}}; state.algo.view=saved.algo.view; if(saved.algo.exec==='live'||saved.algo.exec==='paper') state.algo.exec=saved.algo.exec; } if(saved.algo.market==='in'||saved.algo.market==='crypto'){ state.algo=state.algo||{bt:{algo:0,period:'1Y'}}; state.algo.market=saved.algo.market; } } // CRYPTO_ONLY forces state BEFORE the first setMode/applyRegime/recompute below, not after - those // calls fan out into renderAlgo() and 30+ other spots gated on state.algo.market==='crypto'. Forcing // this late (as it used to) meant the very first render of the whole chain ran against unset/legacy // state and showed the pre-pivot layout for one frame - the flash a fresh login used to hit every time. // BUG FIX (2026-09-19): this used to also force state.persona back to 'algo' here (undoing the // real saved-persona read a few lines up) and set #investHub/#deskView to display:none via // INLINE style - which, since nothing ever clears an inline style, would leave Investing // permanently stuck hidden forever after boot even after switching to it, regardless of what the // CSS for the active persona says. Visibility is fully owned by the data-persona CSS rules // (already !important, already correct per-mode) - no JS inline-style hiding needed here at all. if(CRYPTO_ONLY){ state.algo=state.algo||{}; state.algo.market='crypto'; } if(state.layout==='options')state.desk.view='chain'; else if(state.layout==='futures')state.desk.view='futures'; // restore named custom layouts (validate card keys against the live catalog) const validCard=c=>c&&canvasCatalog().some(w=>w.key===c.key); const cleanCard=c=>{const o={key:c.key,span:c.span===3?3:c.span===2?2:1}; if(c.grp==='A'||c.grp==='B')o.grp=c.grp; return o;}; // restore named custom layouts (tabs shape; validate card keys against the live catalog) state.customLayouts=(saved&&Array.isArray(saved.customLayouts))?saved.customLayouts.map(l=>({id:l.id,name:l.name,activeTab:l.activeTab, tabs:(Array.isArray(l.tabs)?l.tabs:[]).map(t=>({id:t.id,name:t.name,sync:(t.sync&&typeof t.sync==='object')?t.sync:{A:0,B:1},cards:(t.cards||[]).filter(validCard).map(cleanCard)}))})):[]; state.activeCustom=(saved&&typeof saved.activeCustom==='string')?saved.activeCustom:null; state.aiCfg=(saved&&saved.aiCfg)||null; // migrate the legacy single canvas into one named layout (one "Main" tab) if(!state.customLayouts.length && saved && Array.isArray(saved.canvas) && saved.canvas.length){ const cards=saved.canvas.filter(validCard).map(cleanCard); if(cards.length){const id=newLayoutId(),tid=newTabId(); state.customLayouts=[{id,name:'My Layout',activeTab:tid,tabs:[{id:tid,name:'Main',cards,sync:{A:0,B:1}}]}]; if(state.layout==='build')state.activeCustom=id;} } // if 'build' is active but the referenced layout is gone, fall back to a preset if(state.layout==='build' && !activeCustom()) state.layout='originals'; document.documentElement.dataset.layout=state.layout; state.widgets=(saved&&saved.widgets)||{trader:WIDGET_DEFAULTS.trader.slice(),investor:WIDGET_DEFAULTS.investor.slice()}; document.documentElement.dataset.persona=state.persona; document.documentElement.dataset.layout=state.layout; syncFab(); applyChartHeight(); setSurface(saved&&saved.surface?saved.surface:'day', true); // silent: no power-on sweep on reload updateClock(); setInterval(updateClock,1000); const startMode=(saved&&saved.mode)||'auto'; setMode(startMode,true); applyRegime(startMode==='manual'&&saved&&saved.regime?saved.regime:'bull'); recompute({silent:true}); renderHdrMarket(); if(CRYPTO_ONLY && !(saved&&saved.watchlist)){ SYMS=CRYPTO_UNIVERSE.slice(0,6).map(c=>({sym:c.tk,name:c.name,exch:'CRYPTO',type:'SPOT',key:'CRYPTO:'+c.sym,ltp:0,chg:0,live:false})); state.wlCustom=true; } if(CRYPTO_ONLY || (state.algo&&state.algo.market==='crypto')){ renderTopIndex(); loadCrypto().then(()=>{ if(state.algo.market==='crypto'){ patchCryptoTape(); applyTickerSpeed(); } }); connectCryptoWS(); } tapeLoop(); if(CRYPTO_ONLY){ loadBotData().then(()=>{ if(typeof renderAlgo==='function'&&isAlgo()) renderAlgo(); }); setInterval(()=>{ if(document.visibilityState==='visible') loadBotData().then(()=>{ if(isAlgo()) renderAlgo(); }); }, 15000); } // live crypto prices: the WebSocket drives the tape sub-second; this 5s REST poll is the FALLBACK, // firing only when the socket isn't delivering (first paint, dropped socket, WS unsupported). setInterval(()=>{ if(!((CRYPTO_ONLY || (state.algo && state.algo.market==='crypto')) && document.visibilityState==='visible')) return; if(CWS.on && Date.now()-CWS.lastMsg<8000) return; // socket is live → skip the REST poll loadCrypto().then(()=>{ patchCryptoTape(); }); }, 5000); // WATCHDOG (2026-09-20): reached production where init() threw partway through (root cause: an // unclosed /* comment a few hundred lines up had silently deleted renderEngine() and friends // from the running code, see the fix on syncSlidersFromLive's comment above), aborting boot // before it ever reached the WebSocket connect call below - the ticker froze at its first REST // fetch forever, no console error visible by the time a tab was inspected. That specific cause // is fixed now, but a silent boot-abort is a class of bug, not a one-off, and nothing previously // re-checked "is the tape actually still updating" after the fact. If data goes stale for longer // than any single legitimate gap (poll + socket hiccup) should ever allow, force a fresh // connect + fetch regardless of what CWS.on currently claims. setInterval(()=>{ if(!((CRYPTO_ONLY || (state.algo && state.algo.market==='crypto')) && document.visibilityState==='visible')) return; if(CRYPTO.t && Date.now()-CRYPTO.t<15000) return; connectCryptoWS(); loadCrypto().then(()=>{ patchCryptoTape(); }); }, 10000); // Instant refresh the moment the tab regains focus. Background tabs throttle setInterval (Chrome caps // hidden-tab timers to ~1/min), so on return the crypto tape/book can look frozen until the next tick, // pull fresh data immediately instead of waiting for it. const refreshVisible=()=>{ if(document.visibilityState!=='visible') return; if(CRYPTO_ONLY || (state.algo && state.algo.market==='crypto')){ connectCryptoWS(); // ensure the price socket is up again after the tab was hidden loadCrypto().then(()=>{ patchCryptoTape(); if(typeof renderTrading==='function' && state.persona==='trader') renderTrading(); if(typeof renderInvesting==='function' && state.persona==='investor') renderInvesting(); }); if(typeof isAlgo==='function' && isAlgo()){ const v=state.algo.view; if((v==='monitor'||v==='positions')&&typeof loadCryptoMonitor==='function') loadCryptoMonitor().then(()=>{ if(isAlgo()&&(state.algo.view==='monitor'||state.algo.view==='positions')){ if(document.querySelector('.cxm-tbl') && CRYPTOMON._sig===cryptoMonSig()) patchCryptoMon(); else renderAlgo(); } }); else if(v==='risk'&&typeof loadCryptoRisk==='function') loadCryptoRisk().then(()=>{ if(isAlgo()) renderAlgo(); }); } } else if(BOT.live){ loadTicks(); } // Indian book: pull fresh ticks on return too }; document.addEventListener('visibilitychange',refreshVisible); window.addEventListener('focus',refreshVisible); // live crypto paper book (7s poll), refresh the Monitor/Positions/Risk while the studio is scoped to Crypto setInterval(()=>{ if(!(isAlgo() && state.algo && state.algo.market==='crypto' && document.visibilityState==='visible')) return; const v=state.algo.view; if(v==='monitor'||v==='positions') loadCryptoMonitor().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&(state.algo.view==='monitor'||state.algo.view==='positions')){ // steady state → patch the ticking numbers in place (NO rebuild, no flicker); only full-render on a structural change if(document.querySelector('.cxm-tbl') && CRYPTOMON._sig===cryptoMonSig()) patchCryptoMon(); else renderAlgo(); } }); else if(v==='risk') loadCryptoRisk().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='risk') renderAlgo(); }); else if(v==='forward'||v==='accuracy') loadCryptoFwd().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&(state.algo.view==='forward'||state.algo.view==='accuracy')) renderAlgo(); }); else if(v==='analytics') loadCryptoAn().then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='analytics') renderAlgo(); }); else if(v==='accuracy') loadReadiness('crypto').then(()=>{ if(isAlgo()&&state.algo.market==='crypto'&&state.algo.view==='accuracy') renderAlgo(); }); else if(v==='analytics') loadRegimeFit('crypto'); }, 7000); mountStableCardCtls(); applyCardStates(); // first-run: onboarding wizard (pick a persona, then connect Kite) if(!(saved&&saved.persona)) startOnboarding(); } /* ---------- keyboard nav for segmented controls ---------- */ function initKeyboardNav(){ [document.querySelector('.seg')].forEach(grp=>{ if(!grp)return; grp.addEventListener('keydown',e=>{ if(e.key!=='ArrowLeft'&&e.key!=='ArrowRight')return; const btns=[...grp.querySelectorAll('button')]; const i=btns.indexOf(document.activeElement); if(i<0)return; e.preventDefault(); const ni=(i+(e.key==='ArrowRight'?1:-1)+btns.length)%btns.length; btns[ni].focus(); btns[ni].click(); }); }); } /* ---------- watchlist: click-to-select + drag reorder ---------- */ function initWatchlistDnD(){ const wl=$('wlRows'); wl.addEventListener('click',e=>{if(e.target.closest('[data-wlremove]'))return;const row=e.target.closest('.wl-row');if(row)selectSym(row.dataset.key);}); // Keyboard a11y: rows are role="button" tabindex="0", Enter/Space selects, Up/Down roves focus // (WCAG 2.1.1). Without this the watchlist was mouse-only. The remove button keeps its own focus. wl.addEventListener('keydown',e=>{ if(e.target.closest('[data-wlremove]'))return; // let the remove button handle its own keys const row=e.target.closest('.wl-row'); if(!row)return; if(e.key==='Enter'||e.key===' '){ e.preventDefault(); selectSym(row.dataset.key); return; } if(e.key==='ArrowDown'||e.key==='ArrowUp'){ e.preventDefault(); const rows=[...wl.querySelectorAll('.wl-row')], i=rows.indexOf(row); const nxt=rows[e.key==='ArrowDown'?Math.min(i+1,rows.length-1):Math.max(i-1,0)]; if(nxt)nxt.focus(); } }); wl.addEventListener('dragstart',e=>{const row=e.target.closest('.wl-row');if(!row)return;state.dragKey=row.dataset.key;row.classList.add('dragging');e.dataTransfer.effectAllowed='move';}); wl.addEventListener('dragend',e=>{const row=e.target.closest('.wl-row');if(row)row.classList.remove('dragging');wl.querySelectorAll('.drag-over').forEach(x=>x.classList.remove('drag-over'));}); wl.addEventListener('dragover',e=>{e.preventDefault();const row=e.target.closest('.wl-row');wl.querySelectorAll('.drag-over').forEach(x=>x.classList.remove('drag-over'));if(row&&row.dataset.key!==state.dragKey)row.classList.add('drag-over');}); wl.addEventListener('drop',e=>{e.preventDefault();const row=e.target.closest('.wl-row');if(!row||!state.dragKey)return; const dragged=byKey(state.dragKey), target=byKey(row.dataset.key); if(dragged&&target&&dragged!==target){ SYMS.splice(SYMS.indexOf(dragged),1); SYMS.splice(SYMS.indexOf(target),0,dragged); } state.wlCustom=true; state.dragKey=null; renderWatchlist(state.displayed); saveState();}); // segment-filter tabs (My Watchlist / F&O / Indices) document.querySelectorAll('.pane-left .wl-tab').forEach((b,i)=>b.onclick=()=>{ document.querySelectorAll('.pane-left .wl-tab').forEach(x=>x.classList.remove('active')); b.classList.add('active'); state.wlTab=['all','fno','idx'][i]||'all'; renderWatchlist(state.displayed);}); // "Add scrip…" box = universal instrument search scoped to the watchlist initAddScrip(); } /* ---------- resizable panes + chart ---------- */ function initResize(){ let active=null,sx=0,sl=0,sr=0; const startPW=()=>{if(!state.paneW){const l=document.querySelector('.pane-left').getBoundingClientRect().width;const rp=document.querySelector('.pane-right').getBoundingClientRect().width;state.paneW={left:Math.round(l),right:Math.round(rp)};}}; const move=e=>{if(!active)return;const dx=e.clientX-sx; if(active==='left')state.paneW.left=clamp(sl+dx,240,560);else state.paneW.right=clamp(sr-dx,240,560); applyPaneWidths();}; const up=()=>{active=null;document.body.classList.remove('resizing');window.removeEventListener('mousemove',move);window.removeEventListener('mouseup',up);saveState();}; document.querySelectorAll('[data-resize]').forEach(h=>{ h.addEventListener('mousedown',e=>{e.preventDefault();startPW();active=h.dataset.resize;sx=e.clientX;sl=state.paneW.left;sr=state.paneW.right;document.body.classList.add('resizing');window.addEventListener('mousemove',move);window.addEventListener('mouseup',up);}); h.addEventListener('dblclick',()=>{state.paneW=null;applyPaneWidths();saveState();}); }); const ch=$('chartResize'), card=$('chartCard'); if(ch&&card){let cy=0,sh=0,drag=false; const cmove=e=>{if(!drag)return;state.chartH=clamp(sh+(e.clientY-cy),300,680);card.style.height=state.chartH+'px';}; const cup=()=>{drag=false;document.body.classList.remove('resizing');window.removeEventListener('mousemove',cmove);window.removeEventListener('mouseup',cup);saveState();}; ch.addEventListener('mousedown',e=>{e.preventDefault();drag=true;cy=e.clientY;sh=card.getBoundingClientRect().height;document.body.classList.add('resizing');window.addEventListener('mousemove',cmove);window.addEventListener('mouseup',cup);}); ch.addEventListener('dblclick',()=>{state.chartH=null;card.style.height='';saveState();}); } applyChartHeight(); } /* ===== Universal instrument search (ALL segments), shared by the top-bar search and the watchlist "Add scrip" box. Hits /api/instruments (the full 128k Kite master), debounced + abortable, keyboard-navigable. Picking an instrument adds it to the watchlist. ===== */ function instRow(r){ const exp=r.expiry?' · '+new Date(r.expiry).toLocaleDateString('en-IN',{day:'2-digit',month:'short'}):''; const strike=(r.type==='CE'||r.type==='PE')&&r.strike?(' '+Math.round(r.strike)):''; const cls2=(''+(r.type||'')).toLowerCase(); return `
${esc(r.ts)}${esc(r.type||'')} ${esc(r.name||r.ts)} · ${esc(r.exch)}${esc(strike)}${esc(exp)}
${icon('plus',13)}
`; } function wireInstSearch(input, box, onPick){ let t=0, ctrl=null, items=[], active=-1, cache={}; const close=()=>{box.classList.remove('show');box.innerHTML='';active=-1;items=[];}; const draw=()=>{ box.innerHTML=items.length?items.map(instRow).join(''):'
No instruments found
'; box.classList.add('show'); [...box.querySelectorAll('.sr-item')].forEach((el,i)=>{ el.classList.toggle('active',i===active); el.onmousedown=e=>{e.preventDefault();pick(items[i]);}; }); }; const pick=r=>{ if(!r)return; onPick(r); input.value=''; close(); input.blur(); }; const run=q=>{ q=q.trim(); if(q.length<2){close();return;} // CRYPTO_ONLY: search the real, bounded crypto universe locally, no network round-trip needed. // This used to always call the old Kite/NSE instrument-search API regardless of market, so // typing "BTC" (or any crypto symbol) against a crypto-only build returned nothing, found via // an adversarial QA pass - the search bar's own placeholder says "e.g. BTCUSDT" but the search // itself could never find it. if(CRYPTO_ONLY){ const ql=q.toLowerCase(); items=CRYPTO_UNIVERSE.filter(c=>c.tk.toLowerCase().includes(ql)||c.sym.toLowerCase().includes(ql)||c.name.toLowerCase().includes(ql)) .map(c=>({key:'CRYPTO:'+c.sym, ts:c.tk, sym:c.sym, name:c.name, exch:'CRYPTO', type:'SPOT'})); active=items.length?0:-1; draw(); return; } if(cache[q]){items=cache[q];active=items.length?0:-1;draw();return;} if(ctrl)ctrl.abort(); ctrl=new AbortController(); fetch(`${BOT_API}/api/instruments?q=${encodeURIComponent(q)}&limit=30`,{signal:ctrl.signal}) .then(r=>r.json()).then(d=>{items=(d&&d.results)||[];cache[q]=items;active=items.length?0:-1;draw();}) .catch(err=>{ if(!(err&&err.name==='AbortError')){items=[];draw();} }); }; input.addEventListener('input',()=>{clearTimeout(t);t=setTimeout(()=>run(input.value),170);}); // debounce input.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();active=Math.min(active+1,items.length-1);draw();} else if(e.key==='ArrowUp'){e.preventDefault();active=Math.max(active-1,0);draw();} else if(e.key==='Enter'){if(active>=0&&items[active]){e.preventDefault();pick(items[active]);}} else if(e.key==='Escape'){input.value='';close();input.blur();}}); input.addEventListener('blur',()=>setTimeout(close,150)); } function initSearch(){ // top-bar global search → add any instrument const si=document.querySelector('.search input'), sr=$('searchResults'); if(si&&sr) wireInstSearch(si, sr, r=>addInstrument(r)); document.addEventListener('keydown',e=>{const a=document.activeElement;if(e.key==='/'&&a!==si&&a.tagName!=='INPUT'){e.preventDefault();si&&si.focus();}}); initTerminalShortcuts(); } /* ============================================================ TERMINAL AUDIO SYNTHESIZER MICRO-FEEDBACK (Web Audio API) ============================================================ */ function playAudioFeedback(type = 'click'){ try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if(!AudioCtx) return; if(!window.__ztAudioCtx) window.__ztAudioCtx = new AudioCtx(); const ctx = window.__ztAudioCtx; if(ctx.state === 'suspended') ctx.resume(); const osc = ctx.createOscillator(); const gain = ctx.createGain(); osc.connect(gain); gain.connect(ctx.destination); const now = ctx.currentTime; if(type === 'success'){ osc.type = 'sine'; osc.frequency.setValueAtTime(587.33, now); // D5 osc.frequency.setValueAtTime(880, now + 0.08); // A5 gain.gain.setValueAtTime(0.08, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35); osc.start(now); osc.stop(now + 0.35); } else if(type === 'bracket'){ osc.type = 'triangle'; osc.frequency.setValueAtTime(440, now); osc.frequency.exponentialRampToValueAtTime(880, now + 0.06); gain.gain.setValueAtTime(0.07, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); osc.start(now); osc.stop(now + 0.12); } else { osc.type = 'sine'; osc.frequency.setValueAtTime(520, now); gain.gain.setValueAtTime(0.04, now); gain.gain.exponentialRampToValueAtTime(0.001, now + 0.06); osc.start(now); osc.stop(now + 0.06); } } catch(e){} } /* ============================================================ TERMINAL KEYBOARD SHORTCUTS & MODAL (Institutional Parity) ============================================================ */ function toggleShortcutsModal(){ const existing = $('shortcutsModalOverlay'); if(existing){ existing.remove(); return; } const ov = document.createElement('div'); ov.id = 'shortcutsModalOverlay'; ov.className = 'shortcuts-overlay'; ov.innerHTML = `
${icon('command', 16)} Terminal Keyboard Shortcuts
1Investing Mode (Shield & DCA Simulator)
2Trading Mode (Confluence & R:R Brackets)
3Algo Studio (Resilience Matrix & Deploy)
BSelect Bitcoin (BTCUSDT)
ESelect Ethereum (ETHUSDT)
SSelect Solana (SOLUSDT)
FToggle Fullscreen Chart
/Focus Global Search Bar
?Toggle This Shortcuts Dialog
EscExit Fullscreen / Close Dialogs
`; document.body.appendChild(ov); ov.addEventListener('click', e => { if(e.target === ov || e.target.id === 'smCloseBtn') ov.remove(); }); } function initTerminalShortcuts(){ document.addEventListener('keydown', e => { const a = document.activeElement; const inInput = a && (['INPUT', 'TEXTAREA', 'SELECT'].includes(a.tagName) || a.isContentEditable); if(inInput) return; if(e.key === '1'){ e.preventDefault(); applyPersona('investor', {user: true}); playAudioFeedback('click'); quickToast('Mode: Investing', 'Switched to Investing & DCA Simulator workstation.'); } else if(e.key === '2'){ e.preventDefault(); applyPersona('trader', {user: true}); playAudioFeedback('click'); quickToast('Mode: Trading', 'Switched to Trading & Multi-Timeframe Confluence workstation.'); } else if(e.key === '3'){ e.preventDefault(); applyPersona('algo', {user: true}); playAudioFeedback('click'); quickToast('Mode: Algo Studio', 'Switched to Algo Studio & Walk-Forward Resilience Matrix.'); } else if(e.key === 'b' || e.key === 'B'){ e.preventDefault(); selectSym('BTCUSDT'); playAudioFeedback('click'); } else if(e.key === 'e' || e.key === 'E'){ e.preventDefault(); selectSym('ETHUSDT'); playAudioFeedback('click'); } else if(e.key === 's' || e.key === 'S'){ e.preventDefault(); selectSym('SOLUSDT'); playAudioFeedback('click'); } else if(e.key === 'f' || e.key === 'F'){ e.preventDefault(); if(window.TPChart && window.TPChart.toggleFullscreen) window.TPChart.toggleFullscreen(); } else if(e.key === '?'){ e.preventDefault(); toggleShortcutsModal(); } else if(e.key === 'Escape'){ const card = document.getElementById('chartCard'); if(card && card.classList.contains('chart-fullscreen')){ if(window.TPChart && window.TPChart.toggleFullscreen) window.TPChart.toggleFullscreen(); } const ov = $('shortcutsModalOverlay'); if(ov) ov.remove(); } }); } function initAddScrip(){ // watchlist "Add scrip…" box → add any instrument const si=document.querySelector('.wl-search input'); if(!si) return; let box=si.parentElement.querySelector('.search-results'); if(!box){ box=document.createElement('div'); box.className='search-results wl-results'; si.parentElement.appendChild(box); } wireInstSearch(si, box, r=>addInstrument(r)); } // The old in-app "pricing preview" modal (PLANS/ADDONS tiers, Zerodha-cockpit copy, Razorpay, // RIA/PMS white-label pitch) was entirely pre-pivot Indian-equity content with its own fake // tier prices in rupees, disconnected from the real crypto billing (NOWPayments, $19/mo // Founding Pro) that actually exists on /pricing and /app#pricing. That's why a previous session // made the chip just navigate to real pricing instead of faking a modal. // Session 2026-09-16: founder asked for the modal back, for real this time - studio.js's // ztOpenPricingModal() shows the genuine plans in place (real prices, real NOWPayments checkout on // "Choose Pro/Elite", same edge function /app#pricing uses) so comparing plans doesn't need a full // page navigation. studio.js loads before this script, but fall back to the real page if it's ever // missing (e.g. the local operator terminal, which never loads studio.js) rather than do nothing. function renderPlanChip(){ const el=$('planChip'); if(!el) return; el.innerHTML=`${icon('bolt',12)}Pricing`; el.title='View plans & pricing'; el.onclick=()=>{ window.ztOpenPricingModal ? window.ztOpenPricingModal() : (window.location.href='/app#pricing'); }; } // A single, obvious, always-visible entry point for "I want to trade with real money" - // previously only discoverable by already being in Trading mode or digging into Account // settings. Same window.zt* feature-detection precedent as renderPlanChip() above: on the // untouched local terminal (no studio.js, no window.ztExchange) this chip simply never shows, // consistent with Trading mode's own Live toggle already being unreachable there. function renderExchangeChip(){ const el=$('exChip'); if(!el) return; if(typeof window.ztExchange==='undefined'){ el.hidden=true; return; } if(state.headerExchange===undefined){ window.ztExchange.status().then(s=>{ state.headerExchange=s; renderExchangeChip(); }); return; // stay hidden until the first status check resolves, no flash of the wrong state } el.hidden=false; if(state.headerExchange.connected){ el.classList.add('ex-on'); el.innerHTML=`Exchange Connected`; el.title='Manage your connected exchange'; el.onclick=()=>{ window.location.href='/account'; }; } else { el.classList.remove('ex-on'); el.innerHTML=`${icon('link',12)}Connect Exchange`; el.title='Connect your own Binance account to trade with real money'; el.onclick=openConnectExchangeModal; } } function openConnectExchangeModal(){ // MONETIZATION FIX (2026-09-20): live trading is a Pro/Elite feature, enforced server-side in // exchange-connect/place-order (saas/supabase/functions/_shared/tier.mjs). Check tier first so a // free-tier user sees an upgrade prompt instead of pasting real credentials only to hit a 403 at // the very end. Pure UX convenience - if the tier bridge or pricing modal isn't available for any // reason, fall through to the form rather than hard-blocking; the server-side gate is authoritative. if(typeof window.ztExchange!=='undefined'&&typeof window.ztExchange.tier==='function'){ window.ztExchange.tier().then(t=>{ if(t==='pro'||t==='elite') openConnectExchangeModalForm(); else if(typeof window.ztOpenPricingModal==='function') window.ztOpenPricingModal(); else openConnectExchangeModalForm(); }); return; } openConnectExchangeModalForm(); } function openConnectExchangeModalForm(){ const trust=[['shield','Non-custodial'],['lock','Encrypted at rest'],['link','Trade-only key']] .map(([ic,l])=>`${icon(ic,11)}${l}`).join(''); // UX FIX (2026-09-20): both fields now get a show/hide toggle - a masked key/secret with no way // to verify what was actually pasted is a real point of friction for exactly the two hardest // values in the whole product to type or eyeball-check (64-char random strings). const eyeField=(id,ic,label,placeholder)=>`
${icon(ic,13)}
`; // UX REWORK (2026-09-20): restructured from one dense instructional paragraph into a guided // 2-step flow - the real confusion this modal risks isn't the paste itself, it's that step 1 // requires LEAVING the page to go do something on Binance before coming back to step 2, and // nothing previously signaled that explicitly. Reuses the app's own existing patterns (the // .se-ic hero-icon circle from secEmpty(), the check icon, .flow-links) rather than inventing a // new visual language just for this one modal. const checklist=[ ['API Management → Create API'], ['Check only "Enable Spot & Margin Trading" — leave "Enable Withdrawals" unchecked'], ].map(([t])=>`
  • ${icon('check',13)}${t}
  • `).join(''); flowModal({title:'Connect Binance',confirm:'Connect', body:`
    ${icon('shield',22)}

    Your key only ever places orders on your own Binance account, zengtrade never touches your funds or sees your Binance password.

    ${trust}
    1Create a trade-only key on Binance
      ${checklist}
    2Paste it here
    ${eyeField('gyokKey','link','API key','Paste your API key')} ${eyeField('gyokSecret','lock','API secret','Paste your API secret')}
    `, wire(body){ body.querySelectorAll('[data-eyefor]').forEach(btn=>{ btn.onclick=()=>{ const inp=body.querySelector('#'+btn.dataset.eyefor); const show=inp.type==='password'; inp.type=show?'text':'password'; btn.innerHTML=icon(show?'eyeOff':'eye',14); btn.setAttribute('aria-label',(show?'Hide':'Show')+(btn.dataset.eyefor==='gyokKey'?' API key':' API secret')); }; }); }, onConfirm(body){ const key=body.querySelector('#gyokKey').value.trim(), secret=body.querySelector('#gyokSecret').value.trim(); const err=body.querySelector('#gyokErr'); const showErr=msg=>{ err.textContent=msg; err.hidden=false; }; if(!key||!secret){ showErr('Enter both the API key and secret.'); return false; } // UX FIX (2026-09-20): a real Binance API key/secret is always 64 characters - catch an // obviously partial paste instantly instead of waiting on a round trip to Binance to say so. if(key.length<40||secret.length<40){ showErr("That looks too short for a real Binance API key/secret, check you copied the full value."); return false; } err.hidden=true; const cf=$('modalConfirm'); if(cf){ cf.disabled=true; cf.textContent='Connecting…'; } window.ztExchange.connect(key,secret).then(res=>{ if(res.ok&&res.data&&res.data.connected){ closeModal(); quickToast('Binance connected','You can now switch Trading mode to Live.'); state.headerExchange={connected:true,connectedAt:new Date().toISOString()}; renderExchangeChip(); if(state.trading){ state.trading.exchangeStatus=undefined; if(state.persona==='trader'&&typeof renderTrading==='function') renderTrading(); } } else { // BUG FIX (2026-09-20): this used to closeModal() unconditionally before checking // success, so a failure - even a specific, actionable one like "withdrawal permission // enabled" - wiped the form and only flashed a toast; the user had to reopen the modal // and re-paste both fields from scratch. Keep it open, show the real backend message // (exchange-connect/index.ts already returns specific, actionable errors) inline instead. if(cf){ cf.disabled=false; cf.textContent='Connect'; } showErr((res.data&&res.data.error)||'Could not connect, please check your key/secret and try again.'); } }); return false; // keep the modal open until the async call resolves either way } }); } document.addEventListener('DOMContentLoaded',init); initGlossTips(); // delegated listeners on document, safe to attach before DOMContentLoaded