# JavaScript 指标开发文档

## 多晶硅与标的切换

顶部标的框及右侧自选支持白银、多晶硅主连 PS0。切换会重新加载页面，关闭旧连接并恢复新标的自己的指标，避免把两种价格混在同一条指标线上。原有白银指标保存位置保持兼容；多晶硅指标单独保存。

`ctx.symbol` 为当前数据代码：白银为 `XAG-USDT-SWAP`，多晶硅为 `PS0`。`ctx.data.history()` 默认读取当前标的，仍与主图周期独立。显式传入 `symbol: 'PS0'` 可以跨标的查询多晶硅。

多晶硅通过 AKShare 的 `futures_zh_minute_sina('PS0', '1')` 和 `futures_zh_daily_sina('PS0')` 读取真实主连分钟、日线；不填补休市分钟。时间按北京时间解析，分钟源标签是结束时间，转换为 K 线开始时间（减 60 秒）；日线使用北京时间零点。分钟历史长度由上游决定，当前约 1,023 根，不能承诺任意多年分钟历史。成交量沿用上游字段。

多晶硅成功更新后等待 60 秒再次拉取；连续失败按 60、120、240、480、900 秒退避，之后最多每 15 分钟重试；成功后归零。服务端共享缓存和退避状态，刷新页面、多开页面或指标查历史都不会绕过上游冷却。单次抓取最多 50 秒。

请求失败时保留旧图表，`onData`、`onTick` 与框架计时器暂停处理旧数据，恢复成功后继续。后台定时拉取不受页面可见性影响。`onTick` 对多晶硅表示一分钟轮询得到的最新 K 线变化，`source: 'poll'`，并非交易所逐笔推送；数据不变化时不制造新 tick。最后一根收盘价没有变化不代表交易所开市或实时成交。

`ctx.data.bars(7)`、`ctx.data.bars(40)` 等继续使用分钟数据聚合。异步 `history` 支持 `1m`、自定义正整数分钟（如 `7m`）、小时（如 `1h`）与 `1d`；日线使用上游日线，不把收盘前的一小段分钟线当作完整日线。

此工作台只在本机运行，产品为 OKX 白银永续 XAG-USDT-SWAP（不是 COMEX 期货）。历史使用 OKX 公开 REST，实时使用公开 WebSocket，不需要账号或 Key。图表及指标全部使用完整 24 小时行情。

## 1. 加载与执行

1. 打开 http://127.0.0.1:18080/，点击“指标”。
2. 粘贴代码，或通过“加载 .js（可多选）”批量选择文件；文件会依次执行并保存。
3. 点击“运行 / 保存”，关闭窗口查看图表。

代码直接在当前浏览器页面执行。没有 iframe、语法白名单或沙箱；可以使用 DOM、fetch、WebSocket、Promise、动态 `import()`、第三方库和普通 JavaScript。浏览器自身的 CORS 等行为仍然适用。代码采用异步函数体形式执行，可以在顶层写 `await`；静态 `import ... from ...` 应改成 `await import(...)`。

运行成功的脚本自动保存在当前浏览器当前地址的 localStorage 中，刷新后恢复。不同端口、不同浏览器的数据独立。可以同时运行多个脚本，每个脚本也可以注册多个指标；**不同指标使用不同 `id`**，相同 `id` 表示替换旧指标。点击“新增指标”编写另一份脚本；列表中的“编辑”会加载原脚本，保存时更新该脚本的所有指标，不影响其他脚本。

列表支持独立显示/隐藏、编辑和移除，显示“启用数 / 总数”。隐藏会隐藏 `ctx.draw` 资源并暂停该指标的更新 hook，其他指标继续运行；显示时使用最新数据重新调用 `onData`（reason 为 `resume`）。隐藏状态刷新后保留。通过原始图表 API 自建的资源需自行管理显隐。“清空指标”清除全部指标和已保存脚本。

全局管理入口：`window.SilverIndicators`，简称下文的 `api`。在编辑器中会自动注入 `api` 参数。

## 2. 最小示例

```js
await api.register({
  id: 'my-level',
  name: '我的水平线',
  onInit(ctx) {
    ctx.state.level = ctx.draw.horizontal(54, {
      color: '#3979d0', title: '观察价位'
    });
  }
});
```

只有 `id` 必填；`name` 和所有 hook 均可省略。每次注册得到独立 `ctx` 和 `ctx.state`。

## 3. Hook

| Hook | 调用时机 | 第二个参数 |
| --- | --- | --- |
| `onInit(ctx)` | 实例创建时，一次 | 无 |
| `onData(ctx, event)` | 初始化完成、历史数据或自动 feed 回补、WebSocket 更新、主图周期变化 | `{reason, bar?}`，reason 为 `init` / `history` / `feed` / `backfill` / `period` / `resume` |
| `onTick(ctx, tick)` | 收到 COMEX 开市时段内的 OKX 分钟 K 线更新 | `{id, price, time, source, confirmed}`；price 为当前分钟 close，time 为分钟开始时间（Unix 秒），source 为 candle；不是逐笔成交事件 |
| `onPeriodChange(ctx, event)` | 用户切换主图周期 | `{previous, current}`，单位为分钟，日线是 1440 |
| `onDispose(ctx)` | 移除、覆盖同 id 指标 | 无 |

所有 hook 均支持 `async`。同一实例的 hook 串行执行，初始化完成后才处理更新；执行期间同名更新会合并为最新一条，避免慢指标积压。因此 `onTick` 适合更新显示，不是逐笔成交归档接口。不同指标的异常分别记录到浏览器控制台和指标列表。

`onData` 可能多次收到相同数据。使用 `setData` 替换线数据，或按时间去重，不要假定每次回调都新增了一根已收盘 K 线。最后一根 K 线可能尚未收盘；REST 回补不会触发 onTick。推送同一分钟的成交量是累计值，框架会覆盖更新，不会重复相加。

移除时中止 `ctx.signal`，等待正在执行的 hook，再调用 `onDispose` 和自动清理。自行编写的永不结束 Promise 也会令移除等待，应让长任务响应 `ctx.signal`。

## 24 小时行情

图表与所有指标使用完整 24 小时行情，包含周末及传统 COMEX 休市期间的数据，不再做交易日历过滤。onData、onTick、onPeriodChange 和受管计时器不再因 COMEX 休市暂停。ctx.session.isOpen() 始终为 true。

自定义分钟线以 UTC 零点为起点汇总；日线为 UTC 自然日。不能整除一天的分钟周期，在日末可能不足一个完整周期。数据来源及上游连接中断仍可能造成缺口，不补造价格。

## 4. 不受主图周期限制的行情

```js
const minute = ctx.data.bars(1);      // 原始 1 分钟聚合数据
const custom = ctx.data.bars(17);    // 独立 17 分钟 K 线
const hourly = ctx.data.bars(60);    // 从已载入分钟数据汇总 1 小时
const daily = ctx.data.bars(1440);   // 约一年 UTC 自然日日线
const current = ctx.data.chartBars(); // 主图当前周期
console.log(ctx.period);             // 主图周期，只读，单位：分钟
```

`bars(minutes)` 同步返回副本，不会修改工作台的数据。分钟周期可传任意正整数，不局限于界面上的 1–40 分钟；1440 返回原生 UTC 日线。其他值都从已载入的近 5 个自然日内的完整 1 分钟数据聚合，不会因为传入更大周期而自动扩大历史范围。

每根 K 线结构：

```js
{
  time: 1792980000,       // Unix 秒；图表也使用秒，不是毫秒
  date: '2026-10-26',     // 数据源交易日期标签；计算请优先使用 time
  open: 54.1, high: 54.4, low: 54.0, close: 54.3,
  volume: 123             // 数据源提供的成交量
}
```

以上数字仅说明结构。数据按时间递增。自定义分钟线按 UTC 零点对齐。缺失分钟不补造价格；时段尾部可能不足指定分钟数。聚合取首开、最高、最低、末收和成交量之和。

### 异步读取更多历史 / 其他品种

```js
const hourly = await ctx.data.history({
  symbol: 'XAG-USDT-SWAP', interval: '1h', range: '1mo'
});
const gold = await ctx.data.history({
  symbol: 'XAU-USDT-SWAP', interval: '1d', range: '1y'
});
const fifteen = ctx.data.aggregate(
  await ctx.data.history({ interval: '1m', range: '5d' }), 15
);
```

`history` 返回 Promise<Bar[]>，通过本机 /api/history 读取 OKX 公开数据，不改变主图周期。默认参数是 XAG-USDT-SWAP / 1m / 5d，缓存 5 分钟。可用 interval 为 1m、3m、5m、15m、30m、1h、2h、4h、1d；range 示例为 5d、1mo、1y。其他分钟周期用 aggregate。分钟、小时和日线均包含完整 24 小时数据；接口最多向上游翻页 60,000 根基础 K 线，因此长范围不保证返回完整分钟历史，且数据不早于产品上线。

日线 time 是 UTC 零点，date 是 UTC 日期。分钟源时间来自上游开盘时间戳，包含周末和传统期货闭市期间的价格。

## 5. 画线与标记

### 折线 / 趋势线

```js
const line = ctx.draw.line([
  { time: 1792980000, value: 54 },
  { time: 1792983600, value: 55 }
], { color: '#3979d0', lineWidth: 2, title: '趋势线' });
line.setData(points);              // 替换全部数据，时间必须递增且不重复
line.update({ time, value });       // 更新最后一点或追加新点
line.applyOptions({ color: '#d66a5c' });
line.remove();                    // 可手动删除；卸载时也自动删除
```

两点形成线段。`raw` 是 Lightweight Charts 的原始 series 对象。若数据时间不在主图 K 线时间上，图表会增加对应的时间点。需要严格沿用主图时间轴时，可把独立周期指标投影到主图：

```js
line.setData(ctx.data.align(points));
```

`align` 对每个主图 K 线时间采用不晚于该时间的最近指标点，不插值；早于第一条指标的主图 K 线不输出。它不会自动判断指标是否已收盘。如要求避免回看，先将点的 time 移到该源周期实际结束时间，或只使用已收盘源 K 线。

### 水平价格线

```js
const level = ctx.draw.horizontal(54.5, {
  title: '阻力', color: '#d66a5c', lineWidth: 1,
  lineStyle: ctx.library.LineStyle.Dashed
});
level.applyOptions({ price: 54.8 });
level.remove();
```

### K 线标记

```js
const markers = ctx.draw.markers([{
  time: ctx.data.chartBars().at(-1).time,
  position: 'belowBar', shape: 'arrowUp', color: '#269b80', text: '信号'
}]);
markers.setMarkers(nextMarkers); // 时间递增，使用主图存在的时间
markers.remove();
```

## 6. 完全自由的 JavaScript 与原始图表

```js
const chart = ctx.chart;        // Lightweight Charts IChartApi
const candles = ctx.candles;    // 主 K 线 ISeriesApi
const L = ctx.library;          // window.LightweightCharts
const histogram = chart.addSeries(L.HistogramSeries, {
  priceScaleId: 'indicator-volume', priceFormat: { type: 'volume' }
});
histogram.priceScale().applyOptions({ scaleMargins: { top: 0.8, bottom: 0 } });
ctx.onCleanup(() => chart.removeSeries(histogram));

const onCrosshair = event => ctx.log(event.time);
chart.subscribeCrosshairMove(onCrosshair);
ctx.onCleanup(() => chart.unsubscribeCrosshairMove(onCrosshair));
```

可直接使用完整 Lightweight Charts v5 API，包含原始 series、价格坐标、时间坐标和自定义 primitive。便捷 `draw` 对象创建的资源自动清理；直接创建的图表资源、DOM、监听器、WebSocket 等，通过 `onCleanup` 或 `onDispose` 释放。

```js
ctx.state.counter = 0; // 每个指标实例私有且跨 hook 保留
ctx.setInterval(() => { ctx.state.counter++; }, 1000); // 自动清理
ctx.setTimeout(() => ctx.log('ready'), 500);           // 自动清理
ctx.onCleanup(() => mySocket.close());
await fetch('/somewhere', { signal: ctx.signal });
```

`ctx.setInterval` 的异步回调不会等待上一次结束；需要串行轮询时自行加 busy 标志。普通原生 `setInterval` 也可用，但需自行清理。

## 7. 完整示例

`examples/sma-15m.js`：无论主图周期如何，都使用 15 分钟行情计算 SMA20，并投影到当前图表时间轴。

`examples/daily-levels.js`：在任意分钟图显示过滤后的 COMEX 交易日日线上一条记录的最高价和最低价。当新交易日尚未生成记录时，`at(-2)` 仍指倒数第二条记录，应按自己的交易日规则选择记录。

`examples/multi-timeframe.js`：主图选 15 分钟，指标同时读取 1、7、40、60 分钟和日线，显示 1 分钟 SMA50 及日线高点。**主图周期只影响显示坐标，不限制任何指标的数据周期。**

编辑器外也可以调用：

```js
await window.SilverIndicators.run(code);
window.SilverIndicators.list();
await window.SilverIndicators.setEnabled('sma-15m-20', false); // 隐藏单个指标
await window.SilverIndicators.remove('sma-15m-20');
```

## 8. 调试与数据边界

- 打开浏览器开发者工具 Console 查看 `[Indicator id]` 日志及异常。
- `onTick` 的价格可以用来更新显示；断线期间的完整开高低收通过 REST 回补。
- volume 是 OKX 合约张数；另有 volume_base（XAG 数量）、volume_quote（USDT 数量）及 confirmed（是否已收盘）。
- `onData` 的最后一根通常未收盘，策略结果可能随行情变化。
- 大量同步计算会阻塞整个页面；可自由使用 Web Worker 移走计算。
- 本项目只显示图表，不包含下单接口。

库文档：https://tradingview.github.io/lightweight-charts/ 。本机 API：`/api/market`（启动历史）、`/api/feed`（10 秒缓存的 REST 回补行情）、`/api/history?symbol=XAG-USDT-SWAP&interval=1h&range=1mo`（独立历史）。


## 行情回放

点击工具栏「回放」，可用北京时间定位、拖动进度、逐根前进、1×/2×/5×自动播放。回放范围取决于当前已加载的一分钟数据；定位到范围外会落在边界上。每一步推进当前图表周期的一根 K 线。切换周期后按新周期前进。

回放会冻结一份数据快照，实时 feed 在后台继续接收。`ctx.data.bars()`、`chartBars()`、`history()` 均截断在回放时点；未结束的高周期 K 线仅用回放之前的一分钟数据计算，不读取该根未来的最高、最低或收盘价。回放中的历史请求仍需行情接口可用，分钟历史范围受提供方限制；无法获取时抛出错误。

`ctx.replay.active` 表示正在回放。前进触发 `onData(ctx, {reason: 'replay', bar})` 和 `onTick(ctx, {source: 'replay', replay: true, time, price})`。新进入回放、重新定位或退出回放时，系统卸载并重新加载指标，清空 `ctx.state` 及受托管绘图和计时器；会重新执行脚本顶层代码、`onInit` 和初始 `onData`。指标自行保存在 window、localStorage 等处的数据需自行管理。

## Alert：Webhook、中文 TTS 与桌面通知

图表中已加载的指标会自动接入事件，但**仍需在 Alert 面板保存并启用规则，选择提醒渠道**。选择“所有指标”即可包含高斯包络线，也可按指标 id、标的及事件名称筛选。事件输入 `*` 匹配全部，也支持按事件名或提醒文字中的关键字匹配。设置存入本机 `.webhook.json`，规则可跨标的复用。

### 已有指标无需修改代码

- 指标脚本中的 `window.alert(message)` / `alert(message)` 会自动转换为 `indicator-alert` 事件，提醒文字保留在 `data.message` 中，不再阻塞图表。
- 指标已有的 `ctx.alert.emit(name, data, options)` 继续可用；`ctx.emit` 和 `ctx.events.emit` 是等价入口。
- `ctx.draw.markers` 创建的红、绿、紫箭头/三角形会自动观察。现有高斯包络线通过 `ctx.state.result.markers` 及轨道颜色适配，无需修改其绘图脚本，也不改变计算公式。
- 事件分别为 `triangle.red`、`triangle.green`、`triangle.purple`。只对实时更新中新出现的三角提醒，初次加载、切周期、恢复显示及加载历史时建立基线，不补发历史标记。高斯包络会重绘，提醒表示“现在出现”，不是未来确认的交易信号；同一周期、位置、颜色和方向曾提醒过的三角，消失后重新出现不重复提醒。
- 任意直接绘制到 Canvas、且不暴露标记数据的第三方脚本不能通用识别，需给运行层增加适配。这里的自动接入适用于上述接口及项目现有高斯脚本。

初始化、隐藏指标、回放及失效行情期间不推送。多晶硅按一分钟轮询，无法观察两次更新之间的所有信号。提醒仅针对当前页面已加载、启用的指标；Watchlist 报价轮询不会自动运行其他标的的指标。

### 中文 TTS

启用语音后，可选择自动跟随三角颜色、`{ticker}红色三角` 或 `{ticker}紫色三角`。`{ticker}` 使用中文标的名并保留合约月份：例如 `多晶硅2705红色三角`、`多晶硅2705紫色三角`；主连使用 `多晶硅主连`，白银使用 `国际白银`。自动模式也能按已有绿色事件读出“绿色三角”。试听文本在面板中显示。

首次使用点击“试听”或“启用 TTS”。语音由浏览器的 SpeechSynthesis 和系统中文语音合成，不使用鸡叫/狗叫音效，也不需要付费 API。旧音效规则会迁移为对应红色/紫色语音模板，其他规则条件保持原样。进入回放或离开页面会取消排队语音。浏览器需支持语音 API，系统需有可用的中文语音；不支持或失败会在面板显示。

### Windows / 桌面通知

每条规则可独立开启桌面通知。点击“允许桌面通知”，由浏览器请求站点通知权限。在 Windows 上使用浏览器的系统通知，包含中文标的、指标、周期、事件及价格。通知权限属于当前站点来源，换一个公网域名后需重新授权。页面关闭后不运行告警；系统勿扰或浏览器权限可能影响通知显示。

### Webhook JSON 与代码框

Webhook 始终以 HTTP POST、`Content-Type: application/json` 发送。默认模式为“自定义 JSON”，接收地址为 `https://etf.peekabo.cc/api/v1/tradingview`。编辑请求体可自由增加、删除字段；发送时只包含模板中的字段，不附加事件元数据或代码框。

默认模板：

```json
{
  "webhook_token": "{{webhook_token}}",
  "symbol": "{{symbol}}",
  "cycle_code": "{{cycle_code}}",
  "signal": "{{signal}}"
}
```

`{{symbol}}` 替换为合约代码如 `PS2705`；`{{symbol_name}}` 为中文如 `多晶硅2705`。`{{cycle_code}}` 默认是分钟整数（3、15、1440），如接收端要求其他编码可在模板中填写固定值或改用 `{{interval}}`（3m、1h、1d）。完整字段值为变量时保留数字或对象类型，嵌入文字中的变量替换为字符串。其他可用变量为 `{{period}}`、`{{event}}`、`{{indicator}}`、`{{time}}`、`{{price}}`、`{{data}}`。

`{{signal}}` 仅映射 **绿色三角 BUY、紫色三角 SELL**。其他未定义的三角不发送含此变量的模板，以免默认为错误方向；语音与桌面通知仍独立执行。自定义事件也可通过 `data.signal` 显式提供 BUY/SELL，但红色三角没有默认交易映射。

令牌保存在服务端忽略文件 `.webhook-secret.json`（或环境变量 `TRADING_WEBHOOK_TOKEN`），模板中使用 `{{webhook_token}}` 引用；网页预览隐藏实际令牌。该服务端令牌只用于上述默认接收地址；其他接收端需在自己的模板中指定自己的凭据。请求体在 Alert 面板以代码框预览，不对真实接收端发送测试交易。

仍可选择“完整事件 JSON”，其请求体示例如下；“JSON + 聊天代码框”在完整事件中额外添加 `content` 字段，供支持的聊天 Webhook 显示。
```json
{
  "id": "事件去重标识",
  "symbol": "PS2705",
  "symbol_name": "多晶硅2705",
  "indicator": "research-gaussian-envelope-v3",
  "indicator_name": "高斯包络线",
  "event": "triangle.purple",
  "period": 3,
  "time": 1792997940,
  "price": 35000,
  "key": "三角位置与颜色去重标识",
  "data": {
    "color": "purple",
    "marker_color": "#ff00ff",
    "direction": "down",
    "signal_time": 1792997940,
    "signal_price": 35000,
    "message": "紫色三角出现"
  },
  "replay": false,
  "rule": {"id": "purple-alert", "name": "紫色三角提醒"},
  "sent_at": "2026-10-27T01:00:00+00:00"
}
```

`time` 和 `signal_time` 是标记所在 K 线的开始时间（Unix 秒）；`sent_at` 是实际发送时间。`symbol` 始终保留接口代码供程序处理，中文显示及 TTS 用 `symbol_name`。

显式事件接口仍返回 Promise，可在实时 `onTick` 或 `onData` 的 `feed` / `backfill` 中使用：

```js
await ctx.alert.emit('cross-above', { threshold: 35000 }, { key: '自己的稳定事件 id' });
```

默认以标的、指标 id、图表周期、事件名和 key 去重，key 默认为当前图表 K 线开始时间。前后端保留最近 10000 个标识，重启/刷新不提供永久去重。声音、桌面通知、Webhook 独立执行，一个渠道失败不会阻塞其他渠道。Webhook 每次请求 10 秒超时，不自动重试，避免接收端已处理但响应丢失时重复推送。结果及失败原因显示在 Alert 面板的最近触发记录中。


## 新增标的代码

`SZ002173` 创新医疗、`SH600021` 上海电力：AKShare 不复权 A 股 OHLCV，CNY/股、成交量为股；分钟数据每 60 秒轮询，失败指数退避，休市不填造 K 线。`XAU-USDT-SWAP` 为 OKX XAU/USDT TradFi 黄金永续行情，使用独立 REST 数据及 WebSocket。新标的同样支持 1～40 连续分钟周期、独立数据读取、图表回放与 Alert。`symbol` 保留上述接口代码，中文 `symbol_name` 与 TTS 名称分别为创新医疗、上海电力、国际黄金。


## 指标持久化与恢复

通过编辑器「运行 / 保存」或加载 `.js` 的指标，自动把源码、已移除的指标 id、隐藏状态按标的保存到服务端 `.indicator-state.json`。文件位于项目根目录，不随 Cloudflare 域名改变，也不放在公开静态目录中。指标面板显示磁盘保存状态；刷新及重启服务后自动恢复，编辑器也恢复保存的源码。

浏览器 `localStorage` 作为缓存与待同步备份。首次打开新版本时，若服务器还没有当前标的的记录，会自动迁移当前浏览器、当前域名中的旧记录。以后以服务器记录为主；服务器明确保存的空列表不会被旧缓存重新覆盖。换浏览器或换临时公网域名仍从同一台本机服务恢复。此前已被清除且未保存到服务器的代码无法自动找回，需重新导入一次。

脚本加载完成后才允许新增或编辑，避免慢恢复覆盖刚导入的指标。磁盘同步失败会保留运行中的指标，使用浏览器备份，按 1～30 秒指数退避重试；网络恢复也会重试。看到“已保存到本机磁盘”表示服务器已确认写入。“浏览器备份已保存；磁盘同步待重试”表示仍需本机服务恢复连接。不同标的分别保存，切换到未配置指标的标的不会自动复制另一标的的指标。

只持久化脚本及启用配置；`ctx.state`、图表资源和计时器在恢复时重新创建，不对任意 JavaScript 闭包做序列化。仅直接调用 `api.register()` 注册的运行时对象不含可保存的源码；需要持久化时使用 `api.run(code)` 或指标编辑器/文件导入。可以通过 `await window.SilverIndicators.ready` 等待恢复完成，`await window.SilverIndicators.flush()` 等待当前待同步操作完成（失败返回 false）。
