HTML Candle Countdown Timer For Outside Of ThinkOrSwim

rocketlightspeed

New member
VIP
@merryDay @halcyonguy

I don't know why I haven't done this sooner, but I asked Gemini to help create a Countdown Timer in Javascript to run in a local web browser. This bypasses the "tick-update" lag in TOS by tying the count to your computer's clock (with a +/- sync adjustment).

I know everyone follows different candlesticks, so I had the AI create a "Dashboard Generator." You simply select the timeframes you want (1m, 5m, 2h, etc.), and it generates a custom dashboard file for you.

I also just added Audio Alerts (Text-to-Speech), so the tool will actually tell you when a candle is closing (e.g., "5 minute candle closing in 10 seconds").

I'm really enjoying asking LLMs to develop all these different codes. Let me know if you find it useful, and just throw it back to a LLM to rework it to your needs!
CandleStick_Countdown_Timer.png

CandleStick_Dashboard.png


HTML:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Candlestick Dashboard Generator (Audio)</title>
    <style>
        :root { --bg: #121212; --card: #1e1e1e; --accent: #007bff; --text: #e0e0e0; --border: #333; }
        body { background: var(--bg); color: var(--text); font-family: 'Roboto Mono', monospace; display: flex; flex-direction: column; align-items: center; min-height: 100vh; margin: 0; padding: 20px; }
       
        h1 { color: var(--accent); margin-bottom: 5px; }
        p { color: #888; margin-bottom: 25px; font-size: 0.9rem; }

        .builder-container { background: var(--card); border: 1px solid var(--border); padding: 30px; border-radius: 10px; width: 100%; max-width: 500px; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
        .row-list { margin-bottom: 20px; }
        .config-row { display: flex; gap: 10px; margin-bottom: 10px; animation: fadeIn 0.3s ease; }
        @keyframes fadeIn { from { opacity: 0; transform: translateY(-5px); } to { opacity: 1; transform: translateY(0); } }

        select, input { background: #2a2a2a; border: 1px solid #444; color: white; padding: 10px; border-radius: 5px; flex-grow: 1; font-family: inherit; }
       
        button { cursor: pointer; border: none; border-radius: 5px; padding: 10px 15px; font-weight: bold; transition: 0.2s; }
        .btn-add { background: #333; color: white; border: 1px solid #555; width: 100%; }
        .btn-add:hover { background: #444; }
        .btn-remove { background: #d32f2f; color: white; flex-grow: 0; }
        .btn-remove:hover { background: #b71c1c; }
        .btn-generate { background: var(--accent); color: white; width: 100%; padding: 15px; font-size: 1.1rem; margin-top: 20px; }
        .btn-generate:hover { background: #0056b3; }
    </style>
</head>
<body>

    <h1>Candlestick Dashboard Generator</h1>
    <p>Now includes Text-to-Speech Alerts.</p>

    <div class="builder-container">
        <div id="row-list" class="row-list"></div>
        <button class="btn-add" onclick="addRow()">+ ADD TIMER CARD</button>
        <button class="btn-generate" onclick="generateFile()">DOWNLOAD DASHBOARD (.html)</button>
    </div>

    <script>
        const options = [
            { label: "1 Minute", val: 60 }, { label: "2 Minute", val: 120 },
            { label: "3 Minute", val: 180 }, { label: "5 Minute", val: 300 },
            { label: "10 Minute", val: 600 }, { label: "15 Minute", val: 900 },
            { label: "30 Minute", val: 1800 }, { label: "1 Hour", val: 3600 },
            { label: "2 Hour (Aligned)", val: 7200 }, { label: "4 Hour (Aligned)", val: 14400 }
        ];

        function addRow(val = 300) {
            const div = document.createElement('div'); div.className = 'config-row';
            let html = `<select class="time-select">`;
            options.forEach(o => html += `<option value="${o.val}" ${o.val === val ? 'selected' : ''}>${o.label}</option>`);
            html += `</select><button class="btn-remove" onclick="this.parentElement.remove()">X</button>`;
            div.innerHTML = html;
            document.getElementById('row-list').appendChild(div);
        }

        window.onload = () => { addRow(60); addRow(300); addRow(900); addRow(14400); };

        function generateFile() {
            const selects = document.querySelectorAll('.time-select');
            const config = Array.from(selects).map(sel => {
                const sec = parseInt(sel.value);
                const opt = options.find(o => o.val === sec);
                let warn = "00:00:30";
                if(sec <= 60) warn = "00:00:10"; else if(sec >= 3600) warn = "00:05:00";
                return { label: opt.label.replace(" (Aligned)", ""), seconds: sec, type: sec>=7200?'aligned':'standard', warn: warn };
            });

            const blob = new Blob([buildHTML(config)], { type: 'text/html' });
            const a = document.createElement('a');
            a.href = URL.createObjectURL(blob);
            a.download = 'Trading_Dashboard_Audio.html';
            document.body.appendChild(a); a.click(); document.body.removeChild(a);
        }

        // --- TEMPLATE WITH AUDIO ENGINE ---
        function buildHTML(config) {
            return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Candlesticks Countdown Timer (Audio)</title>
<style>
    :root { --bg: #121212; --card: #1e1e1e; --alert: #7f1d1d; --text: #ffffff; --dim: #888; --accent: #007bff; --input: #2a2a2a; --border: #333; --green: #28a745; --red: #dc3545; }
    html { font-size: 16px; }
    body { background: var(--bg); color: var(--text); font-family: 'Roboto Mono', monospace; display: flex; flex-direction: column; align-items: center; height: 100vh; margin: 0; overflow: hidden; transition: background 0.3s; }
   
    /* TOP BAR */
    .top-bar { width: 100%; padding: 12px 20px; background: rgba(0,0,0,0.2); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; }
    .btn-group { display: flex; gap: 10px; }
    .header-btn { background: transparent; border: 1px solid var(--dim); color: var(--text); padding: 5px 15px; cursor: pointer; font-weight: bold; border-radius: 4px; font-size: 0.8rem; }
    .header-btn:hover { background: rgba(255,255,255,0.1); }
    .help-btn { border-color: var(--accent); color: var(--accent); }
    .help-btn:hover { background: var(--accent); color: white; }
   
    /* AUDIO BUTTON */
    .audio-btn { border: 1px solid var(--red); color: var(--red); min-width: 100px; }
    .audio-btn.on { border-color: var(--green); color: var(--green); background: rgba(40, 167, 69, 0.1); }
   
    /* GRID */
    .grid-wrapper { width: 100%; overflow-y: auto; display: flex; justify-content: center; padding: 20px 0; }
    .grid-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; width: 95%; max-width: 1400px; }
   
    /* CARD */
    .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 15px; text-align: center; display: flex; flex-direction: column; justify-content: space-between; min-height: 10rem; transition: background 0.2s; }
    .card.alert { background: var(--alert) !important; border-color: #ff4444; }
    .alert .c-title, .alert label { color: #ffcccc !important; }
    .c-title { color: var(--dim); font-size: 0.85rem; text-transform: uppercase; margin-bottom: 5px; letter-spacing: 1px; }
    .c-time { font-size: 2.8rem; font-weight: bold; line-height: 1.1; }
    .c-ms { font-size: 1.2rem; color: var(--dim); }
   
    /* INPUTS */
    .row { display: flex; justify-content: center; gap: 10px; margin-top: 5px; padding-top: 5px; border-top: 1px solid var(--border); }
    input { background: var(--input); border: 1px solid var(--border); color: var(--text); padding: 2px 5px; border-radius: 3px; font-family: inherit; font-size: 0.8rem; text-align: center; width: 70px; }
    .align-in { width: 60px !important; color: var(--accent) !important; font-weight: bold; }
    .rth { grid-column: span 2; } .rth-inputs { display: flex; gap: 15px; margin-bottom: 5px; justify-content: center; }
   
    /* CONTROLS */
    button { cursor: pointer; border-radius: 4px; border: 1px solid #555; background: #333; color: white; padding: 5px 10px; }
    button:hover { background: #444; }
   
    /* OVERLAYS */
    .overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); z-index: 1000; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: 0.3s; }
    .overlay.visible { opacity: 1; pointer-events: all; }
    .modal { background: #222; border: 1px solid #444; border-radius: 10px; padding: 30px; max-width: 600px; width: 90%; color: #ddd; box-shadow: 0 10px 30px rgba(0,0,0,0.5); }
    .modal h2 { border-bottom: 1px solid #444; padding-bottom: 10px; margin-top: 0; color: var(--accent); }
    .modal h3 { color: #ddd; margin-top: 20px; font-size: 1.1rem; }
    .modal p, .modal li { color: #aaa; line-height: 1.5; font-size: 0.9rem; }
    .close-btn { float: right; cursor: pointer; font-size: 1.5rem; color: #888; } .close-btn:hover { color: white; }
   
    /* DESIGN STUDIO */
    .cf-grp { margin-bottom: 20px; }
    .cf-grp h4 { margin-bottom: 10px; font-size: 0.8rem; color: #aaa; text-transform: uppercase; letter-spacing: 1px; }
    .cf-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #333; }
    input[type="color"] { border: none; width: 40px; height: 30px; cursor: pointer; background: none; }
    input[type="range"] { width: 150px; cursor: pointer; }
    .rst-btn { width: 100%; background: #d32f2f; margin-top: 10px; padding: 12px; font-weight: bold; border: none; } .rst-btn:hover { background: #b71c1c; }
</style>
</head>
<body>

<div id="set-ov" class="overlay" onclick="tog('set-ov',0)"><div class="modal" onclick="event.stopPropagation()">
    <span class="close-btn" onclick="tog('set-ov',0)">&times;</span>
    <h2>Design Studio</h2>
    <div class="cf-grp"><h4>Interface Scale</h4><div class="cf-row"><span>Global Text Size</span><input type="range" min="12" max="24" value="16" oninput="upTh('size',this.value)"></div></div>
    <div class="cf-grp"><h4>Colors</h4><div class="cf-row"><span>Main Background</span><input type="color" id="p-bg" oninput="upTh('--bg',this.value)"></div><div class="cf-row"><span>Card Background</span><input type="color" id="p-cd" oninput="upTh('--card',this.value)"></div><div class="cf-row"><span>Text Color</span><input type="color" id="p-tx" oninput="upTh('--text',this.value)"></div><div class="cf-row"><span>Alert Background (Warning)</span><input type="color" id="p-al" oninput="upTh('--alert',this.value)"></div></div>
    <button class="rst-btn" onclick="resetTh()">RESET TO DEFAULTS</button>
</div></div>

<div id="hlp-ov" class="overlay" onclick="tog('hlp-ov',0)"><div class="modal" onclick="event.stopPropagation()">
    <span class="close-btn" onclick="tog('hlp-ov',0)">&times;</span>
    <h2>Timer User Manual</h2>
    <h3>1. Audio Alerts (New)</h3>
    <p>Click the <strong>SOUND: OFF</strong> button in the top bar to enable audio. The timer will speak when a candle hits the warning time (e.g., "5 Minute candle closing in 10 seconds").</p>
    <h3>2. Synchronization</h3>
    <p>Manually sync this timer to your trading platform (TOS/Ninja) using the SYNC buttons.</p>
    <h3>3. Alignment (2H & 4H)</h3>
    <p>Set <strong>"ALIGN TO"</strong> to <code>17:00</code> for Futures or <code>00:00</code> for Crypto.</p>
</div></div>

<div class="top-bar">
    <div><span style="color:var(--dim);font-size:0.7rem">NY TIME (ET)</span><br><span id="ny" style="font-size:1.4rem;font-weight:bold">--:--</span></div>
    <div>
        <button id="snd-btn" class="audio-btn" onclick="togSnd()">SOUND: OFF</button>
        <span style="margin:0 10px;color:#333">|</span>
        <button onclick="adj(-100)">-100</button><button onclick="adj(-10)">-10</button>
        <span id="off" style="font-weight:bold;min-width:40px;display:inline-block;text-align:center">0</span>
        <button onclick="adj(10)">+10</button><button onclick="adj(100)">+100</button>
    </div>
    <div class="btn-group">
        <button class="header-btn" onclick="tog('set-ov',1)">SETTINGS</button>
        <button class="header-btn help-btn" onclick="tog('hlp-ov',1)">HELP</button>
    </div>
</div>

<div class="grid-wrapper"><div class="grid-container" id="grid"></div></div>

<script>
const config = ${JSON.stringify(config)};
let offset = 0;
let audioOn = false;
let lastSpeak = {}; // Store last spoken time to prevent repeating

function init() {
    let html = '';
   
    config.forEach((c, i) => {
        const warn = localStorage.getItem('w-'+i) || c.warn;
        let timeHTML = c.seconds === 60 ? '<span id="t-'+i+'" class="c-time">00</span><span id="m-'+i+'" class="c-ms">.00</span>' : '<span id="t-'+i+'" class="c-time">00:00</span>';
       
        let alignHTML = '';
        if(c.type === 'aligned') {
            const alignTime = localStorage.getItem('a-'+i) || (c.seconds === 14400 ? '17:00' : '00:00');
            alignHTML = '<div style="margin-bottom:5px"><label style="justify-content:center">ALIGN TO: <input type="time" id="a-'+i+'" class="align-in" value="'+alignTime+'" onchange="save(0)"></label></div>';
        }
        html += '<div class="card" id="c-'+i+'"><div class="c-title">'+c.label+'</div>'+alignHTML+'<div>'+timeHTML+'</div><div class="row"><label>ALERT AT: <input type="text" id="w-'+i+'" value="'+warn+'" onchange="save(0)"></label></div></div>';
    });

    const rO = localStorage.getItem('ro') || '09:30';
    const rC = localStorage.getItem('rc') || '17:00';
    const rW = localStorage.getItem('rw') || '00:15:00';
    html += '<div class="card rth" id="c-rth"><div class="rth-inputs"><label>OPEN <input type="time" id="ro" value="'+rO+'" onchange="save(0)"></label><label>CLOSE <input type="time" id="rc" value="'+rC+'" onchange="save(0)"></label></div><div><span id="trth" class="c-time">--</span><div id="srth" style="font-size:0.8rem;color:var(--dim)">LOAD</div></div><div class="row"><label>ALERT AT: <input type="text" id="rw" value="'+rW+'" onchange="save(0)"></label></div></div>';

    document.getElementById('grid').innerHTML = html;
    loadTh();
    loop();
}

function loop() {
    const nowMs = Date.now() + offset;
    const now = new Date(nowMs);
    const ms = 999 - now.getMilliseconds();
   
    document.getElementById('ny').innerText = now.toLocaleTimeString('en-US', {timeZone:'America/New_York',hour12:false});
    const [h,m,s] = document.getElementById('ny').innerText.split(':').map(Number);

    config.forEach((c, i) => {
        let secRem = 0;
        if(c.type === 'standard') {
            const total = Math.floor(nowMs/1000);
            secRem = c.seconds - (total % c.seconds) - 1;
        } else {
            const alignVal = document.getElementById('a-'+i).value;
            const [aH] = alignVal.split(':').map(Number);
            const intervalHrs = c.seconds / 3600;
            let dist = (aH - h) % intervalHrs;
            if (dist <= 0) dist += intervalHrs;
            secRem = ((dist - 1) * 3600) + ((59 - m) * 60) + (59 - s);
        }

        const tEl = document.getElementById('t-'+i);
        if(c.seconds === 60) {
            tEl.innerText = (secRem<10?'0':'')+secRem;
            document.getElementById('m-'+i).innerText = '.'+Math.floor(ms/10).toString().padStart(2,'0');
        } else {
            tEl.innerText = fmt(secRem, c.seconds>=3600);
        }
       
        // ALERT & AUDIO LOGIC
        const thresh = parseTime(document.getElementById('w-'+i).value);
        if(secRem <= thresh) document.getElementById('c-'+i).classList.add('alert');
        else document.getElementById('c-'+i).classList.remove('alert');

        if(secRem === thresh) {
            if(lastSpeak['c-'+i] !== secRem) {
                speak(c.label + " closing in " + thresh + " seconds");
                lastSpeak['c-'+i] = secRem;
            }
        } else {
             lastSpeak['c-'+i] = -1; // Reset when not on exact second
        }
    });

    // RTH LOGIC
    const ro = document.getElementById('ro').value.split(':').map(Number);
    const rc = document.getElementById('rc').value.split(':').map(Number);
    const nowS = (h*3600)+(m*60)+s;
    const oS = (ro[0]*3600)+(ro[1]*60);
    const cS = (rc[0]*3600)+(rc[1]*60);
   
    if(nowS >= oS && nowS < cS) {
        const diff = cS - nowS - 1;
        document.getElementById('trth').innerText = fmt(diff,1);
        document.getElementById('srth').innerText = "MARKET OPEN";
       
        const thresh = parseTime(document.getElementById('rw').value);
        if(diff <= thresh) document.getElementById('c-rth').classList.add('alert');
        else document.getElementById('c-rth').classList.remove('alert');
       
        if(diff === thresh && lastSpeak['rth'] !== diff) {
            speak("Session closing in " + thresh + " seconds");
            lastSpeak['rth'] = diff;
        }
    } else {
        document.getElementById('c-rth').classList.remove('alert');
        if(nowS < oS) {
            document.getElementById('trth').innerText = fmt(oS-nowS-1,1);
            document.getElementById('srth').innerText = "PRE-MARKET";
        } else {
            document.getElementById('trth').innerText = "CLOSED";
            document.getElementById('srth').innerText = "SESSION ENDED";
        }
    }
    requestAnimationFrame(loop);
}

// HELPERS
function speak(msg) {
    if(!audioOn) return;
    const u = new SpeechSynthesisUtterance(msg);
    u.rate = 1.1; // Slightly faster
    window.speechSynthesis.speak(u);
}
function togSnd() {
    audioOn = !audioOn;
    const btn = document.getElementById('snd-btn');
    if(audioOn) { btn.innerText="SOUND: ON"; btn.classList.add('on'); speak("Audio Enabled"); }
    else { btn.innerText="SOUND: OFF"; btn.classList.remove('on'); }
}
function parseTime(v) {
    const p = v.split(':').map(Number);
    if(p.length===3) return (p[0]*3600)+(p[1]*60)+p[2];
    if(p.length===2) return (p[0]*60)+p[1];
    return p[0];
}
function fmt(s,h) {
    const hr=Math.floor(s/3600), mn=Math.floor((s%3600)/60), sc=s%60;
    const str = (mn<10?'0':'')+mn+':'+(sc<10?'0':'')+sc;
    return h ? hr+':'+str : str;
}
function adj(n) { offset+=n; document.getElementById('off').innerText = offset>0?'+'+offset:offset; }
function save() {
    config.forEach((c,i) => {
        localStorage.setItem('w-'+i, document.getElementById('w-'+i).value);
        if(c.type==='aligned') localStorage.setItem('a-'+i, document.getElementById('a-'+i).value);
    });
    localStorage.setItem('ro', document.getElementById('ro').value);
    localStorage.setItem('rc', document.getElementById('rc').value);
    localStorage.setItem('rw', document.getElementById('rw').value);
}
function tog(id,s) { const el = document.getElementById(id); if(s) el.classList.add('visible'); else el.classList.remove('visible'); }
function upTh(p,v) { if(p==='size') { document.documentElement.style.fontSize=v+'px'; localStorage.setItem('sz',v); } else { document.documentElement.style.setProperty(p,v); localStorage.setItem(p,v); } }
function loadTh() { ['--bg','--card','--text','--alert','sz'].forEach(k => { const v = localStorage.getItem(k); if(v) { upTh(k==='sz'?'size':k, v); if(k=='--bg') document.getElementById('p-bg').value=v; if(k=='--card') document.getElementById('p-cd').value=v; if(k=='--text') document.getElementById('p-tx').value=v; if(k=='--alert') document.getElementById('p-al').value=v; } }); }
function resetTh() { localStorage.clear(); location.reload(); }

init();
<\/script>
</body>
</html>`;
        }
    </script>
</body>
</html>
 
Last edited by a moderator:

Join useThinkScript to post your question to a community of 21,000+ developers and traders.

Thread starter Similar threads Forum Replies Date
Darth Tradicus RENKO charts extreme lagging behind Candle Charts and Last Price... Playground 9

Similar threads

Not the exact question you're looking for?

Start a new thread and receive assistance from our community.

87k+ Posts
367 Online
Create Post

Similar threads

Similar threads

The Market Trading Game Changer

Join 2,500+ subscribers inside the useThinkScript VIP Membership Club
  • Exclusive indicators
  • Proven strategies & setups
  • Private Discord community
  • ‘Buy The Dip’ signal alerts
  • Exclusive members-only content
  • Add-ons and resources
  • 1 full year of unlimited support

Frequently Asked Questions

What is useThinkScript?

useThinkScript is the #1 community of stock market investors using indicators and other tools to power their trading strategies. Traders of all skill levels use our forums to learn about scripting and indicators, help each other, and discover new ways to gain an edge in the markets.

How do I get started?

We get it. Our forum can be intimidating, if not overwhelming. With thousands of topics, tens of thousands of posts, our community has created an incredibly deep knowledge base for stock traders. No one can ever exhaust every resource provided on our site.

If you are new, or just looking for guidance, here are some helpful links to get you started.

What are the benefits of VIP Membership?
VIP members get exclusive access to these proven and tested premium indicators: Buy the Dip, Advanced Market Moves 2.0, Take Profit, and Volatility Trading Range. In addition, VIP members get access to over 50 VIP-only custom indicators, add-ons, and strategies, private VIP-only forums, private Discord channel to discuss trades and strategies in real-time, customer support, trade alerts, and much more. Learn all about VIP membership here.
How can I access the premium indicators?
To access the premium indicators, which are plug and play ready, sign up for VIP membership here.
Back
Top