loading...

Leverage ETF Backtester

24종목 레버리지/인버스 ETF의 과거 성과를 시뮬레이션하세요

1 USD = KRW
리밸런싱 전략
정기 적립 매수 (DCA)
포트폴리오 가치 추이
포트폴리오
투입 원금
최대 낙폭 (Drawdown)
연도시작종료수익률최대 낙폭투입 원금
+Math.round(v).toLocaleString();return '₩'+Math.round(cv).toLocaleString()}}}}}})} function toggleLog(){useLog=!useLog;document.getElementById('logToggle').classList.toggle('active',useLog);if(lastResult)renderChart(lastResult)} function renderDDChart(r){const ctx=document.getElementById('ddChart').getContext('2d');if(ddChart)ddChart.destroy();const st=Math.max(1,Math.floor(r.dates.length/600));const lb=r.dates.filter((_,i)=>i%st===0);const d=r.dd.filter((_,i)=>i%st===0);ddChart=new Chart(ctx,{type:'line',data:{labels:lb,datasets:[{data:d,borderColor:'#ff3b5c',backgroundColor:'rgba(255,59,92,0.06)',fill:true,borderWidth:1,pointRadius:0,tension:0.1}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'#1a1a28',borderColor:'#2a2a42',borderWidth:1,callbacks:{label:c=>`Drawdown: ${c.parsed.y.toFixed(1)}%`}}},scales:{x:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',maxTicksLimit:7,font:{family:'Noto Sans KR',size:10}}},y:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',font:{family:'Noto Sans KR',size:10},callback:v=>v.toFixed(0)+'%'}}}}})} function renderTable(r){const tb=document.querySelector('#yearlyTable tbody');tb.innerHTML='';for(const[yr,d]of Object.entries(r.yearly)){const ret=((d.end-d.start)/d.start*100);tb.innerHTML+=`${yr}$${fmt(d.start)}$${fmt(d.end)}${ret>=0?'+':''}${ret.toFixed(1)}%-${(d.maxDD*100).toFixed(1)}%$${fmt(d.invested)}`}} function showToast(m,e=false){const t=document.getElementById('toast');t.textContent=m;t.className='toast show'+(e?' error':'');setTimeout(()=>t.classList.remove('show'),3000)} document.addEventListener('keydown',e=>{if(e.key==='Enter'&&selectedTicker)runBacktest()}); init(); } function fmtCurr(n){const ex=getExRate();const v=Math.round(n*ex);return getCurrSymbol()+v.toLocaleString()} function toggleCurrency(){ showKRW=!showKRW; document.getElementById('krwToggle').classList.toggle('active',showKRW); document.getElementById('usdToggle').classList.toggle('active',!showKRW); if(lastResult){renderStats(lastResult);renderChart(lastResult);renderTable(lastResult)} } document.addEventListener('change',function(e){ if(e.target.id==='exRate'&&lastResult){renderStats(lastResult);renderChart(lastResult);renderTable(lastResult)} }); // === CONFIG === const DATA_BASE='./data'; const CATEGORIES={all:{label:'전체',tickers:[]},nasdaq:{label:'나스닥',tickers:['QQQ','QLD','TQQQ','SQQQ','PSQ']},sp500:{label:'S&P500',tickers:['SPY','SSO','UPRO','SPXU','SH']},dow:{label:'다우',tickers:['DIA','DDM','SDOW','DOG']},semiconductor:{label:'반도체',tickers:['SOXX','SOXL','SOXS']},fang:{label:'FANG+',tickers:['FNGU','FNGD']},bond:{label:'채권',tickers:['TLT','TMF','TMV']},commodity:{label:'금/원자재',tickers:['GLD','UGL']}}; const LEV={QLD:'2x',TQQQ:'3x',SQQQ:'-3x',PSQ:'-1x',SSO:'2x',UPRO:'3x',SPXU:'-3x',SH:'-1x',DDM:'2x',SDOW:'-3x',DOG:'-1x',SOXL:'3x',SOXS:'-3x',FNGU:'3x',FNGD:'-3x',TMF:'3x',TMV:'-3x',UGL:'2x'}; const NAMES={QQQ:'Nasdaq-100',QLD:'2x Nasdaq',TQQQ:'3x Nasdaq',SQQQ:'-3x Nasdaq',PSQ:'-1x Nasdaq',SPY:'S&P 500',SSO:'2x S&P500',UPRO:'3x S&P500',SPXU:'-3x S&P500',SH:'-1x S&P500',DIA:'Dow Jones',DDM:'2x Dow',SDOW:'-3x Dow',DOG:'-1x Dow',SOXX:'Semiconductor',SOXL:'3x Semi',SOXS:'-3x Semi',FNGU:'3x FANG+',FNGD:'-3x FANG+',TLT:'20Y+ Treasury',TMF:'3x Bond',TMV:'-3x Bond',GLD:'Gold',UGL:'2x Gold'}; let allTickers=[],selectedTicker=null,tickerCache={},mainChart=null,ddChart=null,useLog=false,lastResult=null; async function init(){try{const r=await fetch(`${DATA_BASE}/meta.json`);const m=await r.json();allTickers=m.tickers;CATEGORIES.all.tickers=allTickers;document.getElementById('updateBadge').textContent=m.updated;renderCats();renderTickers('all');setDefaultDates()}catch(e){showToast('데이터 로드 실패',true)}} function renderCats(){const w=document.getElementById('catTabs');w.innerHTML='';for(const[k,c]of Object.entries(CATEGORIES)){const b=document.createElement('button');b.className='cat-tab'+(k==='all'?' active':'');b.textContent=c.label;b.onclick=()=>{document.querySelectorAll('.cat-tab').forEach(x=>x.classList.remove('active'));b.classList.add('active');renderTickers(k)};w.appendChild(b)}} function renderTickers(cat){const w=document.getElementById('tickerGrid');w.innerHTML='';CATEGORIES[cat].tickers.forEach(t=>{const b=document.createElement('button');b.className='ticker-btn'+(t===selectedTicker?' selected':'');b.innerHTML=t+(LEV[t]?`${LEV[t]}`:'');b.onclick=()=>selectTicker(t);w.appendChild(b)})} async function selectTicker(t){selectedTicker=t;document.querySelectorAll('.ticker-btn').forEach(b=>{b.classList.toggle('selected',b.childNodes[0].textContent.trim()===t)});const d=await loadData(t);if(d){const p=d.prices,last=p[p.length-1],prev=p[p.length-2];const chg=((last.c-prev.c)/prev.c*100);document.getElementById('tickerInfo').classList.add('show');document.getElementById('tiName').textContent=t;const lE=document.getElementById('tiLev');const lT=LEV[t]||'1x';lE.textContent=lT+' · '+(NAMES[t]||'');lE.className='ti-lev '+(lT.includes('-')?'short':lT==='1x'?'neutral':'long');document.getElementById('tiPrice').innerHTML='$'+last.c.toFixed(2)+` ${chg>=0?'+':''}${chg.toFixed(2)}%`;document.getElementById('tiRange').textContent=`${p[0].d} — ${last.d} · ${d.count.toLocaleString()}일`;applyPeriod()}} function setDefaultDates(){const n=new Date(),y=new Date(n);y.setFullYear(n.getFullYear()-5);const es=n.toISOString().split('T')[0];const ss=y.toISOString().split('T')[0];document.getElementById('endDate').value=es;document.getElementById('startDate').value=ss;fpS.setDate(ss,false);fpE.setDate(es,false)} async function loadData(t){if(tickerCache[t])return tickerCache[t];try{const r=await fetch(`${DATA_BASE}/${t}.json`);const d=await r.json();tickerCache[t]=d;return d}catch(e){showToast(`${t} 로드 실패`,true);return null}} document.querySelectorAll('.period-btn').forEach(b=>{b.addEventListener('click',function(){document.querySelectorAll('.period-btn').forEach(x=>x.classList.remove('active'));this.classList.add('active');applyPeriod()})}); function applyPeriod(){const a=document.querySelector('.period-btn.active');if(!a||!selectedTicker)return;const p=a.dataset.period;const e=new Date();let s=new Date(e);if(p==='1Y')s.setFullYear(e.getFullYear()-1);else if(p==='3Y')s.setFullYear(e.getFullYear()-3);else if(p==='5Y')s.setFullYear(e.getFullYear()-5);else if(p==='10Y')s.setFullYear(e.getFullYear()-10);else if(p==='MAX'){const d=tickerCache[selectedTicker];if(d)s=new Date(d.prices[0].d);else s=new Date('2000-01-01')}const ev=e.toISOString().split('T')[0];const sv=s.toISOString().split('T')[0];document.getElementById('endDate').value=ev;document.getElementById('startDate').value=sv;fpS.setDate(sv,false);fpE.setDate(ev,false)} async function runBacktest(){if(!selectedTicker){showToast('종목을 선택해주세요',true);return}const btn=document.getElementById('runBtn');btn.innerHTML='분석 중...';btn.classList.add('loading');const data=await loadData(selectedTicker);if(!data){btn.innerHTML='백테스트 실행';btn.classList.remove('loading');return}const sd=document.getElementById('startDate').value,ed=document.getElementById('endDate').value;const ic=parseFloat(document.getElementById('initCash').value);const st=selectedStrat;const mA=parseFloat(document.getElementById('monthlyAmt').value)||500;const dT=parseFloat(document.getElementById('ddThreshold').value)/100||0.1;const dA=parseFloat(document.getElementById('ddAmount').value)||2000;const prices=data.prices.filter(p=>p.d>=sd&&p.d<=ed);if(prices.length<2){showToast('데이터가 부족합니다',true);btn.innerHTML='백테스트 실행';btn.classList.remove('loading');return}const result=simulate(prices,ic,st,{mA,dT,dA});lastResult=result;setTimeout(()=>{renderStats(result);renderChart(result);renderDDChart(result);renderTable(result);document.getElementById('results').classList.add('show');document.getElementById('results').scrollIntoView({behavior:'smooth',block:'start'});btn.innerHTML='백테스트 실행';btn.classList.remove('loading')},150)} function simulate(prices,ic,st,o){const dates=[],pv=[],iv=[],dd=[];let shares=ic/prices[0].c,inv=ic,peak=ic,mxDD=0,mxDDd='',lM='',lW=-1,lDB=0,weeklyCarry=0;const dR=[]; // Build date set for quick lookup const dateSet=new Set(prices.map(p=>p.d)); for(let i=0;i0){shares+=o.mA/p.c;inv+=o.mA} // === WEEKLY DCA (Monday buy, carry over if holiday) === if(st==='weekly'&&i>0){ const wk=getISOWeek(dt); if(wk!==lW){ // New week: check if today is Monday or first trading day of this week const monDate=getMondayOfWeek(dt); const monStr=fmtDate(monDate); // If Monday is a trading day and today is Monday, buy // If Monday wasn't a trading day, buy on first available day of the week if(dow===1||(wk!==lW&&!dateSet.has(monStr))){ const amt=o.mA+weeklyCarry;shares+=amt/p.c;inv+=amt;weeklyCarry=0;lW=wk; } else if(dow===1){ shares+=o.mA/p.c;inv+=o.mA;weeklyCarry=0;lW=wk; } // If we haven't bought yet this week but it's not Monday if(lW!==wk){ // Check: was Monday this week a trading day? if(!dateSet.has(monStr)){ // Monday was closed, buy today (first available day) const amt=o.mA+weeklyCarry;shares+=amt/p.c;inv+=amt;weeklyCarry=0; } lW=wk; } } // End of data: check if last week had no purchase if(i===prices.length-1&&lW!==getISOWeek(dt)){weeklyCarry+=o.mA} } // === MONTHLY DCA === if(st==='monthly'&&mo!==lM&&i>0){shares+=o.mA/p.c;inv+=o.mA} // === DRAWDOWN === if(st==='drawdown'){if(cv>peak)peak=cv;const d=(peak-cv)/peak;if(d>=o.dT&&(lDB===0||d>lDB+o.dT)){shares+=o.dA/p.c;inv+=o.dA;lDB=d}} const v=shares*p.c;if(i>0)dR.push((v-pv[i-1])/pv[i-1]);dates.push(p.d);pv.push(v);iv.push(inv);const hp=Math.max(...pv);const d=(hp-v)/hp;dd.push(-d*100);if(d>mxDD){mxDD=d;mxDDd=p.d}lM=mo} const fv=pv[pv.length-1],tr=(fv-inv)/inv*100,yrs=prices.length/252;const cagr=(Math.pow(fv/ic,1/yrs)-1)*100;const avgR=dR.reduce((a,b)=>a+b,0)/dR.length;const stdR=Math.sqrt(dR.reduce((a,b)=>a+(b-avgR)**2,0)/dR.length);const sharpe=(avgR/stdR)*Math.sqrt(252);const vol=stdR*Math.sqrt(252)*100;const yearly={};for(let i=0;iyearly[yr].peak)yearly[yr].peak=pv[i];const yd=(yearly[yr].peak-pv[i])/yearly[yr].peak;if(yd>yearly[yr].maxDD)yearly[yr].maxDD=yd}return{ticker:selectedTicker,dates,pv,iv,dd,fv,inv,tr,cagr,mxDD:mxDD*100,mxDDd,sharpe,vol,yearly,days:prices.length}} // Helper: ISO week number function getISOWeek(d){const t=new Date(d.valueOf());t.setDate(t.getDate()+4-(t.getDay()||7));const y=new Date(t.getFullYear(),0,1);return Math.ceil(((t-y)/86400000+1)/7)} // Helper: get Monday of a given date's week function getMondayOfWeek(d){const t=new Date(d.valueOf());const day=t.getDay()||7;t.setDate(t.getDate()-day+1);return t} // Helper: format date as YYYY-MM-DD function fmtDate(d){return d.toISOString().split('T')[0]} function renderStats(r){const s=[{l:'최종 가치',v:'$'+fmt(r.fv),c:r.tr>=0?'up':'down'},{l:'총 수익률',v:(r.tr>=0?'+':'')+r.tr.toFixed(1)+'%',c:r.tr>=0?'up':'down'},{l:'CAGR',v:(r.cagr>=0?'+':'')+r.cagr.toFixed(1)+'%',c:r.cagr>=0?'up':'down'},{l:'최대 낙폭',v:'-'+r.mxDD.toFixed(1)+'%',c:'down',s:r.mxDDd},{l:'Sharpe Ratio',v:r.sharpe.toFixed(2),c:r.sharpe>=1?'up':r.sharpe>=0?'':'down'},{l:'변동성',v:r.vol.toFixed(1)+'%',c:''},{l:'투입 원금',v:'$'+fmt(r.inv),c:''},{l:'투자 기간',v:r.days.toLocaleString()+'일',c:''}];document.getElementById('statsGrid').innerHTML=s.map(x=>`
${x.l}
${x.v}
${x.s?`
${x.s}
`:''}
`).join('')} function fmt(n){return Math.round(n).toLocaleString()} function renderChart(r){const ctx=document.getElementById('mainChart').getContext('2d');if(mainChart)mainChart.destroy();const st=Math.max(1,Math.floor(r.dates.length/600));const lb=r.dates.filter((_,i)=>i%st===0);const pd=r.pv.filter((_,i)=>i%st===0);const id=r.iv.filter((_,i)=>i%st===0);mainChart=new Chart(ctx,{type:'line',data:{labels:lb,datasets:[{label:'포트폴리오',data:pd,borderColor:'#00f0a8',backgroundColor:'rgba(0,240,168,0.06)',fill:true,borderWidth:1.5,pointRadius:0,tension:0.1},{label:'투입 원금',data:id,borderColor:'#3b7dff',borderWidth:1,borderDash:[6,4],pointRadius:0,fill:false}]},options:{responsive:true,maintainAspectRatio:false,interaction:{mode:'index',intersect:false},plugins:{legend:{display:false},tooltip:{backgroundColor:'#1a1a28',borderColor:'#2a2a42',borderWidth:1,titleColor:'#eaeaf4',bodyColor:'#9898b8',padding:10,titleFont:{family:'Noto Sans KR',size:11},bodyFont:{family:'Noto Sans KR',size:11},callbacks:{label:c=>`${c.dataset.label}: $${fmt(c.parsed.y)}`}}},scales:{x:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',maxTicksLimit:7,font:{family:'Noto Sans KR',size:10}}},y:{type:useLog?'logarithmic':'linear',grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',font:{family:'Noto Sans KR',size:10},callback:v=>{const ex=getExRate();const cv=v*ex;if(ex===1)return '}}}}})} function toggleLog(){useLog=!useLog;document.getElementById('logToggle').classList.toggle('active',useLog);if(lastResult)renderChart(lastResult)} function renderDDChart(r){const ctx=document.getElementById('ddChart').getContext('2d');if(ddChart)ddChart.destroy();const st=Math.max(1,Math.floor(r.dates.length/600));const lb=r.dates.filter((_,i)=>i%st===0);const d=r.dd.filter((_,i)=>i%st===0);ddChart=new Chart(ctx,{type:'line',data:{labels:lb,datasets:[{data:d,borderColor:'#ff3b5c',backgroundColor:'rgba(255,59,92,0.06)',fill:true,borderWidth:1,pointRadius:0,tension:0.1}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'#1a1a28',borderColor:'#2a2a42',borderWidth:1,callbacks:{label:c=>`Drawdown: ${c.parsed.y.toFixed(1)}%`}}},scales:{x:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',maxTicksLimit:7,font:{family:'Noto Sans KR',size:10}}},y:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',font:{family:'Noto Sans KR',size:10},callback:v=>v.toFixed(0)+'%'}}}}})} function renderTable(r){const tb=document.querySelector('#yearlyTable tbody');tb.innerHTML='';for(const[yr,d]of Object.entries(r.yearly)){const ret=((d.end-d.start)/d.start*100);tb.innerHTML+=`${yr}$${fmt(d.start)}$${fmt(d.end)}${ret>=0?'+':''}${ret.toFixed(1)}%-${(d.maxDD*100).toFixed(1)}%$${fmt(d.invested)}`}} function showToast(m,e=false){const t=document.getElementById('toast');t.textContent=m;t.className='toast show'+(e?' error':'');setTimeout(()=>t.classList.remove('show'),3000)} document.addEventListener('keydown',e=>{if(e.key==='Enter'&&selectedTicker)runBacktest()}); init(); +Math.round(v).toLocaleString();return '₩'+Math.round(cv).toLocaleString()}}}}}})} function toggleLog(){useLog=!useLog;document.getElementById('logToggle').classList.toggle('active',useLog);if(lastResult)renderChart(lastResult)} function renderDDChart(r){const ctx=document.getElementById('ddChart').getContext('2d');if(ddChart)ddChart.destroy();const st=Math.max(1,Math.floor(r.dates.length/600));const lb=r.dates.filter((_,i)=>i%st===0);const d=r.dd.filter((_,i)=>i%st===0);ddChart=new Chart(ctx,{type:'line',data:{labels:lb,datasets:[{data:d,borderColor:'#ff3b5c',backgroundColor:'rgba(255,59,92,0.06)',fill:true,borderWidth:1,pointRadius:0,tension:0.1}]},options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{display:false},tooltip:{backgroundColor:'#1a1a28',borderColor:'#2a2a42',borderWidth:1,callbacks:{label:c=>`Drawdown: ${c.parsed.y.toFixed(1)}%`}}},scales:{x:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',maxTicksLimit:7,font:{family:'Noto Sans KR',size:10}}},y:{grid:{color:'rgba(42,42,66,0.3)'},ticks:{color:'#6a6a88',font:{family:'Noto Sans KR',size:10},callback:v=>v.toFixed(0)+'%'}}}}})} function renderTable(r){const tb=document.querySelector('#yearlyTable tbody');tb.innerHTML='';for(const[yr,d]of Object.entries(r.yearly)){const ret=((d.end-d.start)/d.start*100);tb.innerHTML+=`${yr}$${fmt(d.start)}$${fmt(d.end)}${ret>=0?'+':''}${ret.toFixed(1)}%-${(d.maxDD*100).toFixed(1)}%$${fmt(d.invested)}`}} function showToast(m,e=false){const t=document.getElementById('toast');t.textContent=m;t.className='toast show'+(e?' error':'');setTimeout(()=>t.classList.remove('show'),3000)} document.addEventListener('keydown',e=>{if(e.key==='Enter'&&selectedTicker)runBacktest()}); init();