Add Telegram watcher, dashboard docs, and deployment config
- scripts/telegram-watcher.js: push ENTER signals from signals.csv to Telegram - DASHBOARD_GUIDE.md: complete dashboard field-by-field documentation - package.json: add watch:telegram npm script - .env.example: deployment env template - .gitignore: exclude ecosystem.config.cjs/start.sh (server-only with paths) Watcher reads recommendation column and triggers on: - transition NO_TRADE -> ENTER:side - flip ENTER:UP <-> ENTER:DOWN - 30s cooldown prevents spam
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
# Telegram (revoke 后用新 token)
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
TELEGRAM_COOLDOWN_MS=30000
|
||||||
|
|
||||||
|
# Polygon RPC
|
||||||
|
POLYGON_RPC_URL=https://lb.drpc.live/polygon/your-key
|
||||||
|
POLYGON_WSS_URLS=wss://lb.drpc.live/polygon/your-key
|
||||||
|
|
||||||
|
# Proxy (服务器本地通常不需要;国内云服务器必须)
|
||||||
|
# HTTPS_PROXY=http://user:pass@ip:7890
|
||||||
@@ -2,7 +2,10 @@ node_modules/
|
|||||||
logs/
|
logs/
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
!.env.example
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
ecosystem.config.cjs
|
||||||
|
start.sh
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# 仪表盘逐项解读手册(基于代码逻辑链)
|
||||||
|
|
||||||
|
本文严格按 `src/index.js`、`src/engines/*.js` 的执行顺序,逐行推演仪表盘上每一行/每一个数字是如何生成的、它代表什么、如何判断信号方向。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、整体渲染流程
|
||||||
|
|
||||||
|
主循环 `main()`(`src/index.js:398-731`)每 `CONFIG.pollIntervalMs`(默认 ~1500ms)刷新一屏。每一帧的渲染数据来自六个数据源,按以下顺序组装:
|
||||||
|
|
||||||
|
```
|
||||||
|
Binance WS (实时成交) → spotPrice ─┐
|
||||||
|
Polymarket Live WS (BTC/USD) → polymarketWsPrice ─┐
|
||||||
|
Chainlink on-chain WSS/HTTP → chainlinkWsPrice ─┼─→ currentPrice (PRICE TO BEAT 当前价)
|
||||||
|
fetchChainlinkBtcUsd() (HTTP 回退)
|
||||||
|
Binance REST klines (1m×240, 5m×200) ──→ 1分钟K线序列,喂给所有指标
|
||||||
|
Polymarket REST (Gamma + CLOB) ──→ 市场/订单簿/UP-DOWN价
|
||||||
|
策略引擎 ──→ scoreDirection → applyTimeAwareness → computeEdge → decide
|
||||||
|
```
|
||||||
|
|
||||||
|
随后调用 `renderScreen()`(`src/index.js:71-79`)用 ANSI 重定位光标 + `clearScreenDown` 把整张表重写到终端,**所以你看到的不是新增日志,而是"整帧覆写"**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、逐字段拆解
|
||||||
|
|
||||||
|
仪表盘上从上到下共有五个区段,每个区段的字段解释如下。
|
||||||
|
|
||||||
|
### 1. 顶部信息(市场识别)
|
||||||
|
|
||||||
|
#### `Bitcoin Up or Down - July 21, 4:30AM-4:45AM ET`
|
||||||
|
来源:`poly.market?.question`(`src/index.js:648`)。这是 Polymarket Gamma API 返回的 `question` 字段,等价于"市场标题",标识当前 15 分钟窗口的起止时间(美国东部时间)。
|
||||||
|
|
||||||
|
#### `Market: btc-updown-15m-1784622600`
|
||||||
|
来源:`poly.market?.slug`(`src/index.js:649`)。是 Polymarket 的市场唯一 slug。其中 `1784622600` 是市场开始时的 Unix 时间戳秒数(用作市场 ID)。市场选取逻辑:
|
||||||
|
|
||||||
|
- 如果你设置了 `POLYMARKET_SLUG`,则锁定该市场(`src/index.js:285-287`)。
|
||||||
|
- 否则 `pickLatestLiveMarket()` 从 Gamma 系列 `seriesId=10192`(`btc-up-or-down-15m`)的活跃事件中挑"最新正在进行的 15 分钟窗口"(`src/index.js:296-302`,并有 25s 缓存避免重复请求)。
|
||||||
|
- 每 15 分钟滚动一次新窗口(Polymarket 自身的事件节奏)。
|
||||||
|
|
||||||
|
#### `Time left: 04:49`
|
||||||
|
来源:`timeLeftMin`(`src/index.js:452, 658`)。计算方法:
|
||||||
|
|
||||||
|
```
|
||||||
|
settlementMs = poly.market.endDate → 转毫秒
|
||||||
|
timeLeftMin = (settlementMs - Date.now()) / 60_000
|
||||||
|
```
|
||||||
|
|
||||||
|
颜色编码规则(`src/index.js:651-657`):
|
||||||
|
- 绿色:`[10, 15]` 分钟 — EARLY 阶段
|
||||||
|
- 黄色:`[5, 10)` 分钟 — MID 阶段
|
||||||
|
- 红色:`[0, 5)` 分钟 — LATE 阶段(最后冲刺)
|
||||||
|
|
||||||
|
⚠️ **关键解读**:剩余时间是策略相位切换的唯一信号。它直接影响 `decide()` 的入场阈值(见下文 Decision Logic)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 策略预测区
|
||||||
|
|
||||||
|
#### `TA Predict: LONG 63% / SHORT 37%`
|
||||||
|
这是整张表**最关键**的输出,由两条流水线生成:
|
||||||
|
|
||||||
|
**第一步 — `scoreDirection()`(`src/engines/probability.js:3-53`)**:
|
||||||
|
从六个原始信号累加 up/down 计数,起始都是 `1`:
|
||||||
|
|
||||||
|
| 触发条件 | 加成 |
|
||||||
|
|---|---|
|
||||||
|
| `price > vwap` | up += 2 |
|
||||||
|
| `price < vwap` | down += 2 |
|
||||||
|
| `vwapSlope > 0` | up += 2 |
|
||||||
|
| `vwapSlope < 0` | down += 2 |
|
||||||
|
| `rsi > 55 && rsiSlope > 0` | up += 2 |
|
||||||
|
| `rsi < 45 && rsiSlope < 0` | down += 2 |
|
||||||
|
| MACD histogram 扩张且为正 | up += 2 |
|
||||||
|
| MACD histogram 扩张且为负 | down += 2 |
|
||||||
|
| MACD 主线 > 0 | up += 1 |
|
||||||
|
| MACD 主线 < 0 | down += 1 |
|
||||||
|
| Heiken Ashi 连续 ≥2 根同色 | 该方向 += 1 |
|
||||||
|
| 跌破 VWAP 后未收回(failed reclaim) | down += 3 ⚠️ |
|
||||||
|
|
||||||
|
最终 `rawUp = up / (up + down)`(`src/engines/probability.js:51`)。
|
||||||
|
|
||||||
|
**第二步 — `applyTimeAwareness()`(`src/engines/probability.js:55-59`)**:
|
||||||
|
随着剩余时间减少,模型预测会被"拉回"到 50/50:
|
||||||
|
|
||||||
|
```
|
||||||
|
timeDecay = remainingMinutes / 15 (clamp 0..1)
|
||||||
|
adjustedUp = 0.5 + (rawUp - 0.5) * timeDecay (clamp 0..1)
|
||||||
|
adjustedDown= 1 - adjustedUp
|
||||||
|
```
|
||||||
|
|
||||||
|
**这意味着**:越临近到期,TA 的偏向越被"折扣",因为只剩下几分钟,已发生的价格走势对最终结果的代表性越弱。极端情形:到 `timeLeftMin=0` 时 `adjustedUp = 0.5` 完全无偏向。
|
||||||
|
|
||||||
|
仪表盘上 `Predict` 就是这个 `adjustedUp/adjustedDown`(`src/index.js:537-543`)。
|
||||||
|
|
||||||
|
#### `Heiken Ashi: green x8`
|
||||||
|
来源:`computeHeikenAshi()` + `countConsecutive()`(`src/indicators/heikenAshi.js`)。
|
||||||
|
最后 N 根 Heiken Ashi 同色(绿=收>开)的连击数。`x8` 表示当前已经连续 8 根绿色 K 线 → 短期明显多头。
|
||||||
|
|
||||||
|
⚠️ **注意**:在 TA 评分里只触发 +1(`heikenCount >= 2` 时单边 +1,计数本身没有单调加权,见 `src/engines/probability.js:44-47`)。**它是确认信号,不是主导信号。**
|
||||||
|
|
||||||
|
#### `RSI: 52.6 ↑`
|
||||||
|
来源:`computeRsi(closes, period=14)` + `slopeLast(rsiSeries, 3)`(`src/index.js:465-473`)。
|
||||||
|
`52.6` 是当前 14 周期 RSI,`↑` 表示最近 3 根的斜率 > 0。
|
||||||
|
|
||||||
|
⚠️ **关键解读 — RSI 在本系统里被硬阈值化**(`src/engines/probability.js:29-32`):
|
||||||
|
- 只有 `rsi > 55 且 rsiSlope > 0` 才触发 up += 2
|
||||||
|
- 只有 `rsi < 45 且 rsiSlope < 0` 才触发 down += 2
|
||||||
|
- **52.6(中性区)→ 零贡献**
|
||||||
|
|
||||||
|
所以"RSI 52.6 ↑"看起来像多头,但 TA 计分里完全没贡献。它只是显示给你看,不影响模型。
|
||||||
|
|
||||||
|
#### `MACD: bullish (expanding)`
|
||||||
|
来源:`computeMacd(closes, 12, 26, 9)`(`src/index.js:475`)。
|
||||||
|
`bullish (expanding)` 表示 `hist > 0` 且 `histDelta > 0`(`src/index.js:519-523`)。
|
||||||
|
|
||||||
|
`expanding` 触发 `up += 2`(`src/engines/probability.js:34-38`)。所以这个字段是 TA 模型里**真实的加分项**。
|
||||||
|
|
||||||
|
#### `Delta 1/3Min: -$4.40, -0.01% | -$1.03, -0.00%`
|
||||||
|
来源:`src/index.js:525-530`:
|
||||||
|
- `delta1m = lastClose - klines1m[-2].close`
|
||||||
|
- `delta3m = lastClose - klines1m[-4].close`
|
||||||
|
|
||||||
|
⚠️ **它只是展示,不参与 TA 计分**。在 `scoreDirection()` 的输入里没有 delta1/delta3。所以这个字段是"信息参考",**不是模型输入**。
|
||||||
|
|
||||||
|
#### `VWAP: 65,923 (0.63%) | slope: UP`
|
||||||
|
来源:`computeSessionVwap()` + `computeVwapSeries()` + `slopeLast()`(`src/index.js:457-462`)。
|
||||||
|
- `65,923` = 当日开盘至今的成交量加权均价
|
||||||
|
- `0.63%` = 当前价相对 VWAP 的偏离(price 在 VWAP 之上,正数)
|
||||||
|
- `slope: UP` = 最近 `vwapSlopeLookbackMinutes` 分钟的 VWAP 单调变化方向为正
|
||||||
|
|
||||||
|
这两个字段**在 TA 计分里贡献最大**(`src/engines/probability.js:19-27`):
|
||||||
|
- `price > vwap` → up += 2
|
||||||
|
- `vwapSlope > 0` → up += 2
|
||||||
|
|
||||||
|
合计 +4,是单项最大的多头信号源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Polymarket 实时市场区
|
||||||
|
|
||||||
|
#### `POLYMARKET: ↑ UP 0.94¢ | ↓ DOWN 0.05¢`
|
||||||
|
来源:`fetchPolymarketSnapshot()`(`src/index.js:305-396`)。
|
||||||
|
|
||||||
|
**价格优先级**(`src/index.js:388-389`):
|
||||||
|
```
|
||||||
|
upPrice = CLOB buy 价 ?? Gamma outcomePrices[upIndex]
|
||||||
|
downPrice = CLOB buy 价 ?? Gamma outcomePrices[downIndex]
|
||||||
|
```
|
||||||
|
|
||||||
|
先取订单簿实时买价(=`bestAsk`,即你能立即以多大概率买到的成本),失败时回落到 Gamma 的 midpoint 价格。
|
||||||
|
|
||||||
|
**如何解读**:
|
||||||
|
- `0.94¢` 意味着押 UP 每份成本 $0.94,赢了收到 $1.00 → **潜在收益 ≈ 6.4%**
|
||||||
|
- `0.05¢` 意味着押 DOWN 每份成本 $0.05,赢了收到 $1.00 → **潜在收益 ≈ 20 倍**
|
||||||
|
|
||||||
|
⚠️ **赔率反映 Polymarket 集体押注的预期概率**(隐含 UP ≈ 94%,DOWN ≈ 5%)。但这个"市场概率"和你的 TA 模型概率可以不一致 → 这就是 **edge**(见下文)。
|
||||||
|
|
||||||
|
#### `Liquidity: 6,027`
|
||||||
|
来源:`poly.market.liquidityNum ?? poly.market.liquidity`(`src/index.js:576-578`)。
|
||||||
|
是 Polymarket 该市场的报价方总资金(USDC)。**只显示,不参与任何计算**。
|
||||||
|
|
||||||
|
> 解读建议:低于 ~$1000 的市场深度很浅,挂单价差大(spread 大),下单时滑点可能吃掉所有 edge。
|
||||||
|
|
||||||
|
#### `Time left: 04:49`(第二个)
|
||||||
|
来源与顶部 `Time left` **不同**:这里是 `settlementLeftMin`(基于 `poly.market.endDate`),而顶部是基于 `timing.remainingMinutes`(基于 15 分钟 K 线窗口)。多数情况下两者数值接近但不完全相等。
|
||||||
|
|
||||||
|
颜色用同样的 EARLY/MID/LATE 编码(`src/index.js:660-668`)。
|
||||||
|
|
||||||
|
#### `PRICE TO BEAT: $66,274`
|
||||||
|
**这是整个仪表盘决定胜负的关键数字。**
|
||||||
|
|
||||||
|
来源链路(`src/index.js:585-597`):
|
||||||
|
1. 进入新市场(slug 变化)时清空 `priceToBeatState`。
|
||||||
|
2. 当 `marketStartMs <= Date.now()` 且当前 `currentPrice` 可用时,**锁存**当前 Chainlink 价格作为本市场的 PRICE TO BEAT。
|
||||||
|
3. 之后 PRICE TO BEAT 固定不变(直到切到下一个市场)。
|
||||||
|
|
||||||
|
锁定值来源优先级(`src/index.js:435-439`):
|
||||||
|
```
|
||||||
|
polymarket_ws (Polymarket Live WS BTC/USD) > chainlink_ws (Chainlink 链上 WSS) > fetchChainlinkBtcUsd() (HTTP)
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ 也就是说:**PRICE TO BEAT 取的是 Polymarket UI 同一个 Chainlink 喂价源**,不是 Binance 现货!这能避免"Binance 与 Polymarket 数据源不同步"造成的胜负误判。
|
||||||
|
|
||||||
|
**到期判定规则**(Polymarket 官方):到期时刻 BTC 价格(同样取 Chainlink)> PRICE TO BEAT → UP 赢;否则 DOWN 赢。
|
||||||
|
|
||||||
|
#### `CURRENT PRICE: $66,291.57 ↓ (+$17.27)`
|
||||||
|
来源:
|
||||||
|
- 价格:`currentPrice = chainlink.price`(`src/index.js:581`),来源与 PRICE TO BEAT 相同
|
||||||
|
- 颜色箭头:与上一帧比较(`colorPriceLine`,`src/index.js:109-131`)
|
||||||
|
- `↑` 绿色:上涨
|
||||||
|
- `↓` 红色:下跌
|
||||||
|
- `+$17.27`:当前价 − PRICE TO BEAT(`ptbDelta`,`src/index.js:606-618`)
|
||||||
|
- 正数绿色:在 BEAT 之上 → 倾向 UP 赢
|
||||||
|
- 负数红色:在 BEAT 之下 → 倾向 DOWN 赢
|
||||||
|
|
||||||
|
**解读**:当前比 BEAT 高 $17.27。要反转需要 BTC 在剩余时间内回撤至少 $17.27。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Binance 现货参考区
|
||||||
|
|
||||||
|
#### `BTC (Binance): $66,338 (+$46.02, +0.07%)`
|
||||||
|
来源:`binanceStream.getLast()?.price ?? fetchLastPrice()`(`src/index.js:580`)。
|
||||||
|
- `+46.02`:相对上一帧 WS tick 的变动
|
||||||
|
- 括号外的 `(diff)`:Binance 现货价 − 当前 Chainlink 价(`src/index.js:636-644`)
|
||||||
|
|
||||||
|
⚠️ **只是参考,不参与 TA 计分**。但可以用来判断:
|
||||||
|
- 两个数据源是否脱钩(diff 持续 >$50 → 数据源不一致风险)
|
||||||
|
- 现货是否比 Chainlink 先行(>0 → 现货领先,可能预示上行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 时间与会话区
|
||||||
|
|
||||||
|
#### `ET | Session: 04:40:10 | Europe`
|
||||||
|
来源:`fmtEtTime(new Date())` + `getBtcSession(new Date())`(`src/index.js:173-199`)。
|
||||||
|
|
||||||
|
`Session` 是基于 UTC 小时划分的**全球流动性时段**(仅显示,不参与计算):
|
||||||
|
- Asia: 0–7 UTC
|
||||||
|
- Europe: 7–15 UTC
|
||||||
|
- US: 13–21 UTC
|
||||||
|
- 重叠时段会有 "Europe/US overlap" / "Asia/Europe overlap"
|
||||||
|
|
||||||
|
**实战含义**:欧美重叠时段(13–16 UTC)波动率最大,亚盘(0–7 UTC)波动最小。短窗口(15 分钟)市场对流动性敏感,低流动性时段 edge 模型容易失效。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、决策逻辑(用户最关心的"该怎么判断")
|
||||||
|
|
||||||
|
虽然仪表盘**不直接显示**入场信号,但主循环每帧都在调用:
|
||||||
|
|
||||||
|
```
|
||||||
|
rec = decide({ remainingMinutes, edgeUp, edgeDown, modelUp, modelDown })
|
||||||
|
```
|
||||||
|
|
||||||
|
代码在 `src/engines/edge.js:23-48`:
|
||||||
|
|
||||||
|
### Step 1 — 计算市场隐含概率
|
||||||
|
|
||||||
|
```
|
||||||
|
marketUp = marketYes / (marketYes + marketNo)
|
||||||
|
marketDown = 1 - marketUp
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ 因为订单簿买价 `marketYes + marketNo ≠ 1`(spread),所以要先归一化。本例:UP 0.94 + DOWN 0.05 = 0.99 → marketUp ≈ 94.9%。
|
||||||
|
|
||||||
|
### Step 2 — 计算 edge
|
||||||
|
|
||||||
|
```
|
||||||
|
edgeUp = modelUp - marketUp // TA 对 UP 的看法 − 市场对 UP 的隐含概率
|
||||||
|
edgeDown = modelDown - marketDown
|
||||||
|
```
|
||||||
|
|
||||||
|
**edge 才是交易价值所在**:模型和市场分歧越大,edge 越高,理论上"无风险套利空间"越大。
|
||||||
|
|
||||||
|
### Step 3 — 三阶段阈值(剩余时间分桶)
|
||||||
|
|
||||||
|
| Phase | 剩余时间 | edge 阈值 | modelProb 阈值 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| EARLY | > 10 min | 5% | 55% |
|
||||||
|
| MID | 5–10 min | 10% | 60% |
|
||||||
|
| LATE | ≤ 5 min | 20% | 65% |
|
||||||
|
|
||||||
|
### Step 4 — 入场判定
|
||||||
|
|
||||||
|
只有当:
|
||||||
|
1. `bestEdge ≥ 阶段阈值`
|
||||||
|
2. `bestModelProb ≥ 阶段概率下限`
|
||||||
|
|
||||||
|
才返回 `action: ENTER`,否则 `NO_TRADE`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、回到你的示例数据
|
||||||
|
|
||||||
|
```
|
||||||
|
TA Predict: LONG 63% / SHORT 37% ← timeAware 模型概率
|
||||||
|
Heiken Ashi: green x8 ← 确认(+1 已包含)
|
||||||
|
RSI: 52.6 ↑ ← 中性区,零贡献
|
||||||
|
MACD: bullish (expanding) ← +2(关键多头信号)
|
||||||
|
Delta 1/3: -$4.40, -0.01% ← 展示用,不入模型
|
||||||
|
VWAP: 65,923 (0.63%) | UP ← price>vwap(+2) + slope>0(+2) = +4
|
||||||
|
|
||||||
|
POLYMARKET: ↑ UP 0.94¢ | ↓ DOWN 0.05¢
|
||||||
|
PRICE TO BEAT: $66,274
|
||||||
|
CURRENT PRICE: $66,291.57 ↓ (+$17.27) ← 当前比 BEAT 高 $17.27
|
||||||
|
```
|
||||||
|
|
||||||
|
**逐项核对模型**(你看到的 LONG 63% 是怎么来的):
|
||||||
|
|
||||||
|
`timeLeftMin ≈ 4.8`(4:49)→ `timeDecay = 4.8/15 = 0.32`
|
||||||
|
|
||||||
|
假设其他指标命中情况(基于字段):
|
||||||
|
- VWAP +4(price>vwap 且 slope up)
|
||||||
|
- MACD +2(expanding bullish)
|
||||||
|
- Heiken Ashi +1(green ×8,count ≥ 2)
|
||||||
|
- RSI 52.6:中性,无加成
|
||||||
|
- failedVwapReclaim:未触发
|
||||||
|
|
||||||
|
→ upScore ≈ 1+4+2+1 = 8
|
||||||
|
→ downScore ≈ 1
|
||||||
|
→ rawUp = 8/9 ≈ 0.889
|
||||||
|
|
||||||
|
→ adjustedUp = 0.5 + (0.889 - 0.5) × 0.32 = 0.624 ≈ **62%** ✓
|
||||||
|
|
||||||
|
(与面板的 63% 吻合,差距来自小数四舍五入或个别次要信号 0/1 边界)
|
||||||
|
|
||||||
|
**市场隐含**:marketUp ≈ 0.94/0.99 ≈ 0.949
|
||||||
|
**edgeUp** = 0.62 − 0.949 = **−0.33**(负 edge)
|
||||||
|
|
||||||
|
LATE 阶段(剩 <5 分钟)要求 edge ≥ 0.20 且 modelProb ≥ 0.65:
|
||||||
|
- edge −0.33 < 0.20 → ❌
|
||||||
|
- modelProb 0.62 < 0.65 → ❌
|
||||||
|
- **结论:`NO_TRADE`**
|
||||||
|
|
||||||
|
也就是说:**这个时刻模型和市场完全同向(都看 UP),没有可赚的 edge。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、这套仪表盘的设计哲学
|
||||||
|
|
||||||
|
1. **数据源优先级**:Polymarket Live WS(直接复用 Polymarket UI 的 BTC/USD Chainlink 喂价)→ 链上 Chainlink WSS → HTTP 回退。所有"涨跌判定"逻辑都用 Chainlink 价,避免与 Polymarket 的结算价脱钩。
|
||||||
|
2. **Binance 现货只作参考**,不进入 TA 计分。
|
||||||
|
3. **TA 模型信号是堆叠式评分**(不是加权平均),单指标上限 +4(VWAP),MACD 上限 +2,Heiken 上限 +1,RSI 硬阈值化(中位区无效)。
|
||||||
|
4. **时间感知是衰减机制**:剩余越少,模型预测越往 50/50 拉回。LATE 阶段 edge 必须 ≥20%,几乎只在模型和市场严重分歧时才入场。
|
||||||
|
5. **入场动作隐藏在 CSV log 中**(`./logs/signals.csv`),仪表盘只显示中间过程。命令行不会有"BUY UP"提示,所有决策要在 `signals.csv` 里看 `recommendation` 字段:`ENTER:UP:LATE:STRONG` 之类的格式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、常见误读警示
|
||||||
|
|
||||||
|
| 误读 | 正确认知 |
|
||||||
|
|---|---|
|
||||||
|
| "RSI 52.6 ↑ = 多头信号" | 中性区,**零贡献** |
|
||||||
|
| "Delta 1/3 显示跌 → 模型看空" | Delta 只是展示,**不入模型** |
|
||||||
|
| "Predict 63% = 应该买 UP" | Predict 是 modelProb,需要和 marketUp 比较才算 edge |
|
||||||
|
| "TIME LEFT 04:49 还有时间" | LATE 阶段,**入场门槛最严**(edge ≥20%、prob ≥65%) |
|
||||||
|
| "UP 0.94¢ 稳赢要下" | 押 UP 收益仅 ~6%,DOWN 0.05¢ 才是 20 倍冷门 |
|
||||||
|
| "VWAP 0.63% 很弱" | 0.63% 在 15 分钟窗口是**显著偏离**,是核心多头证据 |
|
||||||
|
| "BTC Binance 和 Chainlink 价差 = 信号" | 价差是数据源同步度问题,**非交易信号** |
|
||||||
|
| "Heiken Ashi x8 决定方向" | 仅触发 +1,影响远小于 VWAP/MACD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、文件落点速查
|
||||||
|
|
||||||
|
| 你想了解的内容 | 文件:行 |
|
||||||
|
|---|---|
|
||||||
|
| 仪表盘渲染主循环 | `src/index.js:398-731` |
|
||||||
|
| PRICE TO BEAT 锁存逻辑 | `src/index.js:585-597` |
|
||||||
|
| Polymarket 价抓取(含回退) | `src/index.js:305-396` |
|
||||||
|
| TA 信号评分细节 | `src/engines/probability.js:3-53` |
|
||||||
|
| 时间感知衰减 | `src/engines/probability.js:55-59` |
|
||||||
|
| 入场决策(edge/threshold) | `src/engines/edge.js:23-48` |
|
||||||
|
| 市场状态分类(regime) | `src/engines/regime.js` |
|
||||||
|
| 时间颜色与剩余阶段编码 | `src/index.js:651-668` |
|
||||||
|
| CSV 决策日志格式 | `src/index.js:407-419, 709-722` |
|
||||||
+2
-1
@@ -4,7 +4,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node src/index.js"
|
"start": "node src/index.js",
|
||||||
|
"watch:telegram": "node scripts/telegram-watcher.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ethers": "^6.11.1",
|
"ethers": "^6.11.1",
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = path.resolve(__dirname, "..");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { applyGlobalProxyFromEnv } = await import(path.join(ROOT, "src/net/proxy.js"));
|
||||||
|
applyGlobalProxyFromEnv();
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
const BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
|
||||||
|
const CHAT_ID = process.env.TELEGRAM_CHAT_ID;
|
||||||
|
const CSV_PATH = process.env.SIGNALS_CSV || path.join(ROOT, "logs/signals.csv");
|
||||||
|
const COOLDOWN_MS = Number(process.env.TELEGRAM_COOLDOWN_MS) || 30_000;
|
||||||
|
const DRY_RUN = process.env.DRY_RUN === "true";
|
||||||
|
const POLY_BASE = process.env.POLYMARKET_BASE_URL || "https://polymarket.com/zh/event";
|
||||||
|
|
||||||
|
if (!DRY_RUN && (!BOT_TOKEN || !CHAT_ID)) {
|
||||||
|
console.error("[watcher] Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID");
|
||||||
|
console.error("[watcher] Or set DRY_RUN=true to log without sending.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevSide = null;
|
||||||
|
let lastSentAt = 0;
|
||||||
|
|
||||||
|
async function send(text) {
|
||||||
|
if (DRY_RUN) {
|
||||||
|
console.log("[watcher] DRY_RUN:", text.replace(/\n/g, " | "));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ chat_id: CHAT_ID, text, parse_mode: "HTML" })
|
||||||
|
});
|
||||||
|
if (!r.ok) console.error("[watcher] Telegram error:", await r.text());
|
||||||
|
else console.log(`[watcher] sent ${new Date().toISOString()}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[watcher] send failed:", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtTime(min) {
|
||||||
|
const m = Math.floor(min);
|
||||||
|
const s = Math.floor((min - m) * 60);
|
||||||
|
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMessage(hdr, row, side, phase, strength, slug) {
|
||||||
|
const v = (k) => row[hdr.indexOf(k)];
|
||||||
|
const isUp = side === "UP";
|
||||||
|
const edge = Number(isUp ? v("edge_up") : v("edge_down"));
|
||||||
|
const model = Number(isUp ? v("model_up") : v("model_down"));
|
||||||
|
const mktYes = Number(isUp ? v("mkt_up") : v("mkt_down"));
|
||||||
|
const mktNo = Number(isUp ? v("mkt_down") : v("mkt_up"));
|
||||||
|
const mktSum = mktYes + mktNo;
|
||||||
|
const mkt = mktSum > 0 ? mktYes / mktSum : mktYes;
|
||||||
|
const lines = [
|
||||||
|
`🚨 ENTER <b>${side}</b> — ${phase} (${strength})`,
|
||||||
|
`Edge: ${edge >= 0 ? "+" : ""}${(edge * 100).toFixed(1)}%`,
|
||||||
|
`Model ${(model * 100).toFixed(1)}% vs Market ${(mkt * 100).toFixed(1)}%`,
|
||||||
|
`Time left: ${fmtTime(Number(v("time_left_min")))}`,
|
||||||
|
`Regime: ${v("regime")}`,
|
||||||
|
v("timestamp")
|
||||||
|
];
|
||||||
|
if (slug) lines.push(`🔗 <a href="${POLY_BASE}/${slug}">Open on Polymarket</a>`);
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSlug() {
|
||||||
|
if (!fs.existsSync("./logs")) return null;
|
||||||
|
const files = fs.readdirSync("./logs")
|
||||||
|
.filter((f) => f.startsWith("polymarket_market_") && f.endsWith(".json"))
|
||||||
|
.map((f) => ({ f, mtime: fs.statSync(path.join("./logs", f)).mtimeMs }))
|
||||||
|
.sort((a, b) => b.mtime - a.mtime);
|
||||||
|
if (!files.length) return null;
|
||||||
|
try {
|
||||||
|
const m = JSON.parse(fs.readFileSync(path.join("./logs", files[0].f), "utf8"));
|
||||||
|
return m.slug || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function readLast() {
|
||||||
|
if (!fs.existsSync(CSV_PATH)) return null;
|
||||||
|
const content = fs.readFileSync(CSV_PATH, "utf8");
|
||||||
|
const lines = content.trim().split("\n");
|
||||||
|
if (lines.length < 2) return null;
|
||||||
|
const hdr = lines[0].split(",");
|
||||||
|
const row = lines[lines.length - 1].split(",");
|
||||||
|
return { hdr, row, rec: row[hdr.indexOf("recommendation")] || "" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function watch() {
|
||||||
|
if (!fs.existsSync(CSV_PATH)) {
|
||||||
|
console.log(`[watcher] waiting for ${CSV_PATH} ...`);
|
||||||
|
setTimeout(watch, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[watcher] watching ${CSV_PATH} (cooldown ${COOLDOWN_MS}ms${DRY_RUN ? ", DRY_RUN" : ""})`);
|
||||||
|
|
||||||
|
fs.watch(CSV_PATH, { persistent: true }, () => {
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const data = readLast();
|
||||||
|
if (!data) return;
|
||||||
|
const side = data.rec.startsWith("ENTER") ? data.rec.split(":")[1] : null;
|
||||||
|
if (!side) {
|
||||||
|
prevSide = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [, , phase, strength] = data.rec.split(":");
|
||||||
|
const flipped = side !== prevSide;
|
||||||
|
const cooled = Date.now() - lastSentAt > COOLDOWN_MS;
|
||||||
|
if (flipped && cooled) {
|
||||||
|
const msg = buildMessage(data.hdr, data.row, side, phase, strength, getCurrentSlug());
|
||||||
|
await send(msg);
|
||||||
|
lastSentAt = Date.now();
|
||||||
|
}
|
||||||
|
prevSide = side;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[watcher] error:", e.message);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
watch();
|
||||||
Reference in New Issue
Block a user