Luxury Travel
Be Vvip
Luxury Ski Hotels
Aspen Vacation Rentals
Team
Contact
Menu
Street Address
City, State, Zip
Phone Number
Aspen's Luxury Travel Agency
Your Custom Text Here
Luxury Travel
Be Vvip
Luxury Ski Hotels
Aspen Vacation Rentals
Team
Contact
({"&":"&","<":"<",">":">","\\"":""","'":"'"}[c]))} function uid(){return Math.random().toString(36).slice(2,9)} function n2(v,d){return isFinite(v)?Number(v).toLocaleString(undefined,{maximumFractionDigits:d??2}):"-"} // --- State --- var DEFAULT = { settings:{fiat:"usd",refreshSec:60,manualPriceMode:true}, portfolio:[ {id:"bitcoin",symbol:"BTC",units:0.5,cost_basis_total:15000,trailing:{enabled:true,trailPct:25,highest:0}}, {id:"ethereum",symbol:"ETH",units:6,cost_basis_total:12000,trailing:{enabled:false,trailPct:25,highest:0}}, {id:"solana",symbol:"SOL",units:120,cost_basis_total:10000,trailing:{enabled:false,trailPct:30,highest:0}} ], customPrices:{}, ladders:{ bitcoin:[{id:uid(),type:"percent_gain",value:100,sell_pct:10,note:"Double from basis",done:false}], ethereum:[{id:uid(),type:"price_above",value:6000,sell_pct:10,note:"Round target",done:false}], solana:[{id:uid(),type:"price_above",value:400,sell_pct:10,note:"Psych level",done:false}] }, dat:[], // {ticker,shares,price,marketCap,btc,eth,otherUSD,targets:[{id,type,value,sell_pct,done}]} activity:[] }; var S = load(); var prices = {}; // {id:{usd,chg}} var active = "dashboard"; var timer=null; function load(){ try{ var r=localStorage.getItem("cryptoExit_v1"); return r?Object.assign(structuredClone(DEFAULT), JSON.parse(r)):structuredClone(DEFAULT);}catch(e){return structuredClone(DEFAULT)}} function save(){ localStorage.setItem("cryptoExit_v1", JSON.stringify(S)); } // --- Utils --- function cpu(a){return a.units? (a.cost_basis_total/a.units):0} function price(id){ return S.settings.manualPriceMode ? Number(S.customPrices[id]||0)||0 : (prices[id]&&prices[id].usd)||0 } function value(a){return price(a.id)*a.units} function pval(){return S.portfolio.reduce((t,a)=>t+value(a),0)} // --- View --- var tabs=[["dashboard","Dashboard"],["portfolio","Portfolio"],["ladders","Ladders"],["dat","DAT & mNAV"],["settings","Settings"],["help","Help"]]; function renderTabs(){ var t=document.getElementById("tabs"); t.innerHTML=tabs.map(([id,label])=>'
'+label+"
").join(""); [].forEach.call(t.querySelectorAll(".tab"),b=>b.onclick=function(){active=b.dataset.id; render()}); } function render(){ renderTabs(); var v=document.getElementById("view"); if(active==="dashboard") v.innerHTML=viewDash(); if(active==="portfolio") v.innerHTML=viewPortfolio(); if(active==="ladders") v.innerHTML=viewLadders(); if(active==="dat") v.innerHTML=viewDAT(); if(active==="settings") v.innerHTML=viewSettings(); if(active==="help") v.innerHTML=viewHelp(); wire(); } function viewDash(){ return '
'+ '
Overview
'+ '
Portfolio:
$'+n2(pval(),2)+'
Assets:
'+S.portfolio.length+ '
Mode:
'+(S.settings.manualPriceMode?"Manual":"Live")+'
'+ '
Asset
Price
Units
Value
Basis/Unit
P/L
'+ S.portfolio.map(a=>{ var px=price(a.id), val=value(a), basis=cpu(a), pl=val-a.cost_basis_total; return '
'+esc(a.symbol)+'
$'+n2(px,2)+'
'+n2(a.units,6)+'
$'+n2(val,2)+'
$'+n2(basis,2)+'
'+(pl>=0?'
':'
')+'$'+n2(pl,2)+'
'; }).join("")+'
'+ '
Triggered & Pending
'+ '
Check Now
Export
Import
'+ '
Status
Message
Action
'+pendingRows()+'
'+ '
'; } function pendingRows(){ var rows=[]; S.portfolio.forEach(a=>{ var arr=S.ladders[a.id]||[], px=price(a.id), basis=cpu(a); arr.forEach(s=>{ if(s.done) return; var hit=false,rule=""; if(s.type==="price_above"){ hit=px>=s.value; rule="Price ≥ $"+n2(s.value,2); } if(s.type==="percent_gain"){ var g=basis?((px/basis-1)*100):0; hit=g>=s.value; rule="Gain ≥ "+n2(s.value,0)+"% (now "+n2(g,1)+"%)"; } rows.push("
"+(hit?'
HIT
':'
Pending
')+ "
"+esc(a.symbol)+"
— "+rule+" →
Sell "+n2(s.sell_pct,2)+"%
"+(s.note?'
('+esc(s.note)+")
":"")+ '
'+(hit?'
Mark Done
':"")+"
"); }); if(a.trailing && a.trailing.enabled && a.trailing.highest>0){ var th=a.trailing.highest*(1-(a.trailing.trailPct||25)/100), hitT=price(a.id)<=th && price(a.id)>0; rows.push("
"+(hitT?'
SELL
':'
Guard
')+ "
"+esc(a.symbol)+"
— Trailing stop $"+n2(th)+" (trail "+(a.trailing.trailPct||25)+"% from $"+n2(a.trailing.highest)+")
"+ (hitT?'
Reset High
':"")+"
"); } }); // DAT targets (simple) S.dat.forEach(d=>{ var btc=prices.bitcoin?prices.bitcoin.usd:0, eth=prices.ethereum?prices.ethereum.usd:0; var treas=(d.btc||0)*btc+(d.eth||0)*eth+(d.otherUSD||0); var mnav=treas>0?(d.marketCap||0)/treas:0; (d.targets||[]).forEach(t=>{ if(t.done)return; var cond=t.type==="mnav_above"? mnav>=t.value : (mnav>0 && mnav<=t.value); rows.push("
"+(cond?'
HIT
':'
Pending
')+ "
"+esc(d.ticker)+"
— mNAV "+(mnav?mnav.toFixed(2):"-")+" "+(t.type==="mnav_above"?"≥":"≤")+" "+t.value+"x → Sell "+t.sell_pct+"%
"+ '
'+(cond?'
Mark Done
':"")+"
"); }); }); return rows.join("")||'
No pending items.
'; } function viewPortfolio(){ return '
'+ '
Your Holdings
'+ '
Symbol
CoinGecko ID
Units
Cost Basis Total $
Price
Value
'+ S.portfolio.map((a,i)=>'
'+ '
'+ '
'+ '
'+ '
'+ '
$'+n2(price(a.id),2)+'
$'+n2(value(a),2)+'
'+ '
✕
'+ '
'+ '
Trailing stop
'+ '
Trail %
'+ '
Highest: $'+n2((a.trailing&&a.trailing.highest)||0)+'
'+ '
Reset Highest
'+ '
').join("")+ '
'+ '
Add Asset
'+ '
'+ '
Manual Price Mode
'+ '
Use manual prices only
'+ '
'+ '
Asset
Manual Price ($)
'+ S.portfolio.map(a=>'
'+esc(a.symbol)+'
('+esc(a.id)+')
').join("")+ '
'+ '
Recalculate
'+ '
Enable live prices later in Settings if your template allows fetch.
'+ '
'; } function viewLadders(){ return '
Profit Steps
'+ S.portfolio.map(a=>{ var ladd=S.ladders[a.id]||[], basis=cpu(a); return '
'+esc(a.symbol)+'
— Basis/Unit $'+n2(basis,2)+'
'+ '
Type
Target
Sell %
Note
Status
'+ ladd.map(s=>{ var hit = isHit(a,s); return "
"+ '
Price Above
% Gain
'+ '
'+ '
'+ '
'+ '
'+(s.done?'
Done
':(hit?'
HIT
':'
Pending
'))+'
'+ '
'+(!s.done && hit?'
Mark Done
':"")+'
✕
'+ "
"; }).join("")+ '
Price Above
% Gain
'+ '
'+ '
'+ '
'+ '
Add Step
'+ "
"; }).join("")+'
'; } function isHit(a,s){ var px=price(a.id), basis=cpu(a); if(s.type==="price_above") return px>0 && px>=Number(s.value); if(s.type==="percent_gain"){ var g=basis?((px/basis-1)*100):0; return px>0 && g>=Number(s.value); } return false; } function viewDAT(){ return '
'+ '
DAT / mNAV (manual)
'+ '
Ticker
Shares
Price
Market Cap
BTC
ETH
Other $
mNAV
'+ (S.dat.length? S.dat.map((d,i)=>{ var btc=prices.bitcoin?prices.bitcoin.usd:0, eth=prices.ethereum?prices.ethereum.usd:0, tv=(d.btc||0)*btc+(d.eth||0)*eth+(d.otherUSD||0), mnav=tv>0?(d.marketCap||0)/tv:0; return '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+ '
'+(mnav?mnav.toFixed(2):"-")+'x
'+ '
✕
'+ '
'; }).join("") : '
Add DAT tickers below.
')+ '
'+ '
Add DAT
'+ '
'+ '
Notes
mNAV = Market Cap ÷ (BTC×price + ETH×price + other USD). Prices come from Live mode if enabled, else 0.
'+ '
'; } function viewSettings(){ return '
'+ '
App Settings
'+ '
Refresh (sec)
'+ '
Fiat
USD
'+ '
Factory Reset
Copy JSON
'+ '
Manual mode avoids template CSP issues. Turn on Live prices below if allowed.
'+ '
Data
Export
Import
'+ '
Enable Live Prices (CoinGecko)
'+ '
If nothing updates after enabling, your template blocks fetch; keep Manual ON.
'+ '
'; } function viewHelp(){ return '
'+ '
Quick Start
Set coins, units, and cost basis in
Portfolio
.
Add
Price Above
or
% Gain
steps in
Ladders
.
Use
Dashboard
to see hits and mark steps done.
'+ '
Troubleshooting
This iframe version runs even if inline scripts are blocked.
Leave Manual prices ON if live fetch is restricted.
'+ '
'; } // --- Wire / Events --- function wire(){ var v=document.getElementById("view"); var e=v.querySelector("#check"); if(e) e.onclick=function(){ render(); }; var exp=v.querySelector("#exp"); if(exp) exp.onclick=exportJSON; var imp=v.querySelector("#imp"); if(imp) imp.onchange=importJSON; // portfolio [].forEach.call(v.querySelectorAll(".pf"), inp=>{ inp.onchange=function(){ var i=+inp.dataset.i,k=inp.dataset.k,val=inp.value; if(k==="units"||k==="cost_basis_total") val=Number(val||0); S.portfolio[i][k]=val; S.portfolio[i].trailing=S.portfolio[i].trailing||{enabled:false,trailPct:25,highest:0}; save(); render(); }; }); [].forEach.call(v.querySelectorAll(".trail-en"), cb=>{ cb.onchange=function(){ var a=S.portfolio[+cb.dataset.i]; a.trailing=a.trailing||{enabled:false,trailPct:25,highest:0}; a.trailing.enabled=cb.checked; save(); render(); }; }); [].forEach.call(v.querySelectorAll(".trail-pct"), inp=>{ inp.onchange=function(){ var a=S.portfolio[+inp.dataset.i]; a.trailing=a.trailing||{enabled:false,trailPct:25,highest:0}; a.trailing.trailPct=Number(inp.value||25); save(); render(); }; }); [].forEach.call(v.querySelectorAll(".del"), b=>{ b.onclick=function(){ var i=+b.dataset.i, id=S.portfolio[i].id; S.portfolio.splice(i,1); delete S.ladders[id]; save(); render(); }; }); var add=v.querySelector("#add"); if(add) add.onclick=function(){ var sym=(v.querySelector("#newSym").value||"").toUpperCase().trim(); var id=(v.querySelector("#newId").value||"").trim(); var u=Number(v.querySelector("#newUnits").value||0); var c=Number(v.querySelector("#newCost").value||0); if(!sym||!id){ alert("Provide symbol and CoinGecko ID"); return; } S.portfolio.push({id:id,symbol:sym,units:u,cost_basis_total:c,trailing:{enabled:false,trailPct:25,highest:0}}); S.ladders[id]=S.ladders[id]||[]; save(); render(); }); [].forEach.call(v.querySelectorAll(".mp"), inp=>{ inp.onchange=function(){ S.customPrices[inp.dataset.id]=Number(inp.value||0); save(); render(); }; }); var rec=v.querySelector("#recalc"); if(rec) rec.onclick=function(){ render(); }; var man=v.querySelector("#manual"); if(man) man.onchange=function(){ S.settings.manualPriceMode=man.checked; if(!S.settings.manualPriceMode){ startLive(); } else { stopLive(); } save(); render(); }; // ladders [].forEach.call(v.querySelectorAll(".lad"), inp=>{ inp.onchange=function(){ var a=inp.dataset.a; var id=inp.dataset.id; var k=inp.dataset.k; var arr=S.ladders[a]||[]; var s=arr.find(x=>x.id===id); if(!s) return; s[k]= (k==="sell_pct"||k==="value") ? Number(inp.value||0) : inp.value; save(); render(); }; }); [].forEach.call(v.querySelectorAll(".add-step"), btn=>{ btn.onclick=function(){ var a=btn.dataset.a, t=v.querySelector("#nt_"+a).value, val=Number(v.querySelector("#nv_"+a).value||0), pct=Number(v.querySelector("#np_"+a).value||0), note=v.querySelector("#nn_"+a).value||""; if(!val||!pct){ alert("Enter target and sell %"); return; } (S.ladders[a]=S.ladders[a]||[]).push({id:uid(),type:t,value:val,sell_pct:pct,note:note,done:false}); save(); render(); } }); [].forEach.call(v.querySelectorAll(".del-step"), b=>{ b.onclick=function(){ var a=b.dataset.a, id=b.dataset.id; var arr=S.ladders[a]||[]; var i=arr.findIndex(x=>x.id===id); if(i>-1) arr.splice(i,1); save(); render(); }; }); [].forEach.call(v.querySelectorAll(".mark"), b=>{ b.onclick=function(){ var a=b.dataset.asset, id=b.dataset.step; var arr=S.ladders[a]||[]; var s=arr.find(x=>x.id===id); if(s){ s.done=true; save(); render(); } }; }); [].forEach.call(v.querySelectorAll(".reset"), b=>{ b.onclick=function(){ var id=b.dataset.id; var a=S.portfolio.find(x=>x.id===id); if(a){ a.trailing.highest=price(id)||0; S.activity.unshift({id:uid(),ts:new Date().toLocaleString(),text:a.symbol+": reset highest to $"+n2(a.trailing.highest)}); save(); render(); } }; }); // DAT var addD=v.querySelector("#addDAT"); if(addD) addD.onclick=function(){ var t=(v.querySelector("#newDT").value||"").toUpperCase().trim(), s=Number(v.querySelector("#newDS").value||0), p=Number(v.querySelector("#newDP").value||0); if(!t){ alert("Ticker required"); return; } S.dat.push({ticker:t,shares:s,price:p,marketCap:0,btc:0,eth:0,otherUSD:0,targets:[]}); save(); render(); }); [].forEach.call(v.querySelectorAll(".dat"), inp=>{ inp.onchange=function(){ var i=+inp.dataset.i, k=inp.dataset.k, val=(/shares|price|marketCap|btc|eth|otherUSD/.test(k)?Number(inp.value||0):inp.value); S.dat[i][k]=val; save(); render(); }; }); [].forEach.call(v.querySelectorAll(".del-dat"), b=>{ b.onclick=function(){ var i=+b.dataset.i; S.dat.splice(i,1); save(); render(); } }); // settings var ref=v.querySelector("#ref"); if(ref) ref.onchange=function(){ S.settings.refreshSec=Math.max(10, Number(ref.value||60)); save(); if(!S.settings.manualPriceMode){ startLive(); } }; var rst=v.querySelector("#reset"); if(rst) rst.onclick=function(){ if(confirm("Reset everything?")){ localStorage.removeItem("cryptoExit_v1"); S=structuredClone(DEFAULT); stopLive(); prices={}; save(); render(); } }; var dump=v.querySelector("#dump"); if(dump) dump.onclick=function(){ navigator.clipboard.writeText(JSON.stringify(S,null,2)).then(()=>alert("Copied JSON")); }; var exp2=v.querySelector("#exp2"); if(exp2) exp2.onclick=exportJSON; var imp2=v.querySelector("#imp2"); if(imp2) imp2.onchange=importJSON; var live=v.querySelector("#live"); if(live) live.onchange=function(){ S.settings.manualPriceMode=!live.checked; save(); if(live.checked){ startLive(); } else { stopLive(); } render(); }; } // --- Live prices (optional) --- function startLive(){ stopLive(); fetchPrices(); timer=setInterval(fetchPrices, (S.settings.refreshSec||60)*1000); } function stopLive(){ if(timer){ clearInterval(timer); timer=null; } } function fetchPrices(){ try{ var ids=S.portfolio.map(a=>a.id).filter((v,i,a)=>a.indexOf(v)===i); if(!ids.length) return; var url="https://api.coingecko.com/api/v3/simple/price?ids="+encodeURIComponent(ids.join(","))+"&vs_currencies=usd&include_24hr_change=true"; fetch(url,{headers:{"accept":"application/json"}}).then(r=>r.json()).then(d=>{ prices={}; for(var k in d){ prices[k]={usd:d[k].usd,chg:d[k].usd_24h_change}; } // trailing highs S.portfolio.forEach(a=>{ if(a.trailing&&a.trailing.enabled){ var cur=prices[a.id]&&prices[a.id].usd||0; if(cur>0 && (!a.trailing.highest || cur>a.trailing.highest)){ a.trailing.highest=cur; }}); save(); render(); }).catch(()=>showErr("Live price fetch failed. Keep using Manual Mode.")); }catch(e){ showErr("Live price fetch blocked by CSP. Use Manual Mode."); } } // --- Import/Export --- function exportJSON(){ var blob=new Blob([JSON.stringify(S,null,2)],{type:"application/json"}); var url=URL.createObjectURL(blob); var a=document.createElement("a"); a.href=url; a.download="crypto-exit-plan.json"; a.click(); setTimeout(()=>URL.revokeObjectURL(url),500); } function importJSON(ev){ var f=ev.target.files[0]; if(!f) return; var r=new FileReader(); r.onload=function(){ try{ var o=JSON.parse(r.result); S=Object.assign(structuredClone(DEFAULT),o); save(); render(); }catch(e){ alert("Invalid JSON"); } }; r.readAsText(f); } function showErr(msg){ var e=document.getElementById("err"); e.style.display="block"; e.textContent=msg; } // init window.addEventListener("error", e=>showErr("JS Error: "+(e.message||"unknown"))); window.addEventListener("unhandledrejection", e=>showErr("Promise Error: "+(e.reason&& (e.reason.message||e.reason)))); render(); })();