Ziqo
LIVE · Polygon.io · 5 tickers
) + clusterCard(data.clusters,'$') + hotSectorsCard(data.hotSectors,'$',data.results) + pickHeroCard(building,'building','$',scannedAt) + (sprinter?pickHeroCard(sprinter,'sprinter','$',scannedAt):'') + runnersCard(runners,'$') + avoidCard(data.avoid,'$') + `
Last scan: ${scannedAt} · ${data.successful}/${data.universe} scored
High-conviction, higher-risk selections for education. Not financial advice — you decide.
`; } function markPrompt(id,kind){ const x=App.suggestions.find(s=>s.id===id);if(!x)return; const cur=App.stock(x.ticker)?.price||x.priceAtSuggestion; const label=kind==='took'?'Entry price you got? (any broker)':'Exit price you got?'; const v=prompt(label,cur.toFixed(2)); if(v===null)return; App.markSuggestion(id,kind,v); toast(kind==='took'?'Tracking your entry':'Closed — added to history'); } /* ---------------- STOCK DETAIL ---------------- */ // On-demand detail for ANY ticker (picks, runners, sector members, ETFs) — fetched live // Indian research sources — one-tap deep links to cross-check a name (Ziqo doesn't scrape these; they're for your own research) function indiaResearchLinks(t){ const sym=String(t).replace(/\.NS$|\.BO$/i,'').toUpperCase(); const enc=encodeURIComponent(sym); const links=[ ['Screener','📊',`https://www.screener.in/company/${sym}/`], ['Moneycontrol','💹',`https://www.moneycontrol.com/stocks/cptmarket/compsearchnew.php?search_str=${enc}`], ['Economic Times','📰',`https://economictimes.indiatimes.com/topic/${enc}`], ['Groww','🌱',`https://groww.in/stocks/search?query=${enc}`] ]; return `
Research ${sym} elsewhere
Cross-check fundamentals, news & screens on the sources Indian investors trust
${links.map(([name,em,url])=>` ${em}${name} `).join('')}
Opens the source in your browser. These are independent sites — Ziqo doesn't control their data.
`; } function screenLiveDetail(t){ const q=App.liveDetail; // One source of truth for a ticker's action/conviction. Search each pick set thoroughly // (results + hot-sector names + avoid) so the action shown matches what the theme page showed. // Main US scan wins over the speculative tilt, which fixes "Buy in the list, Avoid on open". const findIn=(d)=>{ if(!d)return null; const pool=[d.building,d.igniting,...(d.results||[]),...((d.avoid)||[])].filter(Boolean); let hit=pool.find(x=>x&&x.ticker===t); if(hit)return hit; if(d.hotSectors){for(const s of d.hotSectors){const nm=(s.names||[]).find(n=>n&&n.ticker===t); if(nm)return {ticker:t,action:nm.action,convictionScore:nm.conviction,stage:nm.stage,price:nm.price,themes:[s.theme]};}} return null; }; // Resolve action from the scan that matches where the user opened this — ETF tab uses the ETF // scan, India uses India, etc. — so an instrument has ONE action, never Buy here / Avoid there. const mk=App.liveDetailMarket; const order = mk==='etf' ? [App.pickDataEtf,App.pickData,App.pickDataIndia,App.pickDataCrypto,App.pickDataSpec] : mk==='crypto' ? [App.pickDataCrypto,App.pickData,App.pickDataEtf,App.pickDataIndia,App.pickDataSpec] : mk==='in' ? [App.pickDataIndia,App.pickData,App.pickDataEtf,App.pickDataCrypto,App.pickDataSpec] : [App.pickData,App.pickDataIndia,App.pickDataEtf,App.pickDataCrypto,App.pickDataSpec]; let meta=null; for(const d of order){ meta=findIn(d); if(meta)break; } // Not in the top-N scan results (e.g. AMZN)? Use the on-demand rating so research always shows one. if(!meta && App.liveScore && App.liveScore.ticker===t) meta=App.liveScore; const cur=App.liveDetailMarket==='in'?'₹':'$'; const back=``; if(App.liveDetailLoading&&!q){ return back+`
Loading ${t}…
`; } if(!q){ // No live quote, but we may still have pick meta to show if(!meta)return back+`
${t}
Couldn't load live data right now.
`; } const price=q?q.price:meta.price; const chg=q?q.changePct:null; const fmt=v=>v==null?'—':cur+Number(v).toLocaleString(undefined,{maximumFractionDigits:2}); const act=meta&&meta.action?meta.action:null; const actC=act?(/Strong Buy|Buy/.test(act)?'var(--buy)':act==='Watch'?'#B5B0FF':'var(--sell)'):'var(--ink-3)'; const info=q&&q.info; const coName=(info&&info.name)||(meta&&meta.name)||MEGA_NAMES[t]||''; const chart=q&&q.closes&&q.closes.length>3?liveChartSVG(q.closes,{'1d':'Today','1w':'Past week','6m':'6-month range'}[App.chartRange||'6m']):''; const L=meta&&meta.levels; const rng=App.chartRange||'6m'; const rangeLabel={'1d':'today','1w':'past week','6m':'last 6 months'}[rng]; const tfBtn=(r,lab)=>``; const tf=`
${tfBtn('1d','1D')}${tfBtn('1w','1W')}${tfBtn('6m','6M')}
`; const statRow=(lab,val)=>`
${lab}${val}
`; // "What this company does" — so opening RTX actually tells you what RTX is const aboutCard=(coName||info)?`
About ${coName||t}
${info&&(info.industry||info.marketCap)?`
${info.industry?`${info.industry}`:''}${info.marketCap?`Mkt cap ${cur}${fmtBig(info.marketCap*1e6)}`:''}${info.exchange?`${info.exchange}`:''}
`:''} ${info&&info.desc?`
${info.desc}
`:(info&&info.industry?`
${coName||t} operates in ${info.industry}.
`:'')} ${info&&info.weburl?`Company site ↗`:''}
`:''; return back+`
${t}
${coName?`
${coName}
`:''} ${meta&&meta.themes&&meta.themes.length?`
${meta.themes.join(' · ')}
`:meta&&meta.theme?`
${meta.theme}
`:''}
${act?`${act}`:''}
${fmt(price)} ${chg!=null?`${chg>=0?'+':''}${chg}%`:''}
${aboutCard} ${chart?`
Price · ${rangeLabel}
${App.chartLoading?'updating…':''}
${chart}${tf}
`:''}
Key data
${q?statRow('52-week high',fmt(q.high52)):''} ${q?statRow('52-week low',fmt(q.low52)):''} ${q&&q.dayHigh?statRow('Day range',`${fmt(q.dayLow)} – ${fmt(q.dayHigh)}`):''} ${q&&q.volume?statRow('Volume',fmtBig(q.volume)+' sh'):''} ${(q&&q.rsi!=null?q.rsi:(meta&&meta.rsi))!=null?statRow('RSI',(q&&q.rsi!=null?q.rsi:meta.rsi)+((q&&q.rsi!=null?q.rsi:meta.rsi)>=70?' (overbought)':(q&&q.rsi!=null?q.rsi:meta.rsi)<=35?' (oversold)':'')):''} ${meta&&meta.convictionScore!=null?statRow('Conviction',meta.convictionScore+'/100'):''} ${meta&&meta.relStr!=null?statRow('Rel. strength vs market',meta.relStr+'/100'):''}
${L?tradePlanCard(L,cur,meta&&meta.scannedAt):''} ${App.liveDetailMarket==='in'?indiaResearchLinks(t):''} ${meta&&meta.rationale&&meta.rationale.length?`
Why it's flagged
${meta.rationale.map(b=>`
${b}
`).join('')}
`:''} ${liveNewsBlock(t)} ${meta&&meta.newsTop&&meta.newsTop.length?newsRow(meta):''}
Live quote from market data; levels are from the last scan. Not financial advice — you decide.
`; } function liveNewsBlock(t){ const n=App.liveNews; if(App.liveDetailMarket==='in')return ''; if(!n)return `
📰 News around ${t}
Pulling the latest…
`; if(!n.length)return ''; const ago=(d)=>{if(!d)return '';const h=(Date.now()-new Date(d).getTime())/3600000;return h<1?'just now':h<24?Math.round(h)+'h ago':Math.round(h/24)+'d ago';}; return `
📰 News around ${t}
Recent headlines that could move it
${n.map(a=>{const title=a.title||a.headline||'';const url=a.article_url||a.url||'#';const pub=(a.publisher&&a.publisher.name)||a.source||'';const when=ago(a.published_utc||a.published||a.datetime); return `
${title}
${pub}${when?' · '+when:''}
`;}).join('')}
`; } function liveChartSVG(closes,rangeTxt){ const w=320,h=150,pad=8;const mn=Math.min(...closes),mx=Math.max(...closes);const rng=(mx-mn)||1; const X=i=>pad+(i/(closes.length-1))*(w-2*pad);const Y=v=>pad+(1-(v-mn)/rng)*(h-2*pad); const up=closes[closes.length-1]>=closes[0]; const col=up?'#00C805':'#FF5000'; // explicit hex — CSS vars can fail to resolve inside SVG → black-on-black let d='';closes.forEach((v,i)=>{d+=(i?'L':'M')+X(i).toFixed(1)+' '+Y(v).toFixed(1)+' ';}); const area=d+`L ${X(closes.length-1)} ${h-pad} L ${X(0)} ${h-pad} Z`; const lastX=X(closes.length-1),lastY=Y(closes[closes.length-1]); const hiI=closes.indexOf(mx),loI=closes.indexOf(mn); const uid='c'+Math.random().toString(36).slice(2,7); const grid=[0.25,0.5,0.75].map(g=>``).join(''); // Interactive scrub: drag a finger/mouse across to read the price at any point. // Data + geometry are stashed on the wrapper; attachChartScrub() wires the pointer events after render. const geom=JSON.stringify({closes,pad,w,h,mn,rng}); return `
$
${grid}
${rangeTxt||'range'} · drag across to scrub$${mn}$${mx}
`; } // Wire drag-to-scrub on any interactive chart currently on screen function attachChartScrub(){ document.querySelectorAll('.ichart').forEach(el=>{ if(el._scrub)return;el._scrub=true; let g;try{g=JSON.parse(el.getAttribute('data-geom'));}catch(e){return;} const svg=el.querySelector('svg'),cross=el.querySelector('.ichart-cross'),dot=el.querySelector('.ichart-cdot'), readout=el.querySelector('.ichart-readout'),priceEl=el.querySelector('.ichart-price'); if(!svg||!cross)return; const {closes,pad,w,h,mn}=g;const mx=Math.max(...closes);const rngv=(mx-mn)||1; const X=i=>pad+(i/(closes.length-1))*(w-2*pad);const Y=v=>pad+(1-(v-mn)/rngv)*(h-2*pad); const at=(clientX)=>{const r=svg.getBoundingClientRect();let frac=(clientX-r.left)/r.width;frac=Math.max(0,Math.min(1,frac)); const idx=Math.round(frac*(closes.length-1));const v=closes[idx];const x=X(idx),y=Y(v); cross.setAttribute('x1',x);cross.setAttribute('x2',x);cross.style.opacity='1'; dot.setAttribute('cx',x);dot.setAttribute('cy',y);dot.style.opacity='1'; if(priceEl)priceEl.textContent=Number(v).toLocaleString(undefined,{maximumFractionDigits:2}); if(readout)readout.style.opacity='1';}; const end=()=>{cross.style.opacity='0';dot.style.opacity='0';if(readout)readout.style.opacity='0';}; const move=e=>{const cx=e.touches?e.touches[0].clientX:e.clientX;at(cx);if(e.cancelable)e.preventDefault();}; el.addEventListener('mousedown',move);el.addEventListener('mousemove',e=>{if(e.buttons)move(e);}); el.addEventListener('mouseleave',end);el.addEventListener('mouseup',end); el.addEventListener('touchstart',move,{passive:false});el.addEventListener('touchmove',move,{passive:false}); el.addEventListener('touchend',end); }); } function screenStockDetail(s){if(!s)return'
Stock not found
';const p=s.plan,i=s.ind;const c=ACTION_COLORS[p.action]; const conv=Math.round(p.confidence/20); const dotColor=c==='buy'?'var(--buy)':c==='sell'?'var(--sell)':c==='caution'?'var(--caution)':'var(--green)'; const dots=Array.from({length:5},(_,k)=>`
`).join(''); const sigs=readableSignals(s.price,s.ind,s.news); const pts=[{lab:'Target 2',px:p.target2,c:'buy'},{lab:'Target 1',px:p.target1,c:'buy'},{lab:'Buy up to',px:p.buyHigh,c:'accent'},{lab:'Buy from',px:p.buyLow,c:'accent'},{lab:'Stop',px:p.stopLoss,c:'sell'}]; const allP=[...pts.map(x=>x.px),s.price];const mx=Math.max(...allP)*1.005,mn=Math.min(...allP)*.995;const Y=v=>(1-(v-mn)/(mx-mn))*144+10; const net=s.news.reduce((a,n)=>a+n.impact,0);const tone=net>0.3?'Net positive':net<-0.3?'Net negative':'Mixed';const tc=net>0.3?'buy':net<-0.3?'sell':'caution'; const openSig=App.suggestions.find(x=>x.ticker===s.ticker&&x.status==='open'); return `
${s.name}
${s.sector}
${p.action}
${F.p(s.price)} ${s.up?'+':''}${F.sg(s.change)} (${F.pct(s.changePct)})
Price chart
Last 66 trading days · drag to scrub
${liveChartSVG(s.cands.slice(-66).map(c=>c.cl),'66-day range')}
${p.action}
${plainThesis(s.price,s.ind,p,s.news)}
Confidence ${p.confidence}% · ${p.risk} risk · R/R ${p.rr.toFixed(1)}:1
${dots}
${openSig?`
Tracking this signal?
Mark "I took it" so we can track the algo on any broker.
`:''}
Signal breakdown
Plain-English read on each factor
${sigs.map(g=>`
${g.title}
${g.verdict}
${g.desc}
`).join('')}
Game plan
Exact prices to act on
${pts.map(x=>`
${x.lab}${F.p(x.px)}
`).join('')}
Now${F.p(s.price)}
${s.news.length?`
Latest news
Folded into the score
${tone}
${s.news.map(n=>{const ic=n.impact>0.15?'buy':n.impact<-0.15?'sell':'caution';const bl=n.impact>0.15?'Bullish':n.impact<-0.15?'Bearish':'Neutral'; const href=n.url||('https://news.google.com/search?q='+encodeURIComponent((n.h||'')+' '+(s.ticker||''))); return `
${n.h}
${bl}${n.src}· ${n.age}· Read →
`}).join('')}
Demo headlines reflect real late-May-2026 events. Live app pulls from a news API.
`:''} ${p.reStatus!=='—'?`
Re-Entry Tracker
${p.reStatus}
You sold this earlier. We're watching for the moment it's worth getting back in.
Re-Entry Zone
${F.p(p.reLow)}–${F.p(p.reHigh)}
Stop
${F.p(p.stopLoss)}
Confidence
${p.confidence}%
`:''}
Plain-English analysis — not financial advice. Not a guarantee of profit.
`; } function paperBuyPrompt(t){const sh=prompt('How many shares to paper-buy?','10');if(sh&&!isNaN(sh)&&+sh>0){App.buy(t,+sh);toast('Paper trade opened: '+sh+' '+t)}} /* ===================== CREDIT CARDS ===================== */ function screenCards(){ if(!App.wallet.length)return cardsEmpty(); return App.ccView==='optimize'?cardsOptimize():cardsWallet(); } function cardsEmpty(){ return `
💳
Build your wallet
Add the credit cards you actually own. Then for any purchase — dining, travel, groceries — Ziqo tells you exactly which card to swipe to earn the most.
How it works
${[['💳','Add your cards','Pick from popular cards or enter a custom one with your own rates.'],['🔍','Ask before you buy','Enter the amount and category — get the best card ranked instantly.'],['📊','See your coverage','A grid shows your best card for every spending category.']].map(x=>`
${x[0]}
${x[1]}
${x[2]}
`).join('')}
`; } function cardsWallet(){ const annualFees=App.wallet.reduce((a,c)=>a+(c.fee||0),0); return `
${recWidget()}
Your cards (${App.wallet.length})
${annualFees?'Annual fees: '+F.money(annualFees):'No annual fees'}
${App.wallet.map(c=>cardFace(c)).join('')}
Reward rates reflect typical published earn rates and are for guidance only. Verify current terms with your issuer. Point values are estimates of well-redeemed value.
`; } function cardFace(c){const g=c.color||['#333','#222'];const topRates=Object.entries(c.rates||{}).filter(([k,v])=>v>(c.base||1)).sort((a,b)=>b[1]-a[1]).slice(0,3); return `
${c.issuer}
${c.name}
${topRates.map(([k,v])=>`${fmtRate(v,c.currency)} ${CAT_EM[k]||''}`).join('')||`${fmtRate(c.base||1,c.currency)} flat`}
${CURRENCY_LABEL[c.currency]} · ${c.feeFirstYear===0?'$0 first yr, then '+F.money(c.fee):c.fee?F.money(c.fee)+'/yr':'No annual fee'}
`; } function fmtRate(v,currency){if(currency==='cashback')return (v%1===0?v:v.toFixed(1))+'%';return (v%1===0?v:v.toFixed(1))+'x';} // the recommendation widget (the core feature) function recWidget(){ const r=App.rec; const catOpts=CATS.map(c=>``).join(''); let resultHtml=''; if(r.result&&r.result.length){ const top=r.result[0]; const what=r.parsed?purchaseLabel(r.parsed):CAT_LABEL[r.cat]; const portalCat=['hotels','travel'].includes(r.cat); const portalToggle=portalCat?`
`:''; const portalNote=portalCat?`
${r.viaPortal?'Showing travel-portal rates — these apply only if you book through the card’s own portal (Capital One Travel, Chase Travel, Citi Travel…).':'Showing rates for paying direct at the hotel/airline. Cards like Venture X earn their headline hotel rate only via their travel portal — tap “Via travel portal” if you’ll book that way.'}${top.portalOnly?' · Your top pick here needs a portal booking.':''}
`:''; const sub=r.parsed ? `${fmtRate(top.rate,top.currency)} ${CURRENCY_LABEL[top.currency]} on ${F.money(r.amount)} · ${what}` : `${fmtRate(top.rate,top.currency)} ${CURRENCY_LABEL[top.currency]} on ${F.money(+r.amount||0)} ${CAT_LABEL[r.cat].toLowerCase()}`; resultHtml=`
Best card for this
${r.parsed?`
${CAT_EM[r.parsed.cat]} ${what}${F.money(r.amount)}${r.parsed.place?`📍 ${r.parsed.place}`:''}
`:''} ${portalToggle}
Use this card · earn
${F.money(top.value)}
${sub}
${r.result.map((x,idx)=>pickRow(x,idx,r)).join('')} ${r.result.length>1?`
Best card earns ${F.money(r.result[0].value-r.result[r.result.length-1].value)} more than your worst pick here.
`:''} ${portalNote}
`; } else if(r.query&&r.parsed===null){ resultHtml=`
Couldn’t read that — try the manual picker below, or phrasing like “flight to Tokyo” or “dinner $80”.
`; } const examples=['Flight to Tokyo','Hotel in Paris $1200','Dinner tonight $80','Groceries','Uber to airport','Amazon order $150']; return `
What are you buying?
Say it like a human — we'll find your best swipe
🔍 ${(r.query||'')?``:''}
${examples.map(e=>`
${e}
`).join('')}
⚙︎ Enter amount & category manually
${resultHtml}
`; } function pickRow(x,idx,r){const g=x.card.color||['#333','#222'];const isTop=idx===0; return `
${isTop?'
BEST
':''}
${x.card.name}
${fmtRate(x.rate,x.currency)} · ${CURRENCY_LABEL[x.currency]}${x.card.fee?'':' · no fee'}
${F.money(x.value)}
back
`; } function cardRecommender(){ const spend=App.annualSpend||{}; const SC=[['dining','\ud83c\udf7d\ufe0f','Dining'],['groceries','\ud83d\uded2','Groceries'],['travel','\u2708\ufe0f','Travel'],['gas','\u26fd','Gas'],['online','\ud83d\udecd\ufe0f','Online'],['other','\ud83d\udcb3','Everything else']]; const wallet=App.wallet; const valFor=(cards)=>{let t=0;for(const [cat] of SC){const amt=+spend[cat]||0;if(!amt)continue;let best=0;for(const c of cards){const v=cardValuePerDollar(c,cat,false);if(v>best)best=v;}t+=best/100*amt;}return t;}; const feesOf=(cards)=>cards.reduce((a,c)=>a+(c.fee||0),0); const curVal=valFor(wallet), curNet=curVal-feesOf(wallet); const ownedIds=new Set(wallet.map(c=>c.id).filter(Boolean)); // Cards worth ADDING — personal cards not owned, ranked by extra rewards net of their annual fee. const adds=CARD_CATALOG.filter(c=>!c.business&&!ownedIds.has(c.id)).map(c=>{ const nv=valFor(wallet.concat([c])); return {card:c, extra:nv-curVal, gain:(nv-curVal)-(c.fee||0)}; }).filter(x=>x.gain>1).sort((a,b)=>b.gain-a.gain).slice(0,3); // Cards worth CLOSING — owned fee cards whose UNIQUE rewards don't cover their fee. const closes=wallet.filter(c=>(c.fee||0)>0).map(c=>{ const uniqueVal=curVal-valFor(wallet.filter(x=>x!==c)); return {card:c, uniqueVal, fee:c.fee||0, net:uniqueVal-(c.fee||0)}; }).filter(x=>x.net<-1).sort((a,b)=>a.net-b.net); const inputs=SC.map(([k,em,lab])=>`
$
`).join(''); const addBlock = adds.length ? adds.map((x,i)=>{ const c=x.card; const fy=c.feeFirstYear===0&&c.fee?` ($0 first year, then ${F.money(c.fee)})`:''; return `
${i===0?'\ud83c\udfc6 ':''}${c.name} ${c.issuer}
+${F.money(Math.round(x.extra))}/yr rewards \u00b7 fee ${c.fee?F.money(c.fee):'\u00240'}${fy}
+${F.money(Math.round(x.gain))}
net/yr
`; }).join('') : `
Your wallet already covers your spend well — no new card beats its annual fee at these numbers. Nice.
`; const closeBlock = closes.length ? `
\u2702\ufe0f Worth a second look
These charge more in fees than the unique rewards they add over your other cards
${closes.map(x=>`
${x.card.name}
${F.money(x.fee)} fee vs ~${F.money(Math.round(x.uniqueVal))}/yr of rewards only it earns you
${F.money(Math.round(x.net))}
net/yr
`).join('')}
Keep one if you value its perks (lounge, credits, status) beyond pure rewards — those aren't counted here.
` : ''; return `
\ud83e\udde0 Smarter wallet
Tell us roughly what you spend a year — we'll find the card worth adding and flag any not pulling their weight.
${inputs}
Your wallet now earns~${F.money(Math.round(curNet))}/yr net of fees
\ud83d\udcb3 Worth applying for
Ranked by extra rewards after the annual fee, at your spend
${addBlock}
${closeBlock}
Rewards use published earn rates \u00d7 typical point values, paying direct (not portal). Guidance only \u2014 sign-up bonuses and perks aren't included. Verify terms with the issuer.
`; } function cardsOptimize(){ const best=bestPerCategory(App.wallet); return `
${cardRecommender()}
Your coverage map
Your sharpest card for every kind of spend
${CATS.map(cat=>{const b=best[cat.id]; return `
${cat.em}
${cat.label}
${b?`
${b.card.name}
`:''}
${b?`
${fmtRate(b.rate,b.currency)}
${(b.vpd).toFixed(1)}¢ / $1
`:''}
`;}).join('')}
Spend simulator
Rough yearly rewards if you always use your best card
${spendSim()}
`; } function spendSim(){ // typical monthly spend profile (editable feel via fixed sensible defaults) const monthly={dining:400,groceries:600,travel:300,gas:150,online:350,transit:80,streaming:50,drugstore:40,hotels:120,other:500}; const best=bestPerCategory(App.wallet);let totalYear=0,totalSpend=0; const rows=CATS.map(cat=>{const spend=monthly[cat.id]||0;totalSpend+=spend*12;const b=best[cat.id]; const yearVal=b?(b.vpd/100*spend*12):0;totalYear+=yearVal; return `
${cat.em}
${cat.label}
${F.money(spend)}/mo${b?' · '+b.card.name:''}
${F.money(yearVal)}
`;}).join(''); const fees=App.wallet.reduce((a,c)=>a+(c.fee||0),0); return `${rows}
Rewards / year${F.money(totalYear)}
Annual fees${fees?'−'+F.money(fees):'$0.00'}
Net value / year${F.money(totalYear-fees)}
Based on a sample U.S. spending profile (${F.money(totalSpend)}/yr). Your actual rewards depend on your real spending and redemption choices.
`; } /* ---------------- CARD DETAIL ---------------- */ function screenCardDetail(uid){const c=App.wallet.find(x=>x.uid===uid);if(!c)return'
Card not found
';const g=c.color||['#333','#222']; const rateRows=CATS.map(cat=>{const rate=cardRate(c,cat.id);const isBonus=rate>(c.base||1); return `
${cat.em}${cat.label}
${fmtRate(rate,c.currency)}${isBonus?'':' '}
`;}).join(''); const topPickSelect=c.topPick?`
Your 5% category
This card earns 5% on one category you choose
`:''; return `
${c.issuer}
${c.name}
•••• •••• •••• ${String(1000+Math.floor((hashStr(c.uid)%9000))).slice(-4)}
${CURRENCY_LABEL[c.currency]}
${c.fee?F.money(c.fee)+' / yr':'No annual fee'} · ${c.network}
${c.notes?``:''} ${topPickSelect}
Reward rates by category
Highlighted = bonus categories
${rateRows}
1 ${c.currency==='cashback'?'point':CURRENCY_LABEL[c.currency].split(' ')[0]} ≈${(PT_VALUE[c.currency]||1).toFixed(1)}¢
Estimated value when redeemed well. Cash back is always 1¢; transferable points can be worth more through travel partners.
`; } /* ---------------- MODALS ---------------- */ function renderModal(){const root=document.getElementById('modalRoot');if(!root)return; if(!App.modal){root.innerHTML='';return;} if(App.modal.name==='addCard')root.innerHTML=modalAddCard(); else if(App.modal.name==='customCard')root.innerHTML=modalCustomCard(); else if(App.modal.name==='playbook')root.innerHTML=modalPlaybook(); else if(App.modal.name==='indexInfo')root.innerHTML=modalIndexInfo(App.modal.arg); else if(App.modal.name==='metalInfo')root.innerHTML=modalMetalInfo(App.modal.arg); } function modalIndexInfo(i){ const ix=App.indicesData&&App.indicesData.indices[i];if(!ix)return ''; const st=marketStatus(ix); const yurl=ix.q?`https://finance.yahoo.com/quote/${encodeURIComponent(ix.q)}`:null; const c=ix.changePct>=0?'var(--buy)':'var(--sell)'; const row=(l,v)=>`
${l}${v}
`; return ``; } function modalMetalInfo(which){ const m=App.metalsData;const obj=m&&m[which];if(!obj)return ''; const isGold=which==='gold';const unit=isGold?'10g':'kg';const price=isGold?obj.inrPer10g:obj.inrPerKg; const tc=obj.trend==='rising'?'var(--buy)':obj.trend==='falling'?'var(--sell)':'var(--caution)'; const fmt=v=>'₹'+Number(v).toLocaleString('en-IN'); const row=(l,v)=>`
${l}${v}
`; const yurl=`https://finance.yahoo.com/quote/${isGold?'GC%3DF':'SI%3DF'}`; return ``; } function modalPlaybook(){const pb=App.playbook; const seg=(key,opts)=>`
${opts.map(o=>``).join('')}
`; const alertDefs=[['strongBuy','Strong Buy setups','When a stock hits the highest-conviction buy zone'], ['sellTrim','Sell / Trim on my holdings','Only for stocks you watch or own'], ['reEntry','Re-entry windows','When something you sold is worth buying back'], ['favoriteMove','Favorite stock moves','Big moves or signals on stocks you marked as favorites'], ['newsShock','Market-moving news','Major headlines on your stocks']]; const allTickers=App.stocks.map(s=>s.ticker); const favs=pb.favorites||[]; return ``; } function catalogTopRates(c){ const rates=c.rates||{}; const top=Object.entries(rates).filter(([k,v])=>v>(c.base||1)).sort((a,b)=>b[1]-a[1]).slice(0,2) .map(([k,v])=>fmtRate(v,c.currency)+' '+catLabelOf(k).split(' ')[0].toLowerCase()).join(' · '); return top||(fmtRate(c.base,c.currency)+' flat'); } function catalogItemHtml(c,owned){const g=c.color;const mark=owned.has(c.id)?'✓':'+';const col=owned.has(c.id)?'buy':'ai'; return '
' +'
' +'
'+c.name+'
' +'
'+catalogTopRates(c)+' · '+(c.fee?F.money(c.fee)+'/yr':'No fee')+'
' +'
'+mark+'
'; } function modalAddCard(){ const byIssuer={};CARD_CATALOG.forEach(c=>{(byIssuer[c.issuer]=byIssuer[c.issuer]||[]).push(c)}); const owned=new Set(App.wallet.map(c=>c.id)); const groups=Object.entries(byIssuer).map(([iss,cards])=> `
${iss}
` + cards.map(c=>catalogItemHtml(c,owned)).join('')).join(''); return ``; } function modalCustomCard(){ const curOpts=Object.keys(CURRENCY_LABEL).map(k=>``).join(''); const rateInputs=CATS.map(c=>`
${c.em} ${c.label}
`).join(''); return ``; } function submitCustomCard(){const get=id=>{const e=document.getElementById(id);return e?e.value:''}; const rates={};CATS.forEach(c=>{const v=get('cr_'+c.id);if(v!=='')rates[c.id]=v;}); App.addCustomCard({name:get('cc_name'),issuer:get('cc_issuer'),fee:get('cc_fee'),currency:get('cc_currency'),base:get('cc_base'),rates});} /* ---------------- canvas ---------------- */ function applyTilt(){document.querySelectorAll('[data-tilt]').forEach(el=>{ if(el._tilt)return;el._tilt=true; const reset=()=>{el.style.transform='';const sh=el.querySelector('.cc-shine');if(sh)sh.style.opacity='';}; const move=(cx,cy)=>{const r=el.getBoundingClientRect();const px=(cx-r.left)/r.width-.5,py=(cy-r.top)/r.height-.5; el.style.transform=`rotateY(${px*16}deg) rotateX(${-py*16}deg) translateZ(8px)`; const sh=el.querySelector('.cc-shine');if(sh){sh.style.opacity='1';sh.style.background=`linear-gradient(${90+px*60}deg,transparent 28%,rgba(255,255,255,.3) ${46+py*10}%,transparent 62%)`;}}; el.addEventListener('pointermove',e=>move(e.clientX,e.clientY)); el.addEventListener('pointerleave',reset); el.addEventListener('pointerdown',e=>move(e.clientX,e.clientY)); })} function drawSparks(){document.querySelectorAll('[data-spark]').forEach(cv=>{const s=App.stock(cv.dataset.spark);if(!s)return; const data=s.cands.slice(-40).map(c=>c.cl);cv.width=124;cv.height=68;const ctx=cv.getContext('2d');ctx.scale(2,2); const mn=Math.min(...data),mx=Math.max(...data),col=s.up?C('buy'):C('sell'); ctx.beginPath();data.forEach((v,idx)=>{const x=idx/(data.length-1)*62,y=32-((v-mn)/(mx-mn||1))*28;idx?ctx.lineTo(x,y):ctx.moveTo(x,y)}); ctx.strokeStyle=col;ctx.lineWidth=1.6;ctx.stroke();})} function drawDetailChart(){const cv=document.getElementById('detailChart');if(!cv)return;const s=App.stock(App.detail);if(!s)return; const cands=s.cands.slice(-66);const dpr=2,W=cv.clientWidth||300,H=200;cv.width=W*dpr;cv.height=H*dpr;const ctx=cv.getContext('2d');ctx.scale(dpr,dpr); const lows=cands.map(c=>c.l),highs=cands.map(c=>c.h);const mn=Math.min(...lows)*.99,mx=Math.max(...highs)*1.01;const Y=v=>H-((v-mn)/(mx-mn))*H;const bw=Math.max(2,Math.min(8,W/cands.length*.6)); cands.forEach((c,idx)=>{const x=idx/(cands.length-1)*(W-bw)+bw/2;const col=c.cl>=c.o?C('buy'):C('sell'); ctx.strokeStyle=col;ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(x,Y(c.h));ctx.lineTo(x,Y(c.l));ctx.stroke(); ctx.fillStyle=col;const top=Y(Math.max(c.o,c.cl)),bot=Y(Math.min(c.o,c.cl));ctx.fillRect(x-bw/2,top,bw,Math.max(bot-top,1))}); const cl=cands.map(c=>c.cl);const drawMA=(per,col)=>{ctx.strokeStyle=col;ctx.lineWidth=1.5;ctx.beginPath();let first=true; cands.forEach((c,idx)=>{if(idxa+b,0)/per;const x=idx/(cands.length-1)*(W-bw)+bw/2;first?(ctx.moveTo(x,Y(m)),first=false):ctx.lineTo(x,Y(m))});ctx.stroke()}; drawMA(20,C('blue'));drawMA(50,C('ai')); const vc=document.getElementById('volChart');if(vc){const VW=vc.clientWidth||300,VH=46;vc.width=VW*dpr;vc.height=VH*dpr;const vx=vc.getContext('2d');vx.scale(dpr,dpr); const mxv=Math.max(...cands.map(c=>c.v));cands.forEach((c,idx)=>{const x=idx/(cands.length-1)*(VW-bw)+bw/2;const bh=(c.v/mxv)*VH;vx.fillStyle=(c.cl>=c.o?C('buy'):C('sell'))+'80';vx.fillRect(x-bw/2,VH-bh,bw,bh)})}} App.init();