它做什么:每根 K 线收线前 1 分钟响一下,提醒我回到图表前看这根 K 线怎么收。
用法:点“开始提醒”,然后就可以去做别的事;知道接下来几根没机会,就按“下一根不响”或“静音 15 分钟”。
我做交易最大的问题是等 setup 的时候容易走神:等的时间一长,就去看手机、看别的东西,等回过神来,信号 K 线已经收完了。
所以我需要一个闹钟:每根 5 分钟 K 线快收线的时候响一下。比如第 1 根是 09:30–09:35,就在 09:34 响;第 2 根在 09:39 响,第 3 根在 09:44 响,以此类推。
美东时间
--:--:--
已暂停:点“开始提醒”后才会响(浏览器要求先点一下才能出声)
今天的 K 线点一格就能单独开关这一根;到哪个时段就展开哪个
时间按美东时间算,夏令时自动处理;只在周一到周五的日盘里响,交易所假日不会自动跳过。这个页面要一直开着(可以放在后台),电脑别睡眠。设置和“不响”的记录只存在这台电脑的浏览器里。快捷键:N 下一根不响,R 全部恢复。
怎么用
- 点“开始提醒”:浏览器要求先点一下页面,才允许播放声音。点之前不会响。
- 页面一直开着:可以切到别的标签页或者别的软件,但这个页面别关,电脑也别睡眠。
- 知道接下来没机会时:
- 按“下一根不响”(或者按键盘 N),只跳过最近要响的那一根;
- “下两根不响”“下三根不响”同理;
- “静音 15 分钟 / 30 分钟”适合中午那段慢市;
- 列表按早盘、午盘、晚盘、收盘前分组,到哪个时段就自动展开哪个;点某一格就能单独关掉这一根,比如只想在第 18 根收线前响。
- 改错了:按“全部恢复”(或者键盘 R)。
设置
点工具右上角的齿轮打开设置:
- K 线周期:1、2、3、5、10、15 分钟,默认 5 分钟。
- 收线前多久响:15 秒到 2 分钟,默认 1 分钟。
- 日盘时段:默认美东 09:30–16:15,可以改成只看上午,比如 09:30–11:30。
- 音量和系统通知:开了系统通知,屏幕角落也会弹一条提醒。
一些细节
- 时间按美东时间算,夏令时自动处理。我在国内,电脑是北京时间也没关系。
- 只在周一到周五响,交易所假日(比如感恩节)不会自动跳过,那天点“暂停”就好。
- 后台也能准时响:浏览器会把后台标签页的计时器放慢,所以声音是提前排在浏览器的音频时钟上的,不靠计时器。
- 设置和“不响”的记录只存在这台电脑的浏览器里,不会上传到任何地方。
定义和复现
K 线收线提醒
- 时间:美东时间(America/New_York),用浏览器自带的 Intl 换算,夏令时自动处理。
- K 线:从日盘开始算,第 1 根是 09:30–09:35,第 2 根是 09:35–09:40,以此类推;到日盘结束(默认 16:15)为止,5 分钟图一天 81 根。
- 提醒时刻:收线时间 − 提前量,默认收线前 1 分钟,所以第 1 根在 09:34 响。
- 交易日:周一到周五;交易所假日不跳过。
- 声音:两声短促的“嘀”(880 Hz 和 1175 Hz),提前排在浏览器的音频时钟上,后台标签页也准时。
- 不响:只跳过选中的那几根;静音 15 / 30 分钟是这段时间里的提醒都不响。
- Prompt 和代码:是一个简化版,只保留按时响和“下一根不响”,存成 .html 文件用浏览器打开就能用;分组列表、静音、系统通知这些没放进去。
请帮我写一个单独的 HTML 文件(不用任何库),在浏览器里打开就能用:每根 K 线收线前响一下,提醒我回到图表前。规则如下: 1. 时间按美东时间(America/New_York)算,夏令时自动处理;我的电脑是北京时间,也要算对。用浏览器自带的 Intl.DateTimeFormat 换算,不要手写时差。 2. 只看周一到周五的日盘 09:30–16:15。K 线周期 5 分钟:第 1 根是 09:30–09:35,第 2 根是 09:35–09:40,以此类推。交易所假日不用跳过。 3. 每根 K 线收线前 60 秒响一次:第 1 根在 09:34 响,第 2 根在 09:39 响。周期、提前量、日盘开始和结束时间写成文件开头的常量,方便我改。 4. 声音用 Web Audio 生成两声短促的“嘀”(880 Hz 和 1175 Hz,各 0.2 秒左右)。浏览器要求先点一下页面才能出声,所以放一个“开始提醒”按钮,点了才创建 AudioContext。 5. 后台标签页的计时器会被浏览器放慢,所以不要等计时器到点再出声:算出下一次提醒离现在还有几秒,提前把声音排在 AudioContext 的音频时钟上。要响的那一根变了(上一次已经响过,或者我按了“不响”),就撤掉还没响的那次,重新排;已经在响的不要掐断。 6. 放一个“下一根不响”按钮:跳过最近要响的那一根;连按两次就跳过两根。 7. 页面上显示此刻的美东时间(时:分:秒),以及下一次提醒的时间和是第几根 K 线;每 0.25 秒刷新一次。 请每一步加中文注释,说明为什么这样做。
<!doctype html>
<!-- K 线收线提醒:每根 K 线收线前 LEAD 秒响两声(美东时间,周一到周五日盘) -->
<!-- 用法:存成 .html 文件,用浏览器打开,点“开始提醒”,页面一直开着 -->
<meta charset="utf-8">
<title>K 线收线提醒</title>
<button id="run">开始提醒</button>
<button id="skip">下一根不响</button>
<p id="now"></p>
<p id="next"></p>
<script>
const TF = 5; // K 线周期(分钟)
const LEAD = 60; // 收线前多少秒响
const FROM = 9 * 60 + 30; // 日盘开始 09:30(美东,换成分钟)
const TO = 16 * 60 + 15; // 日盘结束 16:15
// 美东时间:用 Intl 换算,夏令时自动处理,本机是北京时间也没关系
const fmt = new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York", hourCycle: "h23",
year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
});
function et(ms) {
const p = Object.fromEntries(fmt.formatToParts(new Date(ms)).map(x => [x.type, +x.value]));
return { y: p.year, mo: p.month, d: p.day, h: p.hour, mi: p.minute, s: p.second };
}
// 美东某天第 minutes 分钟对应的真实时刻:先按 UTC 猜一个,再减去猜的和美东显示之间的差
function etToMs(y, mo, d, minutes) {
const guess = Date.UTC(y, mo - 1, d, 0, minutes);
const p = et(guess);
return guess - (Date.UTC(p.y, p.mo - 1, p.d, p.h, p.mi) - guess);
}
const pad = n => String(n).padStart(2, "0");
const clock = ms => { const p = et(ms); return `${pad(p.h)}:${pad(p.mi)}:${pad(p.s)}`; };
// 下一根还没到提醒时刻的 K 线:从今天开始往后找,跳过周六周日(交易所假日不跳)
function nextBar(after) {
const t = et(after);
for (let step = 0; step < 7; step++) {
const day = new Date(Date.UTC(t.y, t.mo - 1, t.d + step));
if (day.getUTCDay() === 0 || day.getUTCDay() === 6) continue;
for (let i = 1; FROM + i * TF <= TO; i++) {
const close = etToMs(day.getUTCFullYear(), day.getUTCMonth() + 1, day.getUTCDate(), FROM + i * TF);
if (close - LEAD * 1000 > after) return { n: i, close, alarm: close - LEAD * 1000 };
}
}
}
let ctx = null; // 浏览器要求先点一下页面才能出声,所以点“开始”时才建
let planned = null; // 已经排好的那一次提醒 { at, nodes }
let skipUntil = 0; // 这个时刻之前的提醒都不响
// 两声短促的“嘀”,排在音频时钟上:后台标签页的计时器会被放慢,音频时钟不会
function beepAt(when) {
return [[0, 880], [0.22, 1175]].map(([offset, freq]) => {
const osc = ctx.createOscillator(), g = ctx.createGain();
osc.frequency.value = freq;
g.gain.setValueAtTime(0, when + offset);
g.gain.linearRampToValueAtTime(0.6, when + offset + 0.01);
g.gain.exponentialRampToValueAtTime(0.0001, when + offset + 0.18);
osc.connect(g).connect(ctx.destination);
osc.start(when + offset);
osc.stop(when + offset + 0.2);
return osc;
});
}
function tick() {
const now = Date.now();
const bar = nextBar(Math.max(now, skipUntil));
document.getElementById("now").textContent = "美东时间 " + clock(now);
document.getElementById("next").textContent = ctx
? `下一次提醒 ${clock(bar.alarm)}(第 ${bar.n} 根收线前)`
: "还没开始";
// 要响的那一根变了(时间到了,或者按了“不响”),就撤掉旧的,重新排
if (ctx && planned?.at !== bar.alarm) {
if (planned?.at > now) planned.nodes.forEach(n => n.stop()); // 已经在响的不掐断
planned = { at: bar.alarm, nodes: beepAt(ctx.currentTime + (bar.alarm - now) / 1000) };
}
}
document.getElementById("run").onclick = () => { ctx ??= new AudioContext(); ctx.resume(); tick(); };
document.getElementById("skip").onclick = () => { skipUntil = nextBar(Math.max(Date.now(), skipUntil)).alarm; tick(); };
setInterval(tick, 250);
tick();
</script>