@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!
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!
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)">×</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)">×</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: