From cd988d9c3ab97721bbe15c69e0a30c0f7391605c Mon Sep 17 00:00:00 2001 From: doge-8 Date: Sun, 31 May 2026 13:49:36 +0800 Subject: [PATCH] Initial commit --- .env.example | 19 + .gitignore | 50 + LICENSE | 21 + README.md | 286 ++ backtest-data/README.md | 104 + backtest-data/analyze.py | 701 +++ backtest-data/collector/.gitkeep | 0 chain-watcher.ts | 105 + collect.sh | 53 + data-collector.ts | 739 ++++ docs/README-USER.md | 128 + docs/STRATEGIES-GUIDE.md | 183 + docs/STRATEGIES-TODO.md | 66 + docs/screenshots/dashboard.png | Bin 0 -> 578221 bytes docs/screenshots/monitor.png | Bin 0 -> 62217 bytes index.html | 6619 ++++++++++++++++++++++++++++ market-configs.ts | 110 + monitor/monitor-server.ts | 678 +++ monitor/monitor.html | 1691 +++++++ monitor/start.sh | 5 + monitor/tg-push.ts | 303 ++ package-lock.json | 2799 ++++++++++++ package.json | 25 + polymarket-pnl.ts | 654 +++ scripts/check-adapter.ts | 54 + scripts/check-balance.ts | 64 + scripts/check-claim-tx.ts | 84 + scripts/check-claimable-onchain.ts | 54 + scripts/check-real.ts | 45 + scripts/claim-once.ts | 110 + scripts/claim-relayer.ts | 151 + scripts/list-tokens.ts | 66 + scripts/test-withdraw.ts | 198 + server.ts | 6559 +++++++++++++++++++++++++++ start.bat | 38 + start.sh | 98 + strategies/STRATEGY-GUIDE.md | 504 +++ strategies/_core/diff-extremes.ts | 84 + strategies/_core/fair-prob.ts | 441 ++ strategies/_core/s6-core.ts | 320 ++ strategies/_runtime/loader.ts | 132 + strategies/d1.ts | 68 + strategies/p1.ts | 135 + strategies/p2.ts | 137 + strategies/registry.ts | 41 + strategies/types.ts | 291 ++ terminal.css | 1300 ++++++ tg-push.ts | 115 + tunnels/tunnel-arl-2t4.sh | 147 + tunnels/tunnel-arl-large.bat | 5 + tunnels/tunnel-arl-large.ps1 | 61 + tunnels/tunnel-arl-ot.sh | 147 + tunnels/tunnel-arl-t4.bat | 5 + tunnels/tunnel-arl-t4.ps1 | 61 + tunnels/tunnel-arl-t4.sh | 147 + 55 files changed, 27001 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 backtest-data/README.md create mode 100644 backtest-data/analyze.py create mode 100644 backtest-data/collector/.gitkeep create mode 100644 chain-watcher.ts create mode 100755 collect.sh create mode 100644 data-collector.ts create mode 100644 docs/README-USER.md create mode 100644 docs/STRATEGIES-GUIDE.md create mode 100644 docs/STRATEGIES-TODO.md create mode 100644 docs/screenshots/dashboard.png create mode 100644 docs/screenshots/monitor.png create mode 100644 index.html create mode 100644 market-configs.ts create mode 100644 monitor/monitor-server.ts create mode 100644 monitor/monitor.html create mode 100755 monitor/start.sh create mode 100644 monitor/tg-push.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 polymarket-pnl.ts create mode 100644 scripts/check-adapter.ts create mode 100644 scripts/check-balance.ts create mode 100644 scripts/check-claim-tx.ts create mode 100644 scripts/check-claimable-onchain.ts create mode 100644 scripts/check-real.ts create mode 100644 scripts/claim-once.ts create mode 100644 scripts/claim-relayer.ts create mode 100644 scripts/list-tokens.ts create mode 100644 scripts/test-withdraw.ts create mode 100644 server.ts create mode 100644 start.bat create mode 100755 start.sh create mode 100644 strategies/STRATEGY-GUIDE.md create mode 100644 strategies/_core/diff-extremes.ts create mode 100644 strategies/_core/fair-prob.ts create mode 100644 strategies/_core/s6-core.ts create mode 100644 strategies/_runtime/loader.ts create mode 100644 strategies/d1.ts create mode 100644 strategies/p1.ts create mode 100644 strategies/p2.ts create mode 100644 strategies/registry.ts create mode 100644 strategies/types.ts create mode 100644 terminal.css create mode 100644 tg-push.ts create mode 100755 tunnels/tunnel-arl-2t4.sh create mode 100644 tunnels/tunnel-arl-large.bat create mode 100644 tunnels/tunnel-arl-large.ps1 create mode 100755 tunnels/tunnel-arl-ot.sh create mode 100644 tunnels/tunnel-arl-t4.bat create mode 100644 tunnels/tunnel-arl-t4.ps1 create mode 100755 tunnels/tunnel-arl-t4.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..97f5d57 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# ================================================== +# Required: only these two; all other config is set in the frontend panel +# ================================================== + +# Polymarket private key (used to generate API credentials on first run; can be removed after generation) +POLYMARKET_PRIVATE_KEY=0xYourPrivateKey + +# Polymarket proxy wallet address - note: this is NOT the deposit address! +# How to get it: log in to polymarket.com → top-right avatar → Settings → Wallet → copy "Proxy Wallet" (also called Funder Address) +# This is the Gnosis Safe the platform auto-deploys for your EOA, mapping one-to-one with the private key; a wrong value triggers invalid signature +POLYMARKET_PROXY_ADDRESS=0xYourProxyWalletAddress + +# HTTP service starting port (auto +1 increment if taken, up to 10 times) +# When running multiple instances, change to 3556 / 3656 etc. +PORT=3456 + +# Verbose raw log switch (trade-raw-YYYY-MM-DD.log): off by default to save disk +# Set to true and restart when you need to debug raw data such as orders/signatures/polling +RAW_LOG_ENABLED=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..899aae1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Dependencies +node_modules/ + +# Sensitive config and credentials +.env +.polymarket-creds.json +.tg-config.json +monitor/.tg-config.json +monitor/.balance-history.json +monitor/accounts.json + +# Runtime data +.strategy-config.json +.strategy-sources.json +.manual-config.json +.backtest-state.json +.active-market.json +.trade-history.json +.port + +# Backtest data (collected at runtime, not committed) +backtest-data/*.jsonl +backtest-data/collector/**/*.jsonl + +# System files +.DS_Store +**/.DS_Store +Thumbs.db + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# AI assistant / dev-tool files (not for the public repo) +CLAUDE.md +docs/AGENTS.md +.mcp.json +.claude/ +.cursor/ + +# Logs +*.log +npm-debug.log* +release/*.log + +# Release packaging (build artifacts, not committed) +*.zip +release/*.zip diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..63537cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Penguin Sensei · 岳 (@x_188888_x) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9744c51 --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +# BTC 5m — Quick Order Tool & Automated Strategy Framework + +> A quick-order tool and automated strategy framework for Polymarket's **BTC 5-minute up/down** market. + +![License](https://img.shields.io/badge/license-MIT-blue.svg) +![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg) +![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6.svg) + +Run it fully locally for fast manual trading with helper overlays, or deploy it to a cloud server for low-latency, 24/7 automated trading. It connects to multiple upstream WebSockets (Polymarket order book / Chainlink price / Binance reference price) and shows the live order book, price trends, positions, and an order panel — with a pluggable strategy system for automation. + +**Multi-market by design:** the data layer supports **BTC / ETH / SOL** across **5m / 15m** windows (6 markets, switchable from the UI), and adding a new symbol is a one-line config change. The bundled example strategies target BTC 5m; other markets are ready for your own strategies (thresholds differ a lot per coin, so they shouldn't be blindly reused). + +![Main dashboard](docs/screenshots/dashboard.png) + +> _Main dashboard (full mode): live order book, strategy tooltip, probability & price curves, and the order panel._ + +--- + +## Table of Contents + +- [Why this tool](#why-this-tool) +- [Features](#features) +- [Quick Start](#quick-start) +- [Project Structure](#project-structure) +- [Strategies](#strategies) +- [API](#api) +- [Cloud Deployment](#cloud-deployment) +- [Security](#security) +- [Contributing](#contributing) +- [Support this project](#support-this-project) +- [License](#license) + +--- + +## Why this tool + +| | Polymarket Official | This Tool | +|---|---|---| +| **Order flow** | Wallet signature required every time, easy to miss the moment | Credentials auto-cached after the first signature, one-click order | +| **Price reference** | Chainlink oracle price only | Also shows Binance real-time price, faster reaction | +| **Data visualization** | Probability numbers only | Probability curve + Binance price curve + diff comparison | +| **Automated trading** | None | Modular strategy framework, fully automated execution of custom strategies | +| **Deployment** | Browser only | Local run / cloud server, 24/7 unattended | + +> ⚠️ **Disclaimer:** The built-in strategies are framework examples only and **cannot guarantee profits**. Develop and tune your own strategies based on your own analysis. Trading involves risk; you are solely responsible for any losses. + +![Multi-account monitor](docs/screenshots/monitor.png) + +> _Multi-account monitor: track balance, PnL, win rate, and live status across multiple instances at a glance._ + +--- + +## Features + +### ⚡ Quick Order +- After the first private-key signature, API credentials are auto-cached — no repeated signing +- FOK (fill-or-kill) market orders, avoiding resting-order risk +- One-click buy up / buy down from the panel, with slippage settings + +### 🌐 Multi-Market +- **BTC / ETH / SOL** × **5m / 15m** = 6 markets, switchable from the UI +- Data-driven config (`market-configs.ts`) — add a new symbol/period in one entry +- Per-coin price precision handled automatically (BTC integer, SOL 4 decimals, etc.) +- Built-in strategies target BTC 5m; other markets are ready for your own strategies + +### 📊 Real-Time Data +- Four parallel WebSockets: Polymarket order book, Chainlink oracle, user fills, Binance real-time price +- Binance price reacts faster than the Chainlink oracle, providing a leading signal +- Automatic price-offset calibration (MAD outlier filtering + trimmed mean) +- Automatic market-window switch (every 5m / 15m depending on the market) + +### 🧩 Strategy Framework +- Each strategy is a single file under `strategies/`, implementing a unified interface +- Add a strategy: drop in a file → restart, and the frontend shows it automatically +- Strategy parameters carry comments; frontend hover descriptions are generated automatically +- Supports both **market** and **limit (maker)** order strategies +- Multiple entries per round (count configurable, persisted) +- Trade records saved automatically, with entry/exit reasons and PnL details + +### 🖥 Frontend Panel (Dual Mode) +- **Full mode** — Complete dashboard: order-book depth, probability/price curves, manual order panel, strategy controls, trade records — for manual trading and monitoring +- **Low mode** — Lean panel for automation: core data (probability/diff/countdown) + strategy status + trade records, low bandwidth — for unattended running and mobile viewing +- Toggle strategy switches, amounts, and per-round counts in real time +- Auto-claim of expired positions + +### 🔬 Data Collection & Backtesting +- One-click backtest data collection from the frontend +- Records diff, probability, time remaining, and other key metrics every second +- Companion Python analysis script with parameter-sweep optimization (multi-core) + +--- + +## Quick Start + +### Requirements +- Node.js **20+** + +### Install +```bash +npm install +``` + +### Configure +```bash +cp .env.example .env +``` + +**Required:** +- `POLYMARKET_PRIVATE_KEY` — Polygon private key (auto-generates API credentials on first run) +- `POLYMARKET_PROXY_ADDRESS` — Polymarket proxy wallet address (**not the deposit address**; log in to polymarket.com → top-right avatar → Settings → Wallet → copy "Proxy Wallet", which maps one-to-one with the private key; a wrong value triggers `invalid signature`) + +**Optional:** +- `APP_MODE` — `full` (with panel) or `headless` (API only) +- `STRATEGY__ENABLED` / `STRATEGY__AMOUNT` — strategy switch and amount (KEY is the uppercase strategy name, e.g. `D1`, `P1`, `P2`) +- `ORDER_DEFAULT_SLIPPAGE` — default slippage +- `AUTO_CLAIM_ENABLED` — auto-claim expired positions + +### Run +```bash +# macOS / Linux +./start.sh + +# Windows +start.bat + +# or +npm start +``` + +Then open **http://localhost:3456** + +--- + +## Project Structure + +``` +├── server.ts # Backend service (Express + WebSocket, port 3456) +├── index.html # Frontend panel +├── strategies/ # Strategy modules (plugin-based: add/remove files, no registry edits) +│ ├── types.ts # Shared types and the IStrategy interface +│ ├── registry.ts # Strategy registry (driven by _runtime/loader) +│ ├── _runtime/ # Dynamic loader +│ ├── _core/ # Shared core logic (fair-prob / momentum factors) +│ ├── d1.ts # Diff-based +│ └── p1.ts, p2.ts # Prob-chase +├── backtest-data/ # Backtest data (generated at runtime) +├── .env.example # Environment variable template +├── start.sh # macOS/Linux launch script +└── start.bat # Windows launch script +``` + +--- + +## Strategies + +### Built-in + +| Key | Name | Logic Summary | +|-----|------|---------------| +| **D1** | Diff 1 · Tail Sweep | large-diff entry at the window tail, diff-cross-0 stop-loss, holds to settlement | +| **P1** | Prob-Chase 1 | fair-prob table lookup, entry when probability lags the diff, reverse ±5 stop-loss | +| **P2** | Prob-Chase 2 · End-Game Crossing | rem 90~30s crossing entry + bias/probability filter, reverse ±5 stop-loss | + +Each strategy file has full parameters and comments at the top; hover over the strategy name in the UI to see its detailed rules. + +### Add your own (plugin-based) + +Create a file under `strategies/` whose name matches `.ts` (e.g. `d3.ts`, `x1.ts`) and export a class implementing `IStrategy`. After restarting, the dynamic loader registers it automatically and the frontend generates the UI for it. + +```ts +// strategies/x1.ts example +import type { IStrategy, StrategyTickContext, EntrySignal, ExitSignal } from "./types.js"; + +export default class X1 implements IStrategy { + readonly key = "x1"; + readonly number = 1; + readonly name = "My Strategy"; + getDescription() { return { key: this.key, number: this.number, name: this.name, title: "X1", lines: [] }; } + updateGuards(_ctx: StrategyTickContext) {} + checkEntry(_ctx: StrategyTickContext): EntrySignal | null { return null; } + checkExit(_ctx: StrategyTickContext): ExitSignal { return null; } + resetState() {} + getStatePayload() { return {}; } +} +``` + +To remove a strategy, just delete its file — no registry changes needed. + +Prefixes: `d` diff · `p` prob-chase · `t` trend · `l` limit-order (maker) · `m` momentum (reserved). + +See **[strategies/STRATEGY-GUIDE.md](strategies/STRATEGY-GUIDE.md)** for the full development guide, including how to build limit-order (maker) strategies. + +--- + +## API + +| Method | Path | Description | +|--------|------|-------------| +| `GET` | `/api/state` | Full state snapshot | +| `GET` | `/api/strategy/descriptions` | Strategy descriptions | +| `POST` | `/api/strategy/config` | Update strategy config | +| `POST` | `/api/order` | Manual order | +| `POST` | `/api/claim` | Claim expired positions | +| `POST` | `/api/backtest/toggle` | Toggle backtest data collection | + +--- + +## Cloud Deployment + +After uploading the project to your server, configure and start it as above. Keep it running in the background with `screen`: + +```bash +screen -S btc5m +npm start +# press Ctrl+A then D to detach +``` + +Access the panel securely via an SSH tunnel (do **not** expose the port publicly): + +```bash +ssh -L 3456:127.0.0.1:3456 username@server_ip +``` + +Then open **http://127.0.0.1:3456** in your local browser. + +--- + +## Security + +**Private key & credentials** +- The private key lives only in your local `.env` and is never uploaded anywhere +- API credentials are derived from it and cached to `.polymarket-creds.json` for reuse +- Both are excluded via `.gitignore` + +**Network access** +- The service listens on `localhost:3456`, accessible only from the local machine +- **Never expose the port directly to the public internet** — anyone who can reach it can place orders via the API +- Always use an SSH tunnel for remote access + +**Fund safety** +- Test with the smallest amount first; scale up only after confirming the behavior +- Strategy switches and amounts can be adjusted from the frontend at any time +- Built-in strategies are examples only and are not investment advice + +**Never share or commit these files:** + +| File | Contents | +|------|----------| +| `.env` | Private key and wallet address | +| `.polymarket-creds.json` | API credentials | +| `.strategy-config.json` | Persisted config | + +--- + +## Contributing + +The tool provides a complete strategy framework and backtesting capability, but good strategies need continuous iteration. Contributions and ideas are very welcome — especially if you: + +- Have better entry/exit ideas or have discovered new data patterns +- Want to do strategy backtesting and optimization together +- Want to **build more powerful features on top of this project** (forks encouraged!) +- Have any thoughts on the Polymarket BTC 5-minute market + +Open an issue / PR, or reach out directly — let's achieve a 1+1 > 2 effect. + +--- + +## Support this project + +This project is free and open source. If you find it useful, here are a few ways to support it — all cost you nothing and mean a lot: + +- ⭐ **Star this repo** — it helps more people discover the project and keeps me motivated to maintain it +- 🔗 **Sign up for Polymarket via my referral link** — directly supports continued development: + ### 👉 https://polymarket.com/?r=yue188888x +- 🛠 **Build on top of it** — fork it and create something more powerful; I'd love to see what you make +- 🤖 **Prefer copy-trading?** If you'd rather follow trades than run your own strategies, try **Kreo** — a copy-trading Telegram bot ([@kreoapp](https://x.com/kreoapp)). Tap to open it in Telegram: **https://t.me/KreoPolyBot?start=ref-188888x** +- 📈 **Trade crypto & RWAs?** Check out **Variational (Omni)** ([@variational_io](https://x.com/variational_io)) — a trading platform for crypto, RWAs, and more (recently raised $50M). Trade and earn points along the way: **https://omni.variational.io/?ref=OMNI88888** +- 🐦 **Get in touch** — questions, ideas, or collaboration on X (Twitter): **[@x_188888_x](https://x.com/x_188888_x)** + +Thank you for supporting open source! 🙏 + +--- + +## License + +MIT — for personal use and learning/research. The risk of using this tool for trading is borne solely by the user. diff --git a/backtest-data/README.md b/backtest-data/README.md new file mode 100644 index 0000000..7ba4b0a --- /dev/null +++ b/backtest-data/README.md @@ -0,0 +1,104 @@ +# 回测数据与分析方案 + +## 数据格式 + +文件命名: +- BTC:`YYYY-MM-DD.jsonl`(保留旧命名,向后兼容历史数据) +- 其他币种:`YYYY-MM-DD-{sym}.jsonl`(如 `2026-04-26-eth.jsonl`、`2026-04-26-sol.jsonl`) + +每行一条 JSON: + +```json +{"type":"tick","ts":1775195098289,"symbol":"btc","windowStart":1775194800,"diff":51.16,"upPct":100,"rem":2} +``` + +| 字段 | 含义 | +|---|---| +| ts | 时间戳(毫秒) | +| symbol | 币种(btc/eth/sol)。**老数据无此字段时默认 btc** | +| windowStart | 所属5分钟窗口的起始时间(秒) | +| diff | 参考价 - (priceToBeat - 偏移),正值=涨,负值=跌 | +| upPct | Polymarket 涨概率(0-100整数) | +| rem | 窗口剩余秒数 | + +采样频率:每秒1条,每天约86400条,约4MB。 + +## 分析目标 + +找到 diff 和 upPct 之间的历史映射关系,当实际概率偏离历史均值时入场。 + +## 分析步骤 + +### 1. 建立 diff → 合理概率 映射 + +将 diff 按区间分桶(如每5一档:-60~-55, -55~-50, ..., 55~60),统计每个桶内 upPct 的中位数,得到"在某个 diff 水平下,市场通常给出的概率"。 + +```python +import json, glob +import pandas as pd + +ticks = [] +for f in sorted(glob.glob('*.jsonl')): + for line in open(f): + row = json.loads(line) + if row.get('type') == 'tick': + ticks.append(row) + +df = pd.DataFrame(ticks) + +# 按 diff 分桶,rem 分段 +df['diff_bin'] = (df['diff'] / 5).round() * 5 +df['rem_bin'] = pd.cut(df['rem'], bins=[0, 30, 60, 120, 180, 300], labels=['0-30','30-60','60-120','120-180','180-300']) + +# 每个 (diff_bin, rem_bin) 的概率中位数 +mapping = df.groupby(['diff_bin', 'rem_bin'])['upPct'].agg(['median', 'mean', 'std', 'count']) +``` + +### 2. 计算偏差 + +```python +# 对每条 tick,查找对应的合理概率 +fair_prob = mapping.loc[(diff_bin, rem_bin), 'median'] +bias = fair_prob - actual_upPct + +# bias > 0:市场低估涨(买涨机会) +# bias < 0:市场高估涨(买跌机会) +``` + +### 3. 确定入场阈值 + +统计不同 bias 阈值下的入场次数和后续概率走势: + +```python +# 找出 bias > N 的时刻,看之后概率是否向合理概率回归 +for threshold in [5, 8, 10, 12, 15]: + entries = df[df['bias'] > threshold] + # 看入场后 10s/30s/60s 概率变化 + # 如果概率确实向 fair_prob 回归 → 该阈值可用 +``` + +### 4. 考虑波动率 + +用 diff 序列的标准差衡量波动率: + +```python +# 每个窗口内 diff 的标准差 +vol = df.groupby('windowStart')['diff'].std() + +# 高波动 vs 低波动时,同样 diff 对应的概率分布是否不同 +# 如果不同,映射关系需要加入波动率维度 +``` + +### 5. 输出 + +- diff → 合理概率 的映射表(按 rem 分段) +- 最优入场偏差阈值 +- 波动率是否需要作为额外维度 +- 模拟入场后的概率回归速度和幅度 + +## 注意事项 + +- 数据至少收集 2-3 天再分析(需要覆盖不同时段和市场状态) +- diff 极端值(>80 或 <-80)样本可能很少,映射不可靠 +- rem 对映射有影响:窗口早期(rem>180)概率波动大,晚期(rem<30)概率趋于收敛 +- 分析脚本在 backtest-data/ 目录下运行 diff --git a/backtest-data/analyze.py b/backtest-data/analyze.py new file mode 100644 index 0000000..09495f9 --- /dev/null +++ b/backtest-data/analyze.py @@ -0,0 +1,701 @@ +""" +回测分析脚本 +用法: cd backtest-data && python3 analyze.py + +功能: +1. 建立 diff → 合理概率 映射(按 rem 分段) +2. 计算当前概率与合理概率的偏差 +3. 找出最优入场偏差阈值 +4. 模拟策略1(常规加强)的历史表现 +""" + +import json +import glob +import sys +from collections import defaultdict + +# ── 1. 加载数据 ────────────────────────────────────────────── + +ticks = [] +for f in sorted(glob.glob("*.jsonl")) + sorted(glob.glob("ticks-*.jsonl")): + for line in open(f): + try: + r = json.loads(line) + if r.get("type") == "tick": + ticks.append(r) + elif "diff" in r and "upPct" in r and "type" not in r: + # 兼容旧格式(无 type 字段) + ticks.append(r) + except: + pass + +if not ticks: + print("没有找到 tick 数据") + sys.exit(1) + +windows = sorted(set(t["windowStart"] for t in ticks)) +print(f"总 tick 数: {len(ticks)}") +print(f"覆盖窗口数: {len(windows)}") +print(f"时间范围: {ticks[0]['ts']} ~ {ticks[-1]['ts']}") +print() + +# ── 2. 建立 diff → 合理概率 映射 ───────────────────────────── + +# 按 (diff 桶, rem 段) 分组 +DIFF_BUCKET = 5 # 每5一档 +REM_BINS = [(0, 30), (30, 60), (60, 120), (120, 180), (180, 300)] + +def diff_bucket(d): + return round(d / DIFF_BUCKET) * DIFF_BUCKET + +def rem_bin(r): + for lo, hi in REM_BINS: + if lo <= r < hi: + return f"{lo}-{hi}" + return "300+" + +mapping = defaultdict(list) # (diff_bucket, rem_bin) -> [upPct, ...] + +for t in ticks: + db = diff_bucket(t["diff"]) + rb = rem_bin(t["rem"]) + mapping[(db, rb)].append(t["upPct"]) + +print("=" * 70) +print("diff → 合理概率 映射(中位数,按 rem 分段)") +print("=" * 70) +print(f"{'diff':>6} ", end="") +for lo, hi in REM_BINS: + print(f" {lo}-{hi}s", end="") +print(f" {'样本':>6}") +print("-" * 70) + +all_buckets = sorted(set(db for db, _ in mapping)) +for db in all_buckets: + total = 0 + row = f"{db:+6d} " + for lo, hi in REM_BINS: + rb = f"{lo}-{hi}" + vals = mapping.get((db, rb), []) + total += len(vals) + if vals: + vals_sorted = sorted(vals) + median = vals_sorted[len(vals_sorted) // 2] + row += f" {median:5d}%" + else: + row += f" —" + row += f" {total:6d}" + if total >= 5: # 只显示有足够样本的 + print(row) + +# ── 3. 计算偏差分布 ────────────────────────────────────────── + +print() +print("=" * 70) +print("偏差分析:实际概率 vs 合理概率(中位数)") +print("=" * 70) + +biases = [] +for t in ticks: + db = diff_bucket(t["diff"]) + rb = rem_bin(t["rem"]) + vals = mapping.get((db, rb), []) + if len(vals) < 10: + continue + vals_sorted = sorted(vals) + fair_prob = vals_sorted[len(vals_sorted) // 2] + bias = fair_prob - t["upPct"] + biases.append({ + "bias": bias, + "diff": t["diff"], + "upPct": t["upPct"], + "fair": fair_prob, + "rem": t["rem"], + "windowStart": t["windowStart"], + "ts": t["ts"], + }) + +if biases: + abs_biases = [abs(b["bias"]) for b in biases] + print(f"有效样本数: {len(biases)}") + print(f"偏差均值: {sum(b['bias'] for b in biases) / len(biases):.1f}%") + print(f"偏差绝对值均值: {sum(abs_biases) / len(abs_biases):.1f}%") + print(f"偏差绝对值中位数: {sorted(abs_biases)[len(abs_biases)//2]:.1f}%") + print() + + # 偏差分布 + print("偏差分布:") + from collections import Counter + bias_bins = Counter() + for b in biases: + bb = round(b["bias"] / 2) * 2 + bias_bins[bb] += 1 + for bb in sorted(bias_bins): + pct = bias_bins[bb] / len(biases) * 100 + bar = "█" * int(pct) + print(f" {bb:+4d}%: {bias_bins[bb]:5d} ({pct:4.1f}%) {bar}") + +# ── 4. 入场机会分析 ────────────────────────────────────────── + +print() +print("=" * 70) +print("入场机会分析:偏差超过阈值时概率后续走势") +print("=" * 70) + +# 按窗口分组 +window_ticks = defaultdict(list) +for t in ticks: + window_ticks[t["windowStart"]].append(t) + +for threshold in [5, 8, 10, 12, 15]: + entries = [] + for b in biases: + if abs(b["bias"]) >= threshold and 50 <= b["rem"] <= 200: + entries.append(b) + + if not entries: + print(f"\n偏差阈值 {threshold}%: 无入场机会") + continue + + # 看入场后概率是否回归 + convergences = [] + for entry in entries: + ws = entry["windowStart"] + wticks = window_ticks.get(ws, []) + # 找入场后10秒、30秒的概率变化 + entry_ts = entry["ts"] + for dt_label, dt_ms in [("10s", 10000), ("30s", 30000)]: + future = [t for t in wticks if 0 < t["ts"] - entry_ts <= dt_ms] + if future: + future_prob = future[-1]["upPct"] + prob_change = future_prob - entry["upPct"] + # 如果偏差为正(市场低估涨),概率应该涨 + expected_dir = 1 if entry["bias"] > 0 else -1 + correct = (prob_change * expected_dir) > 0 + convergences.append({ + "dt": dt_label, + "change": prob_change, + "correct": correct, + "bias_dir": "低估" if entry["bias"] > 0 else "高估", + }) + + if convergences: + for dt_label in ["10s", "30s"]: + dt_items = [c for c in convergences if c["dt"] == dt_label] + if dt_items: + correct_count = sum(1 for c in dt_items if c["correct"]) + avg_change = sum(abs(c["change"]) for c in dt_items) / len(dt_items) + print(f"\n偏差阈值 {threshold}%: {len(entries)} 次入场机会") + print(f" {dt_label}后回归率: {correct_count}/{len(dt_items)} ({correct_count/len(dt_items)*100:.0f}%)") + print(f" {dt_label}后平均概率变化: {avg_change:.1f}%") + +# ── 5. 策略1模拟回测 ───────────────────────────────────────── + +print() +print("=" * 70) +print("策略1(常规加强)模拟回测") +print("=" * 70) + +ENTRY_DIFF = 35 +ENTRY_PROB_CAP = 80 +WINDOW_MAX_REM = 210 +WINDOW_MIN_REM = 50 +TRAILING_STOP_RETRACEMENT = 20 +TRAILING_STOP_MIN_DIFF = 5 +PROB_PEAK_MIN = 85 +PROB_PEAK_RETRACEMENT = 8 +FORCE_EXIT_REM = 10 + +trades = [] +for ws in windows: + wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) + if len(wticks) < 10: + continue + + last_diff = None + holding = False + direction = None + entry_price = None + peak_diff = None + peak_prob = None + + for t in wticks: + diff = t["diff"] + upPct = t["upPct"] + rem = t["rem"] + + if not holding: + # 检查入场 + if rem <= WINDOW_MAX_REM and rem > WINDOW_MIN_REM and last_diff is not None: + # 买涨穿越 + if last_diff <= ENTRY_DIFF and diff > ENTRY_DIFF and upPct < ENTRY_PROB_CAP: + holding = True + direction = "up" + entry_price = upPct / 100 # 简化:用概率作为买入价 + peak_diff = diff + peak_prob = upPct + # 买跌穿越 + elif last_diff >= -ENTRY_DIFF and diff < -ENTRY_DIFF and (100 - upPct) < ENTRY_PROB_CAP: + holding = True + direction = "down" + entry_price = (100 - upPct) / 100 + peak_diff = -diff + peak_prob = 100 - upPct + else: + my_pct = upPct if direction == "up" else (100 - upPct) + fav_diff = diff if direction == "up" else -diff + + # 更新峰值 + if fav_diff > peak_diff: + peak_diff = fav_diff + if my_pct > peak_prob: + peak_prob = my_pct + + exit_reason = None + exit_signal = None + + # 强制平仓 + if rem <= FORCE_EXIT_REM and rem > 0: + exit_signal = "tp" if my_pct >= 70 else "sl" + exit_reason = f"强制平仓 rem={rem}" + + # 阶梯止盈 + if not exit_reason and rem >= FORCE_EXIT_REM: + span = WINDOW_MAX_REM - FORCE_EXIT_REM + elapsed = max(0, WINDOW_MAX_REM - rem) + tp_thr = 90 + int(elapsed / span * 10) + tp_capped = min(tp_thr, 100) + if my_pct >= tp_capped: + exit_signal = "tp" + exit_reason = f"阶梯止盈 {my_pct}%>={tp_capped}%" + + # 回撤止盈 + if not exit_reason and peak_prob >= PROB_PEAK_MIN and my_pct <= peak_prob - PROB_PEAK_RETRACEMENT: + exit_signal = "tp" + exit_reason = f"回撤止盈 {my_pct}% 峰{peak_prob}%" + + # 兜底止损 + if not exit_reason: + if direction == "up" and diff <= TRAILING_STOP_MIN_DIFF: + exit_signal = "sl" + exit_reason = f"兜底止损 diff={diff:.0f}" + elif direction == "down" and diff >= -TRAILING_STOP_MIN_DIFF: + exit_signal = "sl" + exit_reason = f"兜底止损 diff={diff:.0f}" + + # 追踪止损 + if not exit_reason and peak_diff - fav_diff >= TRAILING_STOP_RETRACEMENT: + exit_signal = "sl" + exit_reason = f"追踪止损 回撤{peak_diff - fav_diff:.0f}" + + if exit_reason: + exit_price = my_pct / 100 + pnl = exit_price - entry_price + trades.append({ + "window": ws, + "direction": direction, + "entry_price": entry_price, + "exit_price": exit_price, + "pnl": pnl, + "signal": exit_signal, + "reason": exit_reason, + }) + holding = False + direction = None + + last_diff = diff + +if trades: + wins = [t for t in trades if t["pnl"] > 0] + losses = [t for t in trades if t["pnl"] <= 0] + total_pnl = sum(t["pnl"] for t in trades) + + print(f"总交易次数: {len(trades)}") + print(f"盈利次数: {len(wins)} ({len(wins)/len(trades)*100:.0f}%)") + print(f"亏损次数: {len(losses)} ({len(losses)/len(trades)*100:.0f}%)") + print(f"总 PnL: {total_pnl:+.4f}") + if wins: + print(f"平均盈利: +{sum(t['pnl'] for t in wins)/len(wins):.4f}") + if losses: + print(f"平均亏损: {sum(t['pnl'] for t in losses)/len(losses):.4f}") + print() + + # 按出场原因统计 + reason_stats = defaultdict(lambda: {"count": 0, "pnl": 0}) + for t in trades: + key = t["reason"].split(" ")[0] + " " + t["reason"].split(" ")[1] if len(t["reason"].split(" ")) > 1 else t["reason"] + # 简化为类型 + if "阶梯" in t["reason"]: + key = "阶梯止盈" + elif "回撤止盈" in t["reason"]: + key = "回撤止盈" + elif "兜底" in t["reason"]: + key = "兜底止损" + elif "追踪" in t["reason"]: + key = "追踪止损" + elif "强制" in t["reason"]: + key = "强制平仓" + reason_stats[key]["count"] += 1 + reason_stats[key]["pnl"] += t["pnl"] + + print("按出场原因统计:") + for key in sorted(reason_stats, key=lambda k: -reason_stats[k]["count"]): + s = reason_stats[key] + print(f" {key}: {s['count']}次, PnL {s['pnl']:+.4f}") + + print() + print("逐笔明细:") + for t in trades: + dir_zh = "涨" if t["direction"] == "up" else "跌" + print(f" 窗口{t['window']} 买{dir_zh} 入{t['entry_price']:.2f}→出{t['exit_price']:.2f} PnL{t['pnl']:+.4f} {t['reason']}") +else: + print("无交易触发") + +# ── 6. 策略2(常规)模拟回测 ────────────────────────────────── + +print() +print("=" * 70) +print("策略2(常规)模拟回测") +print("=" * 70) + +S2_ENTRY_DIFF = 40 +S2_ENTRY_PROB_CAP = 75 +S2_WINDOW_MAX_REM = 168 +S2_WINDOW_MIN_REM = 48 +S2_STOP_LOSS_DIFF = 5 +S2_TP_LADDER_FLOOR = 8 + +trades2 = [] +for ws in windows: + wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) + if len(wticks) < 10: + continue + + last_diff = None + holding = False + direction = None + entry_price = None + + for t in wticks: + diff = t["diff"] + upPct = t["upPct"] + rem = t["rem"] + + if not holding: + if rem <= S2_WINDOW_MAX_REM and rem > S2_WINDOW_MIN_REM and last_diff is not None: + if last_diff <= S2_ENTRY_DIFF and diff > S2_ENTRY_DIFF and upPct < S2_ENTRY_PROB_CAP: + holding = True + direction = "up" + entry_price = upPct / 100 + elif last_diff >= -S2_ENTRY_DIFF and diff < -S2_ENTRY_DIFF and (100 - upPct) < S2_ENTRY_PROB_CAP: + holding = True + direction = "down" + entry_price = (100 - upPct) / 100 + else: + my_pct = upPct if direction == "up" else (100 - upPct) + exit_reason = None + exit_signal = None + + # 阶梯止盈(固定公式:168→8,每16s升1%) + if rem >= S2_TP_LADDER_FLOOR: + span = S2_WINDOW_MAX_REM - S2_TP_LADDER_FLOOR + elapsed = max(0, S2_WINDOW_MAX_REM - max(rem, S2_TP_LADDER_FLOOR)) + tp_thr = 90 + int(elapsed / span * 10) + tp_capped = min(tp_thr, 100) + if my_pct >= tp_capped: + exit_signal = "tp" + exit_reason = f"阶梯止盈 {my_pct}%>={tp_capped}%" + + # 止损 + if not exit_reason: + if direction == "up" and diff <= S2_STOP_LOSS_DIFF: + exit_signal = "sl" + exit_reason = f"止损 diff={diff:.0f}" + elif direction == "down" and diff >= -S2_STOP_LOSS_DIFF: + exit_signal = "sl" + exit_reason = f"止损 diff={diff:.0f}" + + if exit_reason: + exit_price = my_pct / 100 + pnl = exit_price - entry_price + trades2.append({ + "window": ws, + "direction": direction, + "entry_price": entry_price, + "exit_price": exit_price, + "pnl": pnl, + "signal": exit_signal, + "reason": exit_reason, + }) + holding = False + direction = None + + last_diff = diff + +if trades2: + wins2 = [t for t in trades2 if t["pnl"] > 0] + losses2 = [t for t in trades2 if t["pnl"] <= 0] + total_pnl2 = sum(t["pnl"] for t in trades2) + + print(f"总交易次数: {len(trades2)}") + print(f"盈利次数: {len(wins2)} ({len(wins2)/len(trades2)*100:.0f}%)") + print(f"亏损次数: {len(losses2)} ({len(losses2)/len(trades2)*100:.0f}%)") + print(f"总 PnL: {total_pnl2:+.4f}") + if wins2: + print(f"平均盈利: +{sum(t['pnl'] for t in wins2)/len(wins2):.4f}") + if losses2: + print(f"平均亏损: {sum(t['pnl'] for t in losses2)/len(losses2):.4f}") + print() + + reason_stats2 = defaultdict(lambda: {"count": 0, "pnl": 0}) + for t in trades2: + if "阶梯" in t["reason"]: + key = "阶梯止盈" + elif "止损" in t["reason"]: + key = "止损" + else: + key = t["reason"] + reason_stats2[key]["count"] += 1 + reason_stats2[key]["pnl"] += t["pnl"] + + print("按出场原因统计:") + for key in sorted(reason_stats2, key=lambda k: -reason_stats2[k]["count"]): + s = reason_stats2[key] + print(f" {key}: {s['count']}次, PnL {s['pnl']:+.4f}") + + print() + print("逐笔明细:") + for t in trades2: + dir_zh = "涨" if t["direction"] == "up" else "跌" + print(f" 窗口{t['window']} 买{dir_zh} 入{t['entry_price']:.2f}→出{t['exit_price']:.2f} PnL{t['pnl']:+.4f} {t['reason']}") +else: + print("无交易触发") + +# ── 7. 对比总结 ────────────────────────────────────────────── + +print() +print("=" * 70) +print("策略对比") +print("=" * 70) +s1_pnl = sum(t["pnl"] for t in trades) if trades else 0 +s2_pnl = sum(t["pnl"] for t in trades2) if trades2 else 0 +s1_wins = sum(1 for t in trades if t["pnl"] > 0) if trades else 0 +s2_wins = sum(1 for t in trades2 if t["pnl"] > 0) if trades2 else 0 +print(f"{'':15} {'常规加强':>10} {'常规':>10}") +print(f"{'交易次数':15} {len(trades):>10} {len(trades2):>10}") +print(f"{'胜率':15} {(s1_wins/len(trades)*100 if trades else 0):>9.0f}% {(s2_wins/len(trades2)*100 if trades2 else 0):>9.0f}%") +print(f"{'总PnL':15} {s1_pnl:>+10.4f} {s2_pnl:>+10.4f}") + +# ── 8. 波动率分析 ───────────────────────────────────────────── + +print() +print("=" * 70) +print("波动率分析(每个窗口内 diff 标准差)") +print("=" * 70) + +import math + +vol_data = [] +for ws in windows: + wticks = window_ticks[ws] + if len(wticks) < 10: + continue + diffs = [t["diff"] for t in wticks] + mean = sum(diffs) / len(diffs) + variance = sum((d - mean) ** 2 for d in diffs) / len(diffs) + std = math.sqrt(variance) + vol_data.append({"window": ws, "std": std, "count": len(wticks)}) + +if vol_data: + stds = [v["std"] for v in vol_data] + print(f"窗口数: {len(vol_data)}") + print(f"diff 标准差 范围: {min(stds):.1f} ~ {max(stds):.1f}") + print(f"diff 标准差 均值: {sum(stds)/len(stds):.1f}") + print(f"diff 标准差 中位数: {sorted(stds)[len(stds)//2]:.1f}") + + # 高波动 vs 低波动时的概率偏差 + median_std = sorted(stds)[len(stds) // 2] + high_vol_windows = set(v["window"] for v in vol_data if v["std"] > median_std) + low_vol_windows = set(v["window"] for v in vol_data if v["std"] <= median_std) + + if biases: + high_biases = [b for b in biases if b["windowStart"] in high_vol_windows] + low_biases = [b for b in biases if b["windowStart"] in low_vol_windows] + if high_biases and low_biases: + print(f"\n高波动窗口 偏差绝对值均值: {sum(abs(b['bias']) for b in high_biases)/len(high_biases):.1f}%") + print(f"低波动窗口 偏差绝对值均值: {sum(abs(b['bias']) for b in low_biases)/len(low_biases):.1f}%") + +# ── 9. 参数优化(常规加强) ─────────────────────────────────── + +print() +print("=" * 70) +print("参数优化:常规加强策略 — 遍历参数组合找最优") +print("=" * 70) + +def run_backtest(params): + """用给定参数跑一遍回测,返回交易列表""" + ed = params["entry_diff"] + epc = params["entry_prob_cap"] + wmx = params["window_max_rem"] + wmn = params["window_min_rem"] + tsr = params["trailing_stop_ret"] + tsm = params["trailing_stop_min"] + ppm = params["prob_peak_min"] + ppr = params["prob_peak_ret"] + fer = params["force_exit_rem"] + tps = params["tp_start"] + tpn = 100 - tps # 从起始值到100%的百分点数,每1%一档 + + result = [] + for ws in windows: + wticks = sorted(window_ticks[ws], key=lambda t: t["ts"]) + if len(wticks) < 10: + continue + + last_diff = None + holding = False + direction = None + entry_price = None + peak_diff = None + peak_prob = None + + for t in wticks: + diff = t["diff"] + upPct = t["upPct"] + rem = t["rem"] + + if not holding: + if rem <= wmx and rem > wmn and last_diff is not None: + if last_diff <= ed and diff > ed and upPct < epc: + holding = True + direction = "up" + entry_price = upPct / 100 + peak_diff = diff + peak_prob = upPct + elif last_diff >= -ed and diff < -ed and (100 - upPct) < epc: + holding = True + direction = "down" + entry_price = (100 - upPct) / 100 + peak_diff = -diff + peak_prob = 100 - upPct + else: + my_pct = upPct if direction == "up" else (100 - upPct) + fav_diff = diff if direction == "up" else -diff + + if fav_diff > peak_diff: peak_diff = fav_diff + if my_pct > peak_prob: peak_prob = my_pct + + exit_reason = None + + # 强制平仓 + if rem <= fer and rem > 0: + exit_reason = "强制平仓" + + # 阶梯止盈 + if not exit_reason and rem >= fer: + span = wmx - fer + elapsed = max(0, wmx - rem) + tp_capped = min(tps + int(elapsed / span * tpn), 100) + if my_pct >= tp_capped: + exit_reason = "阶梯止盈" + + # 回撤止盈 + if not exit_reason and peak_prob >= ppm and my_pct <= peak_prob - ppr: + exit_reason = "回撤止盈" + + # 兜底止损 + if not exit_reason: + if direction == "up" and diff <= tsm: + exit_reason = "兜底止损" + elif direction == "down" and diff >= -tsm: + exit_reason = "兜底止损" + + # 追踪止损 + if not exit_reason and peak_diff - fav_diff >= tsr: + exit_reason = "追踪止损" + + if exit_reason: + exit_price = my_pct / 100 + pnl = exit_price - entry_price + result.append(pnl) + holding = False + direction = None + + last_diff = diff + return result + +# 参数搜索空间 +param_grid = { + "entry_diff": [25, 30, 35, 40, 45], + "entry_prob_cap": [75, 80, 85], + "window_max_rem": [190, 210, 240], + "window_min_rem": [30, 50], + "trailing_stop_ret":[15, 20, 25], + "trailing_stop_min":[5], # 上轮不敏感,固定 + "prob_peak_min": [80, 85], + "prob_peak_ret": [5, 8, 10], + "force_exit_rem": [8, 10], + "tp_start": [88, 90, 92, 95], +} + +# 生成所有组合 +from itertools import product +import multiprocessing as mp +mp.set_start_method("fork", force=True) +from multiprocessing import Pool, cpu_count + +keys = list(param_grid.keys()) +combos = list(product(*[param_grid[k] for k in keys])) +print(f"总参数组合数: {len(combos)}, 使用 {cpu_count()} 核并行计算") +print("计算中...") + +def eval_combo(combo): + params = dict(zip(keys, combo)) + pnls = run_backtest(params) + if not pnls: + return None + total = sum(pnls) + wins = sum(1 for p in pnls if p > 0) + return { + "params": params, + "total_pnl": total, + "trades": len(pnls), + "win_rate": wins / len(pnls) * 100, + } + +with Pool(cpu_count()) as pool: + raw_results = pool.map(eval_combo, combos) + +top_results = [r for r in raw_results if r is not None] + +# 按 PnL 排序,显示 Top 15 +top_results.sort(key=lambda x: -x["total_pnl"]) + +best = top_results[0] if top_results else None + +print(f"\n{'排名':>4} {'交易':>4} {'胜率':>6} {'总PnL':>8} 参数") +print("-" * 100) +for i, r in enumerate(top_results[:15]): + p = r["params"] + param_str = f"diff={p['entry_diff']} cap={p['entry_prob_cap']} win={p['window_max_rem']}-{p['window_min_rem']} ts={p['trailing_stop_ret']}/{p['trailing_stop_min']} pp={p['prob_peak_min']}/{p['prob_peak_ret']} fer={p['force_exit_rem']} tp={p['tp_start']}→100%" + marker = " ★" if i == 0 else "" + print(f"{i+1:>4} {r['trades']:>4} {r['win_rate']:>5.0f}% {r['total_pnl']:>+8.4f} {param_str}{marker}") + +if best: + print(f"\n最优参数:") + for k, v in best["params"].items(): + label = { + "entry_diff": "ENTRY_DIFF(入场差价阈值)", + "entry_prob_cap": "ENTRY_PROB_CAP(入场概率上限)", + "window_max_rem": "WINDOW_MAX_REMAINING(扫描起始)", + "window_min_rem": "WINDOW_MIN_REMAINING(扫描截止)", + "trailing_stop_ret": "TRAILING_STOP_RETRACEMENT(追踪止损回撤)", + "trailing_stop_min": "TRAILING_STOP_MIN_DIFF(兜底止损)", + "prob_peak_min": "PROB_PEAK_MIN_THRESHOLD(回撤止盈门槛)", + "prob_peak_ret": "PROB_PEAK_RETRACEMENT(回撤止盈幅度)", + "force_exit_rem": "FORCE_EXIT_REM(强制平仓秒数)", + "tp_start": "阶梯止盈起始概率(每1%一档升至100%)", + }.get(k, k) + print(f" {label}: {v}") + print(f" 总PnL: {best['total_pnl']:+.4f}, 交易{best['trades']}笔, 胜率{best['win_rate']:.0f}%") + print(f" 总PnL: {best_pnl:+.4f}, 交易{len(best_trades)}笔, 胜率{sum(1 for p in best_trades if p>0)/len(best_trades)*100:.0f}%") diff --git a/backtest-data/collector/.gitkeep b/backtest-data/collector/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/chain-watcher.ts b/chain-watcher.ts new file mode 100644 index 0000000..54190ab --- /dev/null +++ b/chain-watcher.ts @@ -0,0 +1,105 @@ +/** + * On-chain fill size calibration + * + * Parses the CTF TransferSingle events in a Polygon transaction receipt + * to obtain the real buy/sell fill size (on-chain truth). + * + * Purpose: correct the size deviation pushed by Polymarket UserWS (on buy, the + * size reported by WS differs from the real on-chain size by about 1%). + * + * Measured latency: 300-700ms (dRPC), about 5 seconds faster than REST API polling. + */ + +import { ethers } from "ethers"; + +// ERC-1155 TransferSingle event topic +const TRANSFER_SINGLE = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62"; + +// Public Polygon RPCs (sorted by measured latency) +const RPCS = [ + "https://polygon.drpc.org", // fastest ~200ms + "https://polygon-bor-rpc.publicnode.com", // fallback 1 + "https://1rpc.io/matic", // fallback 2 +]; + +const QUERY_TIMEOUT_MS = 3000; +// Use a full Network object + staticNetwork object version to avoid ethers v6 still triggering eth_chainId detection during construction +const POLYGON_NETWORK = new ethers.Network("polygon", 137); + +// Reuse provider instances to avoid triggering network detection on each creation +const providerCache = new Map(); + +function getProvider(url: string): ethers.JsonRpcProvider { + let p = providerCache.get(url); + if (!p) { + // Pass the Network object to staticNetwork so ethers skips the eth_chainId detection at startup + p = new ethers.JsonRpcProvider(url, POLYGON_NETWORK, { staticNetwork: POLYGON_NETWORK }); + // Silence the error event (ethers v6 throws by default; we handle it ourselves with catch) + p.on("error", () => { /* ignore, handled by the caller */ }); + providerCache.set(url, p); + } + return p; +} + +/** + * Query the tx receipt from one RPC (with timeout) + */ +async function queryReceipt(url: string, txHash: string): Promise { + try { + const rpc = getProvider(url); + return await Promise.race([ + rpc.getTransactionReceipt(txHash), + new Promise((_, reject) => + setTimeout(() => reject(new Error(`timeout ${QUERY_TIMEOUT_MS}ms`)), QUERY_TIMEOUT_MS), + ), + ]); + } catch { + return null; + } +} + +/** + * Parse the CTF TransferSingle events in a tx that involve the Proxy + * @returns net fill size (positive = buy/inflow, negative = sell/outflow); + * null means all RPCs failed to query, should fall back to REST + */ +export async function getRealFillFromTx( + txHash: string, + proxy: string, +): Promise { + if (!txHash || !proxy) return null; + + const proxyPadded = ethers.zeroPadValue(proxy.toLowerCase(), 32); + + for (const url of RPCS) { + const receipt = await queryReceipt(url, txHash); + if (!receipt) continue; + + // tx failed (in theory a UserWS MINED won't fail, but handle defensively) + if (receipt.status !== 1) return 0; + + let totalIn = 0n; + let totalOut = 0n; + + for (const log of receipt.logs) { + if (log.topics[0] !== TRANSFER_SINGLE) continue; + // TransferSingle(operator, from, to, id, value) + // topics[0] = event sig + // topics[1] = operator + // topics[2] = from + // topics[3] = to + // data = id (32 bytes) + value (32 bytes) + if (log.topics.length < 4) continue; + const value = BigInt("0x" + log.data.slice(66)); + const isIn = log.topics[3].toLowerCase() === proxyPadded.toLowerCase(); + const isOut = log.topics[2].toLowerCase() === proxyPadded.toLowerCase(); + if (isIn) totalIn += value; + else if (isOut) totalOut += value; + } + + // CTF size has 6 decimals (same as USDC) + return Number(totalIn - totalOut) / 1e6; + } + + return null; // all RPCs failed +} diff --git a/collect.sh b/collect.sh new file mode 100755 index 0000000..ac63ce0 --- /dev/null +++ b/collect.sh @@ -0,0 +1,53 @@ +#!/bin/bash +# Data collector launch script (with auto-restart) +# +# Usage: +# ./collect.sh # default 60-day retention +# ./collect.sh 90 # 90-day retention +# +# Behavior: +# - Launches data-collector.ts to continuously collect data from 6 markets +# - Auto-restarts 5 seconds after a process crash +# - Ctrl+C exits gracefully (no further restart) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# Retention days (optional argument) +RETENTION_DAYS="${1:-60}" +export COLLECTOR_RETENTION_DAYS="$RETENTION_DAYS" + +# Ensure node_modules exists +if [ ! -d "node_modules" ]; then + echo "[Collect] node_modules not found, running npm install..." + npm install +fi + +# Ensure the data directory exists +mkdir -p backtest-data/collector + +echo "════════════════════════════════════════════" +echo " Polymarket multi-market data collector" +echo " Retention days: $RETENTION_DAYS" +echo " Data directory: backtest-data/collector/" +echo " Ctrl+C to exit" +echo "════════════════════════════════════════════" +echo "" + +# Catch Ctrl+C for a clean exit +SHOULD_RESTART=true +trap 'SHOULD_RESTART=false; echo ""; echo "[Collect] Exit signal received, stopping..."' INT TERM + +# Main loop: auto-restart +while $SHOULD_RESTART; do + npm run collect || true + if $SHOULD_RESTART; then + echo "" + echo "[Collect] Process exited ($(date '+%Y-%m-%d %H:%M:%S')), auto-restarting in 5 seconds..." + sleep 5 + fi +done + +echo "[Collect] Stopped" diff --git a/data-collector.ts b/data-collector.ts new file mode 100644 index 0000000..d4ee97c --- /dev/null +++ b/data-collector.ts @@ -0,0 +1,739 @@ +/** + * Data collector — standalone process dedicated to collecting multi-market backtest data + * + * Completely independent from server.ts: + * - Does not place orders, does not need a private key + * - Does not need a frontend, does not need an HTTP API + * - Only collects tick data for 6 markets (BTC/ETH/SOL × 5m/15m) + * + * Data output: + * backtest-data/collector/YYYY-MM-DD-{sym}-{period}.jsonl + * one row per second per market, in exactly the same format as server.ts backtestTick + * + * Start: + * npm run collect + * or ./collect.sh + * + * Error handling: aggressive reconnect, error isolation, never exits on its own + */ + +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from "fs"; +import { WebSocket } from "ws"; +import { Agent, setGlobalDispatcher } from "undici"; +import { + MARKETS, priceDecimals, ALL_SYMBOLS, + type MarketKey, type MarketSymbol, type MarketConfig, +} from "./market-configs.js"; + +// ── HTTP keep-alive ── +setGlobalDispatcher(new Agent({ + keepAliveTimeout: 60000, + keepAliveMaxTimeout: 600000, + connections: 10, + pipelining: 1, +})); + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const DATA_DIR = resolve(__dirname, "backtest-data", "collector"); +mkdirSync(DATA_DIR, { recursive: true }); + +// ── Constants (consistent with server.ts) ── +const MARKET_WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; +const CHAINLINK_WS_URL = "wss://ws-live-data.polymarket.com"; +const GAMMA_URL = "https://gamma-api.polymarket.com"; + +const HISTORY_RETENTION_MS = 130000; +const MAX_CHAINLINK_HISTORY_POINTS = 2000; +const MAX_BINANCE_HISTORY_POINTS = 4000; + +const BINANCE_ALIGN_WINDOW_MS = 60000; +const BINANCE_ALIGN_MIN_SPAN_MS = 10000; +const BINANCE_ALIGN_BUCKET_MS = 500; +const BINANCE_ALIGN_REFRESH_MS = 30000; +const BINANCE_OFFSET_EPSILON = 0.01; + +const PTB_RETRY_INTERVAL_MS = 2000; +const PTB_MAX_RETRIES = 5; + +const HEALTH_CHECK_INTERVAL_MS = 60000; +const STALE_DATA_THRESHOLD_MS = 60000; + +const TICK_WRITE_INTERVAL_MS = 1000; + +const RETENTION_DAYS = parseInt(process.env.COLLECTOR_RETENTION_DAYS || "60"); + +// ── Data types ── +interface PricePoint { t: number; price: number; } + +interface SymbolState { + symbol: MarketSymbol; + binanceWs: WebSocket | null; + binanceWsAttempt: number; + binanceLastTickAt: number; + chainlinkWs: WebSocket | null; + chainlinkWsAttempt: number; + chainlinkLastTickAt: number; + binanceHistory: PricePoint[]; + priceHistory: PricePoint[]; // chainlink + currentPrice: number | null; + binanceOffset: number | null; +} + +interface MarketState { + key: MarketKey; + config: MarketConfig; + upTokenId: string; + downTokenId: string; + windowStart: number; // currently subscribed window + windowEnd: number; + conditionId: string; + priceToBeat: number | null; + bids: Map; // price → size + asks: Map; + bestBid: string; // up token best bid + bestAsk: string; // up token best ask + marketWs: WebSocket | null; + marketWsAttempt: number; + marketLastTickAt: number; // time of the last marketWs data received + marketPingTimer: NodeJS.Timeout | null; + switchTimer: NodeJS.Timeout | null; + ptbRetryCount: number; +} + +// ── Global state ── +const symbolStates = new Map(); +const marketStates = new Map(); +let stopped = false; +let lastCleanupDate = ""; + +// ── Utility functions ── +function backoffDelay(attempt: number): number { + return Math.min(60000, 1000 * Math.pow(2, attempt)); +} + +function getCurrentWindowStart(periodSeconds: number, now = Date.now()): number { + return Math.floor(now / 1000 / periodSeconds) * periodSeconds; +} + +function trimHistory(arr: T[], minTs: number, maxLen: number): void { + while (arr.length > 0 && arr[0].t < minTs) arr.shift(); + while (arr.length > maxLen) arr.shift(); +} + +function calcMedian(values: number[]): number | null { + if (!values.length) return null; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; +} + +function calcTrimmedMean(values: number[], trimRatio = 0.15): number | null { + if (!values.length) return null; + if (values.length < 4) { + return values.reduce((s, v) => s + v, 0) / values.length; + } + const sorted = values.slice().sort((a, b) => a - b); + const trim = Math.floor(sorted.length * trimRatio); + const middle = sorted.slice(trim, sorted.length - trim); + return middle.reduce((s, v) => s + v, 0) / middle.length; +} + +function getCstDateStr(now = Date.now()): string { + const d = new Date(now + 8 * 3600 * 1000); + return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`; +} + +// ── Binance offset calculation (exactly consistent with server.ts) ── +function calculateBinanceOffset(sym: MarketSymbol, allowLatestFallback = false): number | null { + const ss = symbolStates.get(sym)!; + if (!ss.binanceHistory.length || !ss.priceHistory.length) { + if (!allowLatestFallback) return null; + const last = ss.binanceHistory[ss.binanceHistory.length - 1]; + if (!last || ss.currentPrice == null) return null; + return ss.currentPrice - last.price; + } + const now = Date.now(); + const binanceRecent = ss.binanceHistory.filter((p) => p.t >= now - BINANCE_ALIGN_WINDOW_MS); + const chainlinkRecent = ss.priceHistory.filter((p) => p.t >= now - BINANCE_ALIGN_WINDOW_MS); + if (!binanceRecent.length || !chainlinkRecent.length) { + if (!allowLatestFallback) return null; + const lastB = ss.binanceHistory[ss.binanceHistory.length - 1]; + if (!lastB || ss.currentPrice == null) return null; + return ss.currentPrice - lastB.price; + } + + const bSpan = binanceRecent.length >= 2 ? binanceRecent[binanceRecent.length - 1].t - binanceRecent[0].t : 0; + const cSpan = chainlinkRecent.length >= 2 ? chainlinkRecent[chainlinkRecent.length - 1].t - chainlinkRecent[0].t : 0; + if (Math.min(bSpan, cSpan) < BINANCE_ALIGN_MIN_SPAN_MS) { + if (!allowLatestFallback) return null; + return chainlinkRecent[chainlinkRecent.length - 1].price - binanceRecent[binanceRecent.length - 1].price; + } + + const overlapStart = Math.max(binanceRecent[0].t, chainlinkRecent[0].t); + const overlapEnd = Math.min(binanceRecent[binanceRecent.length - 1].t, chainlinkRecent[chainlinkRecent.length - 1].t); + const diffs: number[] = []; + + if (overlapEnd - overlapStart >= BINANCE_ALIGN_BUCKET_MS * 2) { + let bIdx = 0, cIdx = 0; + for (let bucketStart = overlapStart; bucketStart <= overlapEnd; bucketStart += BINANCE_ALIGN_BUCKET_MS) { + const bucketEnd = bucketStart + BINANCE_ALIGN_BUCKET_MS; + const bBucket: number[] = [], cBucket: number[] = []; + while (bIdx < binanceRecent.length && binanceRecent[bIdx].t < bucketStart) bIdx++; + while (cIdx < chainlinkRecent.length && chainlinkRecent[cIdx].t < bucketStart) cIdx++; + let i = bIdx; + while (i < binanceRecent.length && binanceRecent[i].t < bucketEnd) { bBucket.push(binanceRecent[i].price); i++; } + let j = cIdx; + while (j < chainlinkRecent.length && chainlinkRecent[j].t < bucketEnd) { cBucket.push(chainlinkRecent[j].price); j++; } + const bMed = calcMedian(bBucket), cMed = calcMedian(cBucket); + if (bMed != null && cMed != null) diffs.push(cMed - bMed); + } + } + + if (!diffs.length) { + return chainlinkRecent[chainlinkRecent.length - 1].price - binanceRecent[binanceRecent.length - 1].price; + } + if (diffs.length < 5) return calcTrimmedMean(diffs, 0); + + const median = calcMedian(diffs); + if (median == null) return null; + const absDeviations = diffs.map((d) => Math.abs(d - median)); + const mad = calcMedian(absDeviations) ?? 0; + const threshold = Math.max(10, mad * 3); + const filtered = diffs.filter((d) => Math.abs(d - median) <= threshold); + const stable = filtered.length >= 3 ? filtered : diffs; + return calcTrimmedMean(stable, 0.15); +} + +function refreshBinanceOffset(sym: MarketSymbol): void { + const next = calculateBinanceOffset(sym, false); + if (next == null) return; + const ss = symbolStates.get(sym)!; + const prev = ss.binanceOffset; + if (prev != null && Math.abs(prev - next) <= BINANCE_OFFSET_EPSILON) return; + ss.binanceOffset = next; + if (prev == null) { + console.log(`[BinanceOffset] ${sym.toUpperCase()} init offset ${next >= 0 ? "+" : ""}${next.toFixed(2)}`); + } +} + +function maybeInitBinanceOffset(sym: MarketSymbol): void { + const ss = symbolStates.get(sym)!; + if (ss.binanceOffset != null) return; + const v = calculateBinanceOffset(sym, true); + if (v == null) return; + ss.binanceOffset = v; + console.log(`[BinanceOffset] ${sym.toUpperCase()} init offset ${v >= 0 ? "+" : ""}${v.toFixed(2)}`); +} + +// ── Binance WS (subscribes only to aggTrade for binanceOffset calibration) ── +function startBinanceWs(sym: MarketSymbol): void { + const ss = symbolStates.get(sym)!; + const config = MARKETS[`${sym}-5m` as MarketKey]; + // Subscribe only to aggTrade, no klines needed (collector does not run momentum strategies) + const url = `wss://stream.binance.com:9443/stream?streams=${config.binanceSymbol}@aggTrade`; + const ws = new WebSocket(url); + ss.binanceWs = ws; + + ws.on("open", () => { + if (stopped) return; + console.log(ss.binanceWsAttempt === 0 ? `[BinanceWS] ${sym.toUpperCase()} connected` : `[BinanceWS] ${sym.toUpperCase()} reconnected`); + ss.binanceWsAttempt = 0; + }); + + ws.on("message", (data) => { + if (stopped) return; + try { + const raw = JSON.parse(data.toString()) as { stream?: string; data?: Record }; + const stream = raw.stream; + const payload = raw.data; + if (!stream || !payload) return; + + if (stream.endsWith("@aggTrade")) { + const p = payload as { p?: string; T?: number }; + const price = parseFloat(p.p ?? ""); + const t = p.T ?? Date.now(); + if (!price) return; + ss.binanceHistory.push({ t, price }); + trimHistory(ss.binanceHistory, t - HISTORY_RETENTION_MS, MAX_BINANCE_HISTORY_POINTS); + ss.binanceLastTickAt = Date.now(); + maybeInitBinanceOffset(sym); + } + } catch { /* ignore */ } + }); + + ws.on("close", () => { + if (stopped) return; + const delay = backoffDelay(ss.binanceWsAttempt++); + console.log(`[BinanceWS] ${sym.toUpperCase()} disconnected, reconnecting in ${delay}ms (attempt ${ss.binanceWsAttempt})`); + setTimeout(() => startBinanceWs(sym), delay); + }); + + ws.on("error", (err) => { + console.error(`[BinanceWS] ${sym.toUpperCase()} error:`, err.message); + }); +} + +// ── Chainlink WS (shared per symbol) ── +function startChainlinkWs(sym: MarketSymbol): void { + const ss = symbolStates.get(sym)!; + const config = MARKETS[`${sym}-5m` as MarketKey]; + const ws = new WebSocket(CHAINLINK_WS_URL); + ss.chainlinkWs = ws; + + ws.on("open", () => { + if (stopped) return; + console.log(ss.chainlinkWsAttempt === 0 ? `[ChainlinkWS] ${sym.toUpperCase()} connected` : `[ChainlinkWS] ${sym.toUpperCase()} reconnected`); + ss.chainlinkWsAttempt = 0; + // Subscribe only to the price topic, not activity (order events not needed) + ws.send(JSON.stringify({ + action: "subscribe", + subscriptions: [ + { topic: "crypto_prices_chainlink", type: "update", filters: JSON.stringify({ symbol: config.chainlinkSymbol }) }, + ], + })); + }); + + ws.on("message", (data) => { + if (stopped) return; + try { + const msg = JSON.parse(data.toString()) as { topic?: string; type?: string; timestamp?: number; payload?: { value?: number; timestamp?: number } }; + if (msg.topic === "crypto_prices_chainlink" && msg.type === "update") { + const val = msg.payload?.value; + if (val == null) return; + const t = msg.payload?.timestamp ?? msg.timestamp ?? Date.now(); + ss.currentPrice = val; + ss.priceHistory.push({ t, price: val }); + trimHistory(ss.priceHistory, t - HISTORY_RETENTION_MS, MAX_CHAINLINK_HISTORY_POINTS); + ss.chainlinkLastTickAt = Date.now(); + maybeInitBinanceOffset(sym); + } + } catch { /* ignore */ } + }); + + ws.on("close", () => { + if (stopped) return; + const delay = backoffDelay(ss.chainlinkWsAttempt++); + console.log(`[ChainlinkWS] ${sym.toUpperCase()} disconnected, reconnecting in ${delay}ms`); + setTimeout(() => startChainlinkWs(sym), delay); + }); + + ws.on("error", (err) => { + console.error(`[ChainlinkWS] ${sym.toUpperCase()} error:`, err.message); + }); +} + +// ── Polymarket Market WS (independent per market) ── +function startMarketWs(ms: MarketState): void { + const ws = new WebSocket(MARKET_WS_URL); + ms.marketWs = ws; + const upTokenId = ms.upTokenId; + + ws.on("open", () => { + if (stopped) return; + console.log(ms.marketWsAttempt === 0 ? `[MarketWS] ${ms.key} connected` : `[MarketWS] ${ms.key} reconnected`); + ms.marketWsAttempt = 0; + ws.send(JSON.stringify({ + assets_ids: [ms.upTokenId, ms.downTokenId], + type: "market", + custom_feature_enabled: true, + })); + if (ms.marketPingTimer) clearInterval(ms.marketPingTimer); + ms.marketPingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) ws.send("PING"); + }, 10000); + }); + + ws.on("message", (data) => { + if (stopped) return; + const msg = data.toString(); + if (msg === "PONG" || msg === "[]") return; + try { + const events = Array.isArray(JSON.parse(msg)) ? JSON.parse(msg) : [JSON.parse(msg)]; + for (const evt of events) { + if (evt.bids !== undefined && evt.asks !== undefined) { + if (evt.asset_id && evt.asset_id !== upTokenId) continue; + ms.bids.clear(); ms.asks.clear(); + for (const b of (evt.bids as { price: string; size: string }[])) { + if (Number(b.size) > 0) ms.bids.set(b.price, b.size); + } + for (const a of (evt.asks as { price: string; size: string }[])) { + if (Number(a.size) > 0) ms.asks.set(a.price, a.size); + } + updateBestBidAsk(ms); + ms.marketLastTickAt = Date.now(); + } else if (evt.event_type === "best_bid_ask") { + if (evt.asset_id && evt.asset_id !== upTokenId) continue; + if (evt.best_bid != null) ms.bestBid = String(evt.best_bid); + if (evt.best_ask != null) ms.bestAsk = String(evt.best_ask); + ms.marketLastTickAt = Date.now(); + } else if (evt.event_type === "price_change" && evt.price_changes) { + for (const change of evt.price_changes as Record[]) { + if (change.asset_id !== upTokenId) continue; + if (change.price && change.size !== undefined) { + const size = Number(change.size); + const map = change.side === "BUY" ? ms.bids : ms.asks; + if (size > 0) map.set(change.price, change.size); + else map.delete(change.price); + } + } + updateBestBidAsk(ms); + ms.marketLastTickAt = Date.now(); + } + } + } catch { /* ignore */ } + }); + + ws.on("close", () => { + if (stopped) return; + if (ms.marketPingTimer) { clearInterval(ms.marketPingTimer); ms.marketPingTimer = null; } + const delay = backoffDelay(ms.marketWsAttempt++); + console.log(`[MarketWS] ${ms.key} disconnected, reconnecting in ${delay}ms`); + setTimeout(() => startMarketWs(ms), delay); + }); + + ws.on("error", (err) => { + console.error(`[MarketWS] ${ms.key} error:`, err.message); + }); +} + +function updateBestBidAsk(ms: MarketState): void { + // Take the best bid/ask from the order book + let bestBid = 0, bestAsk = 1; + for (const p of ms.bids.keys()) { const n = Number(p); if (n > bestBid) bestBid = n; } + for (const p of ms.asks.keys()) { const n = Number(p); if (n < bestAsk && n > 0) bestAsk = n; } + if (bestBid > 0) ms.bestBid = bestBid.toFixed(2); + if (bestAsk < 1) ms.bestAsk = bestAsk.toFixed(2); +} + +// ── Polymarket REST: get token ── +interface MarketInfo { + conditionId: string; + upTokenId: string; + downTokenId: string; + windowStart: number; + windowEnd: number; + eventStartTime: string; + endDate: string; +} + +async function fetchMarketInfo(config: MarketConfig, windowStart: number): Promise { + const slug = `${config.slugPrefix}-${windowStart}`; + try { + const res = await fetch(`${GAMMA_URL}/events?slug=${slug}`); + const events = await res.json() as Record[]; + if (!events?.length) return null; + const event = events[0]; + const market = ((event.markets || []) as Record[])[0]; + if (!market) return null; + const tokens = JSON.parse(market.clobTokenIds as string || "[]") as string[]; + const outcomes = JSON.parse(market.outcomes as string || "[]") as string[]; + const upIdx = outcomes.findIndex((o) => o.toLowerCase() === "up"); + return { + conditionId: market.conditionId as string, + upTokenId: tokens[upIdx >= 0 ? upIdx : 0], + downTokenId: tokens[upIdx >= 0 ? 1 - upIdx : 1], + windowStart, + windowEnd: windowStart + config.periodSeconds, + eventStartTime: market.eventStartTime as string || new Date(windowStart * 1000).toISOString(), + endDate: market.endDate as string || new Date((windowStart + config.periodSeconds) * 1000).toISOString(), + }; + } catch (err) { + console.warn(`[Market] ${config.key} query failed slug=${slug}:`, (err as Error).message); + return null; + } +} + +// ── Polymarket REST: get PTB ── +async function fetchPTB(config: MarketConfig, eventStartTime: string, endDate: string): Promise { + try { + const url = `https://polymarket.com/api/crypto/crypto-price?symbol=${config.cryptoPriceSymbol}&eventStartTime=${encodeURIComponent(eventStartTime)}&variant=${config.cryptoPriceVariant}&endDate=${encodeURIComponent(endDate)}`; + const data = await fetch(url).then((r) => r.json()) as { openPrice?: number | null }; + if (data.openPrice != null) return data.openPrice; + return null; + } catch { + return null; + } +} + +// ── Switch window ── +async function switchToWindow(ms: MarketState, targetWindow: number): Promise { + const info = await fetchMarketInfo(ms.config, targetWindow); + if (!info) { + console.warn(`[Window] ${ms.key} switch failed windowStart=${targetWindow}, retrying in 5s`); + setTimeout(() => switchToWindow(ms, targetWindow), 5000); + return; + } + + // Close the old marketWs + if (ms.marketWs) { + ms.marketWs.removeAllListeners("close"); + ms.marketWs.close(); + ms.marketWs = null; + } + if (ms.marketPingTimer) { clearInterval(ms.marketPingTimer); ms.marketPingTimer = null; } + + // Update window info + ms.upTokenId = info.upTokenId; + ms.downTokenId = info.downTokenId; + ms.conditionId = info.conditionId; + ms.windowStart = info.windowStart; + ms.windowEnd = info.windowEnd; + ms.priceToBeat = null; + ms.bids.clear(); + ms.asks.clear(); + ms.bestBid = "-"; + ms.bestAsk = "-"; + ms.marketWsAttempt = 0; + ms.ptbRetryCount = 0; + + console.log(`[Window] ${ms.key} → ${info.windowStart} (${new Date(info.windowStart * 1000).toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })})`); + + // Start the new marketWs + startMarketWs(ms); + + // Fetch PTB asynchronously + const tryFetchPTB = async () => { + if (stopped || ms.windowStart !== info.windowStart) return; + if (ms.ptbRetryCount >= PTB_MAX_RETRIES) { + console.warn(`[PTB] ${ms.key} failed ${PTB_MAX_RETRIES} times, skipping this window`); + return; + } + const ptb = await fetchPTB(ms.config, info.eventStartTime, info.endDate); + if (ptb != null) { + ms.priceToBeat = ptb; + console.log(`[PTB] ${ms.key} → $${ptb.toFixed(priceDecimals(ptb))}`); + } else { + ms.ptbRetryCount++; + setTimeout(tryFetchPTB, PTB_RETRY_INTERVAL_MS); + } + }; + tryFetchPTB(); + + // Schedule the next switch + if (ms.switchTimer) clearTimeout(ms.switchTimer); + const msUntilEnd = info.windowEnd * 1000 - Date.now(); + ms.switchTimer = setTimeout(() => { + const nextWindow = getCurrentWindowStart(ms.config.periodSeconds); + switchToWindow(ms, nextWindow); + }, Math.max(0, msUntilEnd)); +} + +// ── Write tick ── +function writeTickFor(ms: MarketState): void { + if (ms.priceToBeat == null) return; + const ss = symbolStates.get(ms.config.symbol)!; + if (ss.binanceOffset == null) return; + const lastBinance = ss.binanceHistory[ss.binanceHistory.length - 1]; + if (!lastBinance) return; + + const diff = lastBinance.price - (ms.priceToBeat - ss.binanceOffset); + const bid = Number(ms.bestBid); + const ask = Number(ms.bestAsk); + if (!Number.isFinite(bid) || !Number.isFinite(ask) || bid <= 0 || ask <= 0) return; + const upPct = Math.round((bid + ask) / 2 * 100); + const now = Date.now(); + const rem = Math.max(0, ms.windowEnd - Math.floor(now / 1000)); + + const dec = priceDecimals(ms.priceToBeat); + const factor = Math.pow(10, dec); + + const record = { + type: "tick", + ts: now, + symbol: ms.config.symbol, + period: ms.config.period, + windowStart: ms.windowStart, + diff: Math.round(diff * factor) / factor, + upPct, + rem, + }; + + // File naming: YYYY-MM-DD-{sym}-{period}.jsonl (unified new format, BTC 5m no longer uses the old name) + const date = getCstDateStr(now); + const filename = `${date}-${ms.config.symbol}-${ms.config.period}.jsonl`; + const path = resolve(DATA_DIR, filename); + try { + appendFileSync(path, JSON.stringify(record) + "\n"); + } catch (err) { + console.warn(`[Write] ${ms.key} failed:`, (err as Error).message); + } +} + +// ── Clean up old files ── +function cleanupOldFiles(): void { + try { + if (!existsSync(DATA_DIR)) return; + const files = readdirSync(DATA_DIR).filter((f) => /^\d{4}-\d{2}-\d{2}-\w+-\w+\.jsonl$/.test(f)); + const byDate = new Map(); + for (const f of files) { + const date = f.slice(0, 10); + if (!byDate.has(date)) byDate.set(date, []); + byDate.get(date)!.push(f); + } + const dates = [...byDate.keys()].sort(); + if (dates.length <= RETENTION_DAYS) return; + const toDelete = dates.slice(0, dates.length - RETENTION_DAYS); + for (const d of toDelete) { + for (const f of byDate.get(d) || []) { + try { + unlinkSync(resolve(DATA_DIR, f)); + console.log(`[Cleanup] deleted old file: ${f}`); + } catch {} + } + } + } catch (err) { + console.warn(`[Cleanup] cleanup failed:`, (err as Error).message); + } +} + +// ── Health check ── +function healthCheck(): void { + const now = Date.now(); + for (const [sym, ss] of symbolStates) { + if (ss.binanceLastTickAt > 0 && now - ss.binanceLastTickAt > STALE_DATA_THRESHOLD_MS) { + console.warn(`[Health] ${sym.toUpperCase()} BinanceWS no data for ${Math.floor((now - ss.binanceLastTickAt)/1000)}s, forcing reconnect`); + ss.binanceWs?.close(); // trigger auto reconnect + } + if (ss.chainlinkLastTickAt > 0 && now - ss.chainlinkLastTickAt > STALE_DATA_THRESHOLD_MS) { + console.warn(`[Health] ${sym.toUpperCase()} ChainlinkWS no data for ${Math.floor((now - ss.chainlinkLastTickAt)/1000)}s, forcing reconnect`); + ss.chainlinkWs?.close(); + } + } + for (const [key, ms] of marketStates) { + if (ms.marketLastTickAt > 0 && now - ms.marketLastTickAt > STALE_DATA_THRESHOLD_MS) { + console.warn(`[Health] ${key} MarketWS no data for ${Math.floor((now - ms.marketLastTickAt)/1000)}s, forcing reconnect`); + ms.marketWs?.close(); + } + } +} + +// ── Print status summary ── +function printStatus(): void { + console.log(`\n────────── Status summary (${new Date().toLocaleString("zh-CN", { timeZone: "Asia/Shanghai" })}) ──────────`); + for (const [sym, ss] of symbolStates) { + const offsetStr = ss.binanceOffset != null ? `${ss.binanceOffset >= 0 ? "+" : ""}${ss.binanceOffset.toFixed(2)}` : "—"; + console.log(` ${sym.toUpperCase()}: binance ${ss.binanceHistory.length}pt, chainlink ${ss.priceHistory.length}pt, offset ${offsetStr}`); + } + for (const [key, ms] of marketStates) { + const ptb = ms.priceToBeat != null ? `$${ms.priceToBeat.toFixed(priceDecimals(ms.priceToBeat))}` : "—"; + console.log(` ${key}: window ${ms.windowStart}, PTB ${ptb}, bid/ask ${ms.bestBid}/${ms.bestAsk}`); + } + console.log(`────────────────────────────────────────\n`); +} + +// ── Start ── +async function start(): Promise { + console.log(`╔══════════════════════════════════════════╗`); + console.log(`║ Data collector starting ║`); + console.log(`║ Data dir: backtest-data/collector/ ║`); + console.log(`║ Retention days: ${String(RETENTION_DAYS).padEnd(24)}║`); + console.log(`╚══════════════════════════════════════════╝\n`); + + // Initialize per-symbol state + for (const sym of ALL_SYMBOLS) { + symbolStates.set(sym, { + symbol: sym, + binanceWs: null, binanceWsAttempt: 0, binanceLastTickAt: 0, + chainlinkWs: null, chainlinkWsAttempt: 0, chainlinkLastTickAt: 0, + binanceHistory: [], priceHistory: [], + currentPrice: null, binanceOffset: null, + }); + } + + // Initialize per-market state + for (const [key, config] of Object.entries(MARKETS)) { + marketStates.set(key as MarketKey, { + key: key as MarketKey, + config, + upTokenId: "", downTokenId: "", conditionId: "", + windowStart: 0, windowEnd: 0, + priceToBeat: null, + bids: new Map(), asks: new Map(), + bestBid: "-", bestAsk: "-", + marketWs: null, marketWsAttempt: 0, marketLastTickAt: 0, + marketPingTimer: null, switchTimer: null, ptbRetryCount: 0, + }); + } + + // Start BinanceWS (per symbol, staggered by 500ms) + console.log(`[Start] Connecting Binance WS...`); + for (const sym of ALL_SYMBOLS) { + startBinanceWs(sym); + await new Promise((r) => setTimeout(r, 500)); + } + + // Start ChainlinkWS (per symbol, staggered by 500ms) + console.log(`\n[Start] Connecting Chainlink WS...`); + for (const sym of ALL_SYMBOLS) { + startChainlinkWs(sym); + await new Promise((r) => setTimeout(r, 500)); + } + + // Start the window subscription for each market (staggered by 500ms) + console.log(`\n[Start] Subscribing to market windows...`); + for (const [key, ms] of marketStates) { + const w = getCurrentWindowStart(ms.config.periodSeconds); + switchToWindow(ms, w); + await new Promise((r) => setTimeout(r, 500)); + } + + console.log(`\n[Start] ✓ All ready, starting collection...\n`); + + // Start timers + setInterval(() => { + for (const ms of marketStates.values()) writeTickFor(ms); + }, TICK_WRITE_INTERVAL_MS); + + setInterval(() => { + for (const sym of ALL_SYMBOLS) refreshBinanceOffset(sym); + }, BINANCE_ALIGN_REFRESH_MS); + + setInterval(healthCheck, HEALTH_CHECK_INTERVAL_MS); + + setInterval(() => { + const today = getCstDateStr(); + if (today !== lastCleanupDate) { + cleanupOldFiles(); + lastCleanupDate = today; + } + }, 3600 * 1000); // check once per hour + + setInterval(printStatus, 5 * 60 * 1000); // print status summary every 5 minutes +} + +// ── Process-level protection ── +process.on("uncaughtException", (err) => { + console.error("[Collector] uncaught exception:", err); + // do not exit +}); + +process.on("unhandledRejection", (err) => { + console.error("[Collector] unhandled Promise rejection:", err); +}); + +process.on("SIGTERM", () => { + console.log("\n[Collector] received SIGTERM, shutting down gracefully..."); + stopped = true; + for (const ms of marketStates.values()) { + if (ms.marketWs) ms.marketWs.close(); + if (ms.marketPingTimer) clearInterval(ms.marketPingTimer); + if (ms.switchTimer) clearTimeout(ms.switchTimer); + } + for (const ss of symbolStates.values()) { + if (ss.binanceWs) ss.binanceWs.close(); + if (ss.chainlinkWs) ss.chainlinkWs.close(); + } + setTimeout(() => process.exit(0), 1000); +}); + +process.on("SIGINT", () => { + console.log("\n[Collector] received SIGINT, shutting down gracefully..."); + process.emit("SIGTERM"); +}); + +start().catch((err) => { + console.error("[Collector] startup failed:", err); + process.exit(1); +}); diff --git a/docs/README-USER.md b/docs/README-USER.md new file mode 100644 index 0000000..427c40c --- /dev/null +++ b/docs/README-USER.md @@ -0,0 +1,128 @@ +# BTC 5m User Edition + +## Included Files + +- `server.ts` +- `index.html` +- `package.json` +- `package-lock.json` +- `.env.example` +- `start.sh` +- `start.bat` +- `STRATEGIES-GUIDE.md` + +## How to Use + +1. Install Node.js 20 or higher. +2. Copy `.env.example` to `.env`. +3. Fill in: + - `POLYMARKET_PRIVATE_KEY` — Polygon wallet private key + - `POLYMARKET_PROXY_ADDRESS` — **NOT the deposit address!** Log in to polymarket.com → top-right avatar → Settings → Wallet → copy "Proxy Wallet" (maps one-to-one with the private key; a wrong value triggers invalid signature) +4. Adjust as needed: + - `APP_MODE=full`: with the web panel + - `APP_MODE=headless`: backend only + - `STRATEGY_S1_ENABLED/STRATEGY_S2_ENABLED/STRATEGY_S3_ENABLED` + - `STRATEGY_S1_AMOUNT/STRATEGY_S2_AMOUNT/STRATEGY_S3_AMOUNT` +5. Start: + - macOS / Linux: `./start.sh` + - Windows: double-click `start.bat` + +## Notes + +- On every startup, the default strategy config is initialized from `.env`. +- Changing strategy switches and amounts in the web UI only applies to the current run; after restart it follows `.env` again. +- In `full` mode you can view status, place orders manually, and temporarily toggle strategies via the web UI. +- In `headless` mode you can view status via `/api/state`. +- `STRATEGIES-GUIDE.md` summarizes the triggers, take-profit/stop-loss, buy confirmation, and state-machine semantics of the current 3 strategies. + +## Security + +- Do not give your real `.env` and `.polymarket-creds.json` to anyone. +- If deploying to a cloud server, we recommend accessing the web panel via an SSH tunnel and not exposing the port directly to the public internet. + +## Accessing the Panel via SSH Tunnel + +If the service runs on a cloud server, we recommend using an SSH tunnel to access the panel from your local browser. + +### 1. Server-Side Requirements + +- Set `APP_MODE=full` in `.env` +- The service is already running normally + +### 2. Password Login + +Run on your own computer: + +```bash +ssh -L 3456:127.0.0.1:3456 username@server_IP +``` + +For example: + +```bash +ssh -L 3456:127.0.0.1:3456 root@1.2.3.4 +``` + +Then open in your local browser: + +```text +http://127.0.0.1:3456 +``` + +### 3. Key-Based Login + +If the server uses private-key login: + +```bash +ssh -i ~/.ssh/your_private_key_file -L 3456:127.0.0.1:3456 username@server_IP +``` + +For example: + +```bash +ssh -i ~/.ssh/my-server.pem -L 3456:127.0.0.1:3456 ubuntu@1.2.3.4 +``` + +Then open in your local browser: + +```text +http://127.0.0.1:3456 +``` + +### 4. If the SSH Port Is Not 22 + +For example, if the SSH port is `2222`: + +```bash +ssh -i ~/.ssh/my-server.pem -p 2222 -L 3456:127.0.0.1:3456 ubuntu@1.2.3.4 +``` + +### 5. If Local Port 3456 Is Already in Use + +You can change the local port to `8888`: + +```bash +ssh -L 8888:127.0.0.1:3456 username@server_IP +``` + +Or: + +```bash +ssh -i ~/.ssh/my-server.pem -L 8888:127.0.0.1:3456 ubuntu@1.2.3.4 +``` + +Then open in the browser: + +```text +http://127.0.0.1:8888 +``` + +### 6. Notes + +- As long as you can SSH into the server, you can access the panel this way. +- Closing the SSH tunnel only affects local viewing; it does not affect the program continuing to run on the server. +- If you only want to check the API status, you can also access it locally: + +```bash +curl http://127.0.0.1:3456/api/state +``` diff --git a/docs/STRATEGIES-GUIDE.md b/docs/STRATEGIES-GUIDE.md new file mode 100644 index 0000000..da111dd --- /dev/null +++ b/docs/STRATEGIES-GUIDE.md @@ -0,0 +1,183 @@ +# Strategy Guide + +> **Version:** v4.2.0 +> **Author:** Penguin Sensei · 岳 · [@x_188888_x](https://x.com/x_188888_x) + +## ⚠ Important Disclaimer + +The strategies built into this tool are **examples only**, intended to demonstrate how to use the strategy framework, and **cannot guarantee profits**. +The Polymarket BTC 5-minute market is highly volatile, and any strategy with fixed parameters carries the risk of becoming ineffective. + +**Recommendations:** + +- Run one or two windows with the smallest amount and observe whether the entry/exit logic matches your judgment +- Hover in the frontend to see each strategy's entry/exit conditions +- If you have good entry/exit ideas, new data patterns, or want to work on backtest optimization together, **feel free to contact the author and refine them jointly**, + to achieve a 1+1 > 2 effect + +--- + +## Overview + +There are currently 3 built-in example strategies (only the diff and momentum types are shown; the prob-chase type is not included as an example): + +| Key | Name | Type | Summary | +|-----|------|------|------| +| D1 | Diff 1 · Standard Enhanced | Diff | diff cross entry + trailing stop + drawdown take-profit + stepped take-profit | +| D2 | Diff 2 · Tail Sweep | Diff | large-diff entry at the window tail + stepped take-profit | +| M1 | Momentum 1 | Momentum | 6-factor scoring entry, holds to window end and is decided by settlement | + +Core principles: + +- The authoritative state of automated strategies lives in the backend `server.ts` +- Buy confirmation relies on the local position `localSize` advanced by `UserWS` +- API positions are used only for reconciliation, releasing timed-out buy orders, and clearing residual positions after a sell +- Closing the frontend does not affect the backend strategy from continuing to run + +--- + +## Common Terms + +- **`diff`** — Binance latest price - (PriceToBeat - BinanceOffset); the core indicator for diff-strategy entry +- **`upPct / dnPct`** — the current up/down order book implied probability +- **`rem`** — seconds remaining in the current 5-minute window +- **`localSize`** — the local position advanced by UserWS; both buy confirmation and sell tracking rely on it +- **`apiVerified`** — the API and local positions are aligned + +--- + +## Backend State Machine + +- `IDLE` — no strategy is enabled +- `SCANNING` — scanning for entry conditions +- `BUYING` — buy triggered, order being sent +- `WAIT_FILL` — the first 10 seconds after the buy order is sent, only waiting for UserWS fill confirmation +- `RECONCILING_FILL` — not confirmed within 10 seconds, entering the deferred-confirmation state; after 15 seconds, only if the API also confirms no position does it return to `SCANNING` +- `HOLDING` — position confirmed, starting to run take-profit/stop-loss +- `SELLING` / `WAIT_SELL_FILL` — selling / waiting for sell confirmation +- `DONE` — round ended; when the position is not reconciled, it waits for API reconciliation before checking for residual positions + +--- + +## Strategy D1 · Standard Enhanced (Diff Type) + +### Entry Window + +- Detected between `210s ~ 50s` remaining + +### Entry Conditions + +- **Buy up**: previous tick diff ≤ +35, current tick diff > +35, up probability < 80% +- **Buy down**: previous tick diff ≥ -35, current tick diff < -35, down probability < 80% + +("Re-cross above/below triggers," not "buy whenever the current value is met") + +### Cooldown Lock (Prevents Chasing Highs and Flip-Flopping) + +**Neutral reset**: `|diff| ≤ 25` sustained for 3 seconds → release all cooldown locks + +**Single-direction lock** (locks that direction if any is met, until returning to neutral): + +- High-probability contamination seen first: while diff is within the trigger threshold, the up/down probability is already ≥ 80% +- Overheated: diff ≥ +55 and up probability ≥ 85% (buy-up direction) / diff ≤ -55 and down probability ≥ 85% (buy-down direction) + +### Exit Mechanisms (Multiple) + +1. **Stepped take-profit** — rises linearly from 90% at 210s to 100% at 10s; sells when the current probability reaches the threshold of the moment +2. **Drawdown take-profit** — after the probability peak during holding reaches ≥ 85%, sells once it pulls back 8 percentage points +3. **Trailing stop** — enabled after a minimum holding of 3 seconds; triggered when diff pulls back 20 points from its peak +4. **Backstop stop-loss** — buy-up diff ≤ +5 / buy-down diff ≥ -5, stop out immediately +5. **Forced close** — when rem ≤ 10s: take profit if probability ≥ 70%, otherwise stop out + +--- + +## Strategy D2 · Tail Sweep (Diff Type) + +### Entry Window + +- Detected between `60s ~ 1s` remaining + +### Entry Conditions + +- **Buy up**: diff > +50 and up probability < 95% +- **Buy down**: diff < -50 and down probability < 95% + +### Exit Mechanisms + +**Stepped take-profit** (tightened in tiers by time remaining): + +- `rem ≥ 40s`: probability ≥ 98% +- `20s ≤ rem < 40s`: probability ≥ 99% +- `10s ≤ rem < 20s`: probability ≥ 100% +- `rem < 10s`: hold to the end, decided by settlement + +**Stop-loss**: + +- Buy-up diff ≤ +5 +- Buy-down diff ≥ -5 + +--- + +## Strategy M1 · Momentum (Momentum Type) + +### Entry Window + +- Detected when more than 60s remain (the final segment of the window does not participate in momentum evaluation) + +### Entry Logic + +Based on **6-factor momentum scoring** (see `strategies/_core/s6-core.ts` for details): + +- RSI deviation +- Volume expansion +- 1-minute candle direction +- Price change magnitude +- Candle body ratio +- Number of consecutive same-color candles +- MA7 position +- (Auxiliary filter) MA120 long-term trend + 5-minute structure + +An UP threshold triggers buy up, a DOWN threshold triggers buy down (the short threshold is stricter). + +### Exit Mechanisms + +**No take-profit, no stop-loss, no forced close**; holds to the window end and the win/loss is decided by Polymarket settlement. + +This is a "pure settlement" style strategy example: verifying "whether the momentum direction judgment is accurate" rather than "agonizing over mid-window take-profit/stop-loss." + +--- + +## Buy/Sell Confirmation and Residual-Position Handling + +### Buy Confirmation Flow + +1. Strategy triggers → `BUYING` sends order → `WAIT_FILL` waits for UserWS +2. Not confirmed within 10 seconds: enter `RECONCILING_FILL`, keep waiting for UserWS +3. After 15 seconds, only if the API also confirms no position is the buy order released and it returns to scanning + +### Selling + +- When selling an unaligned position, reserve a `0.05`-share buffer to avoid insufficient balance +- If residual positions remain after API alignment, clear them again + +--- + +## Configuration Source + +- On startup, strategy config is read from `.env` (`STRATEGY_{D1,D2,M1}_ENABLED`, etc.) +- Frontend changes only affect the current process and are not persisted across restarts +- After restart, `.env` still takes precedence + +--- + +## Usage Recommendations + +- We recommend running on the premise that "the account has no position in the current window at startup" +- If you need to view status remotely, prefer `APP_MODE=full` + an SSH tunnel +- The example strategy parameters are all empirical values under historical data; please backtest and verify them yourself before live trading + +--- + +## Co-Development + +Got a good strategy? Let's optimize it together! Contact the author: [@x_188888_x](https://x.com/x_188888_x) diff --git a/docs/STRATEGIES-TODO.md b/docs/STRATEGIES-TODO.md new file mode 100644 index 0000000..90b7e50 --- /dev/null +++ b/docs/STRATEGIES-TODO.md @@ -0,0 +1,66 @@ +Currently, after one strategy finishes executing, the subsequent strategies are not executed; optimize to place multiple orders + +bestBid/bestAsk is sometimes fetched inaccurately; use the REST API to periodically calibrate the WS, fetching once every 5 seconds, and reconnect this WS after 3 consecutive readings exceed the threshold + +Do a code review and optimize the code + +Build a backend-only version for deployment on a cloud server + + +Crash bug to reproduce (observed that every time memory fills up, the Binance line lags on the time axis, but the data trend is real-time; only the overall line becomes laggy, and the yellow dots also drift toward the left of the x-axis; after switching the time window, memory is released again) +<--- Last few GCs ---> + +[57571:0x748400000] 855007 ms: Mark-Compact 3998.8 (4144.0) -> 3998.8 (4144.0) MB, pooled: 0 MB, 29.54 / 0.00 ms (average mu = 0.384, current mu = 0.086) allocation failure; scavenge might not succeed +[57571:0x748400000] 855051 ms: Mark-Compact 3999.0 (4144.2) -> 3998.9 (4144.2) MB, pooled: 0 MB, 40.33 / 0.00 ms (average mu = 0.242, current mu = 0.072) allocation failure; scavenge might not succeed + + +<--- JS stacktrace ---> + +FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory +----- Native stack trace ----- + + 1: 0x1006039c0 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 2: 0x1007dae90 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 3: 0x100a044cc v8::internal::Heap::stack() [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 4: 0x100a0272c v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 5: 0x1009f83c0 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 6: 0x1009f8d8c v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 7: 0x1009cb68c v8::internal::FactoryBase::NewRawOneByteString(int, v8::internal::AllocationType) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 8: 0x1009cb4c8 v8::internal::FactoryBase::NewStringFromOneByte(v8::base::Vector, v8::internal::AllocationType) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] + 9: 0x100b0dee0 v8::internal::JsonStringifier::Stringify(v8::internal::Handle, v8::internal::Handle, v8::internal::Handle) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +10: 0x100b0dc50 v8::internal::JsonStringify(v8::internal::Isolate*, v8::internal::Handle, v8::internal::Handle, v8::internal::Handle) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +11: 0x100850114 v8::internal::Builtin_JsonStringify(int, unsigned long*, v8::internal::Isolate*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +12: 0x1014bb914 Builtins_CEntry_Return1_ArgvOnStack_BuiltinExit [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +13: 0x10bfcf98c +14: 0x10bf3ea20 +15: 0x10c082408 +16: 0x10c0808cc +17: 0x10bfacaf4 +18: 0x10c12b3d0 +19: 0x10c0a9a44 +20: 0x10bfd4390 +21: 0x10c173f18 +22: 0x10c0b03ec +23: 0x10bf8f344 +24: 0x10c09eed0 +25: 0x10bea3fc4 +26: 0x10142250c Builtins_JSEntryTrampoline [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +27: 0x1014221b0 Builtins_JSEntry [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +28: 0x100957ebc v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +29: 0x10095781c v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle, v8::internal::Handle, int, v8::internal::Handle*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +30: 0x1007f2930 v8::Function::Call(v8::Isolate*, v8::Local, v8::Local, int, v8::Local*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +31: 0x100505cec node::InternalMakeCallback(node::Environment*, v8::Local, v8::Local, v8::Local, int, v8::Local*, node::async_context, v8::Local) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +32: 0x10051b1cc node::AsyncWrap::MakeCallback(v8::Local, int, v8::Local*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +33: 0x1007324b0 node::StreamBase::CallJSOnreadMethod(long, v8::Local, unsigned long, node::StreamBase::StreamBaseJSChecks) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +34: 0x100733c48 node::EmitToJSStreamListener::OnStreamRead(long, uv_buf_t const&) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +35: 0x1007b4c34 node::crypto::TLSWrap::ClearOut() [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +36: 0x1007b6b98 node::crypto::TLSWrap::OnStreamRead(long, uv_buf_t const&) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +37: 0x100738080 node::LibuvStreamWrap::OnUvRead(long, uv_buf_t const*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +38: 0x1007387c8 node::LibuvStreamWrap::ReadStart()::$_1::__invoke(uv_stream_s*, long, uv_buf_t const*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +39: 0x10140c8c4 uv__stream_io [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +40: 0x101414edc uv__io_poll [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +41: 0x101401850 uv_run [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +42: 0x100506508 node::SpinEventLoopInternal(node::Environment*) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +43: 0x10064d250 node::NodeMainInstance::Run() [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +44: 0x1005bf5f4 node::Start(int, char**) [/Users/yuepin/.nvm/versions/node/v23.11.0/bin/node] +45: 0x186b79d54 start [/usr/lib/dyld] \ No newline at end of file diff --git a/docs/screenshots/dashboard.png b/docs/screenshots/dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2b3a066ad29a570e06f3ead84c574b2f6e970d GIT binary patch literal 578221 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�vkPomzG@e*N0Cv`rM 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%0AmmJJQ&a+23aAk~#rB5`w62xO8+ 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

7$u@ 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 IM6N<$f-V`%oB#j- literal 0 HcmV?d00001 diff --git a/docs/screenshots/monitor.png b/docs/screenshots/monitor.png new file mode 100644 index 0000000000000000000000000000000000000000..a6888c5a0d44ae31335c419c804e623bebee5198 GIT binary patch literal 62217 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�|aA>tnxTp^9M)u}Yo@T43u%ir$mP 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 0000000..a865772 --- /dev/null +++ b/index.html @@ -0,0 +1,6619 @@ + + + + +BTC 5m Up/Down Order Book + + + + + +

+ +
+
+
Price to Beat
+
+
+
+
BTC current price
+
+
+
+
Diff (CL)
+
+
+
+
Diff (BN)
+
+
+
+
Volatility (30s) high>%
+
+
+
+
Remaining
+
--:--
+
+ + + + + + + + + +
+ +
+
+
+
Probability
+
Waiting for order book
+
+
+
+
Order book summary
+
Waiting for order book
+
+
+ +
+
Strategy
+
+
+
+
+
+ + +
+
+
+ +
+ Run params +
+ Per round + + x +
+
+ Slippage + + % +
+ + ⓘ This run only +
+
+
+ +
+
+ + Disabled +
+ + | + TP/SL: none +
+
+ + + + ⓘ + + + +
+
+
+ Backtest data + +
+
+ +
+
+ 📱 TG push > + ⓘ Help + + +
+ +
+
+
+ + +
+
+
+ Prob chase + [BTC] + | + + | + + | + Deviation - +
+
+ +
+ + +
+
+ Curve + +
+
+
+ +
+
+ + + +
+ + +
+
+
Order
+
+ + +
+
+ + +
+
+
USDC
+
+
+
+
+
+
+
+
Up position
+
+ Not calibrated +
API:
+
+
+
Down position
+
+ Not calibrated +
API:
+
+
API sync: -
+
+ + +
+
Action
+
+ + +
+
+ + +
+
+ Direction +
+ Remaining: --:-- + Diff: - + Deviation: - +
+
+
+ + +
+
+ + + +
+
+
Amount
+
+ +
+ + USDC +
+ + +
+
+ + + + +
+
+
Price: -  Estimate: -
+ +
+
+ + + + + + +
+ + +
+
+ + 📌 Conditional orders + +
+ +
+ +
+
Slippage protection (buy+/sell-)
+
+ + % +
+
+
+ +
+ + +
+
+ Real PnL - Polymarket + Loading... +
+
+ + +
+
+ Time + Type + Direction + Strategy + Shares x Price + Fee + Market + Cash flow + Position PnL +
+
Loading...
+
+ + + + diff --git a/market-configs.ts b/market-configs.ts new file mode 100644 index 0000000..48d3f6d --- /dev/null +++ b/market-configs.ts @@ -0,0 +1,110 @@ +// Multi-symbol / multi-period market configuration +// To add a new symbol / period, just add an entry to MARKETS and confirm the market exists on Polymarket + +export type MarketSymbol = "btc" | "eth" | "sol"; +export type MarketPeriod = "5m" | "15m"; + +// Composite key: `${symbol}-${period}`, used for MARKETS indexing and .active-market.json persistence +export type MarketKey = `${MarketSymbol}-${MarketPeriod}`; + +export interface MarketConfig { + key: MarketKey; + symbol: MarketSymbol; + period: MarketPeriod; + periodSeconds: number; // 5m=300, 15m=900 + displayName: string; // Frontend display, e.g. "BTC 5m" + slugPrefix: string; // Polymarket slug prefix, joined as ${slugPrefix}-${windowStart} + binanceSymbol: string; // Binance spot symbol (lowercase) + coinbaseProduct: string; // Coinbase product_id + chainlinkSymbol: string; // symbol subscribed on Polymarket Chainlink WS (e.g. "btc/usd") + cryptoPriceSymbol: string; // symbol param for polymarket.com /api/crypto/crypto-price (uppercase) + cryptoPriceVariant: string; // variant param for /api/crypto/crypto-price (fiveminute / fifteenminute) +} + +const SYMBOL_DEFS: Record> = { + btc: { + symbol: "btc", + binanceSymbol: "btcusdt", + coinbaseProduct: "BTC-USD", + chainlinkSymbol: "btc/usd", + cryptoPriceSymbol: "BTC", + }, + eth: { + symbol: "eth", + binanceSymbol: "ethusdt", + coinbaseProduct: "ETH-USD", + chainlinkSymbol: "eth/usd", + cryptoPriceSymbol: "ETH", + }, + sol: { + symbol: "sol", + binanceSymbol: "solusdt", + coinbaseProduct: "SOL-USD", + chainlinkSymbol: "sol/usd", + cryptoPriceSymbol: "SOL", + }, +}; + +const PERIOD_DEFS: Record = { + // Note: in Polymarket's crypto-price API the fifteenminute variant does not return the 15m window open price (looks like 1h or daily data), + // while the fiveminute variant's eventStartTime, after rounding, aligns exactly with the first 5m segment of the 15m window, so 15m also uses fiveminute. + "5m": { seconds: 300, periodLabel: "5m", cryptoPriceVariant: "fiveminute" }, + "15m": { seconds: 900, periodLabel: "15m", cryptoPriceVariant: "fiveminute" }, +}; + +const SYMBOL_DISPLAY: Record = { btc: "BTC", eth: "ETH", sol: "SOL" }; + +function buildMarkets(): Record { + const out = {} as Record; + for (const sym of Object.keys(SYMBOL_DEFS) as MarketSymbol[]) { + for (const p of Object.keys(PERIOD_DEFS) as MarketPeriod[]) { + const key: MarketKey = `${sym}-${p}`; + out[key] = { + ...SYMBOL_DEFS[sym], + key, + period: p, + periodSeconds: PERIOD_DEFS[p].seconds, + displayName: `${SYMBOL_DISPLAY[sym]} ${PERIOD_DEFS[p].periodLabel}`, + slugPrefix: `${sym}-updown-${p}`, + cryptoPriceVariant: PERIOD_DEFS[p].cryptoPriceVariant, + }; + } + } + return out; +} + +export const MARKETS: Record = buildMarkets(); + +export const DEFAULT_KEY: MarketKey = "btc-5m"; + +export function isValidKey(s: string): s is MarketKey { + return s in MARKETS; +} + +// Compatible with the legacy .active-market.json that only stored the symbol field +export function isLegacySymbol(s: string): s is MarketSymbol { + return s === "btc" || s === "eth" || s === "sol"; +} + +export function getBinanceWsUrl(key: MarketKey): string { + const s = MARKETS[key].binanceSymbol; + return `wss://stream.binance.com:9443/stream?streams=${s}@aggTrade/${s}@kline_1m/${s}@kline_5m`; +} + +export const ALL_PERIODS: MarketPeriod[] = ["5m", "15m"]; +export const ALL_SYMBOLS: MarketSymbol[] = ["btc", "eth", "sol"]; + +// Return a suitable number of decimal places for the current price magnitude (shared by frontend and backend, to keep display and backtest precision consistent) +// BTC ~$70000 → 0 decimals (integer) +// ETH ~$3000 → 2 decimals +// SOL ~$200 → 4 decimals +// XRP ~$2 → 4 decimals +// DOGE ~$0.x → 5 decimals +export function priceDecimals(price: number | null | undefined): number { + const p = price || 0; + if (p >= 10000) return 0; + if (p >= 1000) return 2; + if (p >= 10) return 4; + if (p >= 1) return 4; + return 5; +} diff --git a/monitor/monitor-server.ts b/monitor/monitor-server.ts new file mode 100644 index 0000000..cfe61b8 --- /dev/null +++ b/monitor/monitor-server.ts @@ -0,0 +1,678 @@ +/** + * Polymarket multi-account monitor server + * + * Start: npx tsx monitor-server.ts + * Open: http://localhost:8080 + * + * Features: + * - Read the accounts.json list + * - Serve the monitor.html static page + * - Reverse-proxy /api/proxy?port=XXXX → http://localhost:XXXX/api/state?lite=1 + * - Provide /api/accounts GET/PUT + * - Provide /api/tg/* config endpoints + long-polling responses for /status /d commands + */ +import { createServer } from "http"; +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { loadTgConfig, saveTgConfig, autoDetectChatId, sendTgMessage, editTgMessage, deleteTgMessage, answerCallbackQuery, maskToken, TgPoller, type TgConfig, type CallbackContext } from "./tg-push.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const PORT = Number(process.env.MONITOR_PORT) || 8080; +const ACCOUNTS_FILE = resolve(__dirname, "accounts.json"); +const HTML_FILE = resolve(__dirname, "monitor.html"); +const HISTORY_FILE = resolve(__dirname, ".balance-history.json"); + +interface Account { name: string; port: number; enabled?: boolean } +function loadAccounts(includeDisabled = false): Account[] { + try { + const data = JSON.parse(readFileSync(ACCOUNTS_FILE, "utf-8")); + if (Array.isArray(data)) { + const valid: Account[] = data.filter(a => a && typeof a.name === "string" && typeof a.port === "number"); + if (includeDisabled) return valid; + return valid.filter(a => a.enabled !== false); // Enabled by default, only false disables + } + } catch (err) { + console.warn("[monitor] Failed to read accounts.json:", err instanceof Error ? err.message : String(err)); + } + return []; +} + +// ── TG config (memory + file) ── +let tgConfig: TgConfig = loadTgConfig(); +function tgConfigPublic(): Record { + return { + enabled: tgConfig.enabled, + botTokenSet: !!tgConfig.botToken, + botTokenMasked: maskToken(tgConfig.botToken), + chatId: tgConfig.chatId, + }; +} + +// ── Fetch one machine's state (lite, ~5KB) ── +// On failure, return the cache if it's within 60s, marking it stale + age; TG push and the monitor page share this buffer +type FetchStateResult = + | { ok: true; state: any; stale?: boolean; staleAgeMs?: number; error?: undefined } + | { ok: false; state?: undefined; error: string }; +const STATE_CACHE_MAX_MS = 60_000; +const _stateCache = new Map(); + +async function fetchAccountState(port: number, timeoutMs = 8000): Promise { + try { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), timeoutMs); + const r = await fetch(`http://localhost:${port}/api/state?lite=1`, { signal: ctrl.signal }); + clearTimeout(timer); + if (!r.ok) return _fallbackFromCache(port, `HTTP ${r.status}`); + const state = await r.json(); + _stateCache.set(port, { state, at: Date.now() }); + return { ok: true, state }; + } catch (e) { + const raw = e instanceof Error ? e.message : String(e); + const cause = (e as { cause?: { code?: string } })?.cause?.code || ""; + let err: string; + if (cause === "ECONNREFUSED" || raw.includes("ECONNREFUSED")) err = "Connection closed"; + else if (cause === "ETIMEDOUT" || raw.includes("ETIMEDOUT")) err = "Connection timed out"; + else if (raw.includes("aborted") || raw.includes("AbortError")) err = "Request timed out"; + else if (cause === "ECONNRESET" || raw.includes("ECONNRESET")) err = "Connection closed"; + else if (raw === "fetch failed") err = "Connection closed"; + else err = raw.slice(0, 60); + return _fallbackFromCache(port, err); + } +} + +function _fallbackFromCache(port: number, error: string): FetchStateResult { + const cached = _stateCache.get(port); + if (!cached) return { ok: false, error }; + const age = Date.now() - cached.at; + if (age > STATE_CACHE_MAX_MS) return { ok: false, error }; + return { ok: true, state: cached.state, stale: true, staleAgeMs: age }; +} + +function fmtUsd(v: number | null | undefined): string { + if (v == null || !Number.isFinite(v)) return "—"; + const sign = v >= 0 ? "" : "-"; + return `${sign}$${Math.abs(v).toFixed(2)}`; +} + +// ── Balance history (record a total-balance snapshot once a day at CST 00:05) ── +interface BalanceSnapshot { total: number; online: number; offline: number; } +type BalanceHistory = Record; // key: YYYY-MM-DD (CST) + +function loadBalanceHistory(): BalanceHistory { + try { + if (!existsSync(HISTORY_FILE)) return {}; + const data = JSON.parse(readFileSync(HISTORY_FILE, "utf-8")); + return (data && typeof data === "object" && !Array.isArray(data)) ? data : {}; + } catch (err) { + console.warn("[monitor] Failed to read .balance-history.json:", err instanceof Error ? err.message : String(err)); + return {}; + } +} +function saveBalanceHistory(h: BalanceHistory): void { + try { + writeFileSync(HISTORY_FILE, JSON.stringify(h, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn("[monitor] Failed to write .balance-history.json:", err instanceof Error ? err.message : String(err)); + } +} + +/** CST date string: 2026-05-09 */ +function cstDateStr(d = new Date()): string { + const cn = new Date(d.getTime() + 8 * 3600 * 1000); + return cn.toISOString().slice(0, 10); +} + +/** Fetch all account balances, tally the total + online/offline counts */ +async function snapshotAllBalances(): Promise { + const accounts = loadAccounts(); + if (accounts.length === 0) return { total: 0, online: 0, offline: 0 }; + const results = await Promise.all(accounts.map(a => fetchAccountState(a.port))); + let total = 0, online = 0, offline = 0; + for (const r of results) { + if (r.ok && typeof r.state.usdc === "number") { + total += r.state.usdc; + online++; + } else { + offline++; + } + } + return { total: Math.round(total * 100) / 100, online, offline }; +} + +/** Record a snapshot (overwrite mode: multiple records on the same day use the latest value) */ +async function recordBalanceSnapshot(reason: string): Promise { + const accounts = loadAccounts(); + if (accounts.length === 0) { + console.log(`[balance] Skipping snapshot (no accounts), reason: ${reason}`); + return; + } + const snap = await snapshotAllBalances(); + if (snap.online === 0) { + console.log(`[balance] Skipping snapshot (all offline), reason: ${reason}`); + return; + } + const dateKey = cstDateStr(); + const history = loadBalanceHistory(); + history[dateKey] = snap; + saveBalanceHistory(history); + console.log(`[balance] Snapshot saved ${dateKey}: total=$${snap.total.toFixed(2)} online=${snap.online} offline=${snap.offline} (${reason})`); +} + +/** Schedule the daily CST 0:05 snapshot trigger */ +function scheduleDailyBalanceSnapshot(): void { + const now = new Date(); + const cnNow = now.getTime() + 8 * 3600 * 1000; + const next = new Date(cnNow); + next.setUTCHours(0, 5, 0, 0); + // If today's 0:05 has already passed, schedule for tomorrow + if (next.getTime() <= cnNow) next.setUTCDate(next.getUTCDate() + 1); + const delayMs = next.getTime() - cnNow; + setTimeout(() => { + void recordBalanceSnapshot("CST 00:05 scheduled"); + scheduleDailyBalanceSnapshot(); // Schedule the next day + }, Math.max(60_000, delayMs)).unref?.(); +} + +/** Backfill on startup: if today has no data yet → record one immediately (avoids losing the day's data after a server restart) */ +async function bootstrapBalanceSnapshot(): Promise { + const dateKey = cstDateStr(); + const history = loadBalanceHistory(); + if (history[dateKey]) { + console.log(`[balance] Today ${dateKey} already has a snapshot, skipping startup backfill`); + return; + } + // Delay 30 seconds so all accounts can connect before recording + setTimeout(() => { void recordBalanceSnapshot("startup backfill"); }, 30_000); +} + +function fmtPnl(v: number | null | undefined): string { + if (v == null || !Number.isFinite(v)) return "—"; + const sign = v >= 0 ? "+" : "-"; + return `${sign}$${Math.abs(v).toFixed(2)}`; +} + +const STATUS_PAGE_SIZE = 10; + +// ── Build overview text (tree layout + pagination, offline first) ── +async function buildStatusText(page: number = 1): Promise<{ text: string; page: number; totalPages: number }> { + const accounts = loadAccounts(); + if (accounts.length === 0) return { text: "⚠ accounts.json is empty, configure accounts on the monitor page first", page: 1, totalPages: 1 }; + + const results = await Promise.all(accounts.map(async a => ({ + account: a, + result: await fetchAccountState(a.port), + }))); + + // Full aggregation (independent of pagination); stale still counts as online + let online = 0, offline = 0, staleCount = 0; + let totalUsdc = 0, totalPnl = 0, totalCount = 0, totalClosed = 0, totalWins = 0; + for (const { result } of results) { + if (!result.ok) { offline++; continue; } + online++; + if (result.stale) staleCount++; + const s = result.state; + if (typeof s.usdc === "number") totalUsdc += s.usdc; + const p = s.pmPnl || {}; + if (typeof p.todayPnl === "number") totalPnl += p.todayPnl; + if (typeof p.todayCount === "number") totalCount += p.todayCount; + if (typeof p.todayClosedCount === "number") totalClosed += p.todayClosedCount; + if (typeof p.todayWins === "number") totalWins += p.todayWins; + } + + // Sort: offline first, the rest keep accounts.json's original order + const sorted = [...results].sort((a, b) => { + const aOff = a.result.ok ? 1 : 0; + const bOff = b.result.ok ? 1 : 0; + return aOff - bOff; + }); + + // Pagination + const totalPages = Math.max(1, Math.ceil(sorted.length / STATUS_PAGE_SIZE)); + const safePage = Math.min(Math.max(1, page), totalPages); + const start = (safePage - 1) * STATUS_PAGE_SIZE; + const pageItems = sorted.slice(start, start + STATUS_PAGE_SIZE); + + const blocks: string[] = []; + // Name format: account name takes priority (s.accountName), machine name (account.name) as a parenthesized subtitle + const fmtName = (accName: string | null | undefined, machine: string): string => { + if (accName && accName !== machine) return `${accName} (${machine})`; + return machine; + }; + for (const { account, result } of pageItems) { + if (!result.ok) { + blocks.push(`🔴 ${account.name}\n└ ${result.error}`); + continue; + } + const s = result.state; + const displayName = fmtName(typeof s.accountName === "string" ? s.accountName : null, account.name); + const usdc = typeof s.usdc === "number" ? s.usdc : null; + const p = s.pmPnl || {}; + const todayPnl = typeof p.todayPnl === "number" ? p.todayPnl : null; + const todayCount = typeof p.todayCount === "number" ? p.todayCount : null; + const todayClosed = typeof p.todayClosedCount === "number" ? p.todayClosedCount : null; + const todayWins = typeof p.todayWins === "number" ? p.todayWins : null; + + const market = s.activeMarket?.displayName || "—"; + const cfg = s.strategyConfig || {}; + const fmtStratSize = (key: string): string | null => { + const lk = String(key).toLowerCase(); + const isLimit = lk.startsWith("l"); + if (isLimit) { + const sh = cfg.shares?.[lk]; + return typeof sh === "number" ? `${sh} shares` : null; + } + const am = cfg.amount?.[lk]; + return typeof am === "number" ? `$${am}` : null; + }; + const enabledKeys = Object.keys(cfg.enabled || {}).filter(k => cfg.enabled[k]); + let stratStr: string; + if (!enabledKeys.length) { + stratStr = "No strategy enabled"; + } else { + stratStr = enabledKeys.map(k => { + const sz = fmtStratSize(k); + return sz ? `${k.toUpperCase()}(${sz})` : k.toUpperCase(); + }).join(" "); + } + const ws = s.wsStatus || {}; + const wsAllOk = ws.user && ws.market && ws.chainlink && ws.binance; + // stale shows a yellow dot + appends the data age after the name + const dot = result.stale ? "🟡" : (wsAllOk ? "🟢" : "🟡"); + let staleTag = ""; + if (result.stale) { + const ageSec = Math.max(1, Math.round((result.staleAgeMs || 0) / 1000)); + staleTag = ageSec >= 60 ? ` ⏳${Math.round(ageSec / 60)}m ago` : ` ⏳${ageSec}s ago`; + } + + const wrStr = todayClosed && todayClosed > 0 + ? `${Math.round((todayWins ?? 0) / todayClosed * 100)}%(${todayWins ?? 0}/${todayClosed})` + : "—"; + + blocks.push( + `${dot} ${displayName}${staleTag} · ${market}\n` + + `├ Balance ${fmtUsd(usdc)} · PnL ${fmtPnl(todayPnl)} · ${todayCount ?? 0} trades\n` + + `└ Win rate ${wrStr} · Strategy ${stratStr}` + ); + } + + const winRate = totalClosed > 0 ? `${Math.round((totalWins / totalClosed) * 100)}% (${totalWins}/${totalClosed})` : "—"; + const pageLine = totalPages > 1 ? `\n📄 Page ${safePage}/${totalPages}` : ""; + const head = [ + `📊 Monitor overview`, + `🕒 ${new Date().toLocaleString("en-US", { hour12: false })}`, + "", + `📦 Machines ${accounts.length} (online ${online}${staleCount > 0 ? ` / cached ${staleCount}` : ""} / offline ${offline})`, + "", + `💰 Overview`, + `├ Balance ${fmtUsd(totalUsdc)}`, + `├ PnL ${fmtPnl(totalPnl)}`, + `├ Trades ${totalCount}`, + `└ Win rate ${winRate}`, + "", + `📋 Accounts${pageLine}`, + ].join("\n"); + return { text: head + "\n" + blocks.join("\n\n"), page: safePage, totalPages }; +} + +/** Build the pagination + refresh inline keyboard */ +function buildStatusKeyboard(page: number, totalPages: number): { text: string; callback_data: string }[][] { + const buttons: { text: string; callback_data: string }[] = []; + if (totalPages > 1) { + if (page > 1) buttons.push({ text: "← Prev", callback_data: `status_page:${page - 1}` }); + buttons.push({ text: `${page}/${totalPages}`, callback_data: `noop` }); + if (page < totalPages) buttons.push({ text: "Next →", callback_data: `status_page:${page + 1}` }); + } + buttons.push({ text: "🔄 Refresh", callback_data: `refresh:${page}` }); + return [buttons]; +} + +// ── Build single-account detail text ── +async function buildDetailText(name: string): Promise { + const accounts = loadAccounts(); + const target = accounts.find(a => a.name.toLowerCase() === name.toLowerCase()); + if (!target) { + const list = accounts.map(a => a.name).join(", ") || "(empty)"; + return `⚠ Account "${name}" not found\n\nAvailable accounts: ${list}`; + } + const result = await fetchAccountState(target.port); + if (!result.ok) return `${target.name} (port=${target.port})\nOffline: ${result.error}`; + const s = result.state; + const accName = s.accountName || target.name; + const market = s.activeMarket?.displayName || "—"; + const usdc = typeof s.usdc === "number" ? s.usdc : null; + const upPos = typeof s.upLocalSize === "number" ? s.upLocalSize : 0; + const dnPos = typeof s.downLocalSize === "number" ? s.downLocalSize : 0; + const lastTrade = s.lastTradeAt; + const stratState = s.strategy?.state || "—"; + const stratActive = s.strategy?.activeStrategy; + const enabledKeys = Object.keys(s.strategyConfig?.enabled || {}).filter(k => s.strategyConfig.enabled[k]).map(k => k.toUpperCase()); + const ws = s.wsStatus || {}; + const wsList = Object.entries(ws).map(([k, v]) => `${k}:${v ? "●" : "○"}`).join(" "); + const p = s.pmPnl || {}; + + const lines = [ + `📋 ${accName} (port=${target.port})`, + `Market: ${market}`, + `USDC: ${fmtUsd(usdc)}`, + `Position: Up ${upPos.toFixed(2)} / Down ${dnPos.toFixed(2)}`, + `Strategy: ${stratActive ? `${String(stratActive).toUpperCase()}·${stratState}` : `Enabled ${enabledKeys.join(" ") || "none"} · ${stratState}`}`, + `Today: PnL ${fmtPnl(p.todayPnl)} | ${p.todayCount ?? 0} trades | Win rate ${p.todayClosedCount ? Math.round((p.todayWins ?? 0) / p.todayClosedCount * 100) + "%" : "—"} (${p.todayWins ?? 0}/${p.todayClosedCount ?? 0})`, + `Last fill: ${lastTrade ? new Date(lastTrade).toLocaleString("en-US", { hour12: false }) : "—"}`, + `WS: ${wsList}`, + ]; + return lines.join("\n"); +} + +// ── Command dispatch ── +async function handleTgCommand(cmd: string, args: string, fromChatId: number): Promise { + // Security: only respond to the configured chatId (prevents strangers from sending commands) + if (tgConfig.chatId && String(fromChatId) !== tgConfig.chatId) { + console.warn(`[monitor.TG] Ignoring command from unauthorized chat_id: ${fromChatId}`); + return; + } + if (cmd === "/start" || cmd === "/help") { + const text = [ + "📊 Polymarket multi-account monitor", + "", + "Available commands:", + "/status — Overview (all accounts)", + "/d — Single-account details (e.g. /d T4-A)", + "/help — Help", + ].join("\n"); + await sendTgMessage({ ...tgConfig, chatId: String(fromChatId) }, text); + return; + } + if (cmd === "/status" || cmd === "/s") { + const r = await buildStatusText(1); + await sendTgMessage({ ...tgConfig, chatId: String(fromChatId) }, r.text, { + inlineKeyboard: buildStatusKeyboard(r.page, r.totalPages), + }); + return; + } + if (cmd === "/d" || cmd === "/detail") { + if (!args) { + await sendTgMessage({ ...tgConfig, chatId: String(fromChatId) }, "Usage: /d \nExample: /d T4-A"); + return; + } + const text = await buildDetailText(args); + await sendTgMessage({ ...tgConfig, chatId: String(fromChatId) }, text); + return; + } + // Unknown commands are silently ignored +} + +async function handleTgCallback(ctx: CallbackContext): Promise { + // Security: only respond to the authorized chat_id + if (tgConfig.chatId && String(ctx.fromChatId) !== tgConfig.chatId) { + await answerCallbackQuery(tgConfig, ctx.callbackQueryId, "Unauthorized"); + return; + } + // Compatible with the old refresh_status, plus the new formats status_page:N / refresh:N + // Refresh (refresh:N): delete+send, triggers the animation, visibly signals a successful refresh + // Page change (status_page:N): edit in place + top toast, doesn't jump to the bottom of the chat + if (ctx.messageId && (ctx.data === "refresh_status" || ctx.data.startsWith("refresh:") || ctx.data.startsWith("status_page:"))) { + const isRefresh = ctx.data === "refresh_status" || ctx.data.startsWith("refresh:"); + let page = 1; + if (ctx.data.startsWith("refresh:")) { + page = parseInt(ctx.data.slice("refresh:".length), 10) || 1; + } else if (ctx.data.startsWith("status_page:")) { + page = parseInt(ctx.data.slice("status_page:".length), 10) || 1; + } + const ackText = isRefresh ? "Refreshed" : `→ Page ${page}`; + await answerCallbackQuery(tgConfig, ctx.callbackQueryId, ackText); + const r = await buildStatusText(page); + const keyboard = buildStatusKeyboard(r.page, r.totalPages); + if (isRefresh) { + const delRes = await deleteTgMessage(tgConfig, String(ctx.fromChatId), ctx.messageId); + if (delRes.ok) { + await sendTgMessage(tgConfig, r.text, { toChatId: String(ctx.fromChatId), inlineKeyboard: keyboard }); + } else { + await editTgMessage(tgConfig, String(ctx.fromChatId), ctx.messageId, r.text, { inlineKeyboard: keyboard }); + } + } else { + await editTgMessage(tgConfig, String(ctx.fromChatId), ctx.messageId, r.text, { inlineKeyboard: keyboard }); + } + return; + } + if (ctx.data === "noop") { + await answerCallbackQuery(tgConfig, ctx.callbackQueryId); + return; + } + await answerCallbackQuery(tgConfig, ctx.callbackQueryId); +} + +const tgPoller = new TgPoller(() => tgConfig, handleTgCommand, handleTgCallback); +tgPoller.start(); + +// ── HTTP server ── +const server = createServer(async (req, res) => { + const url = new URL(req.url || "/", `http://localhost:${PORT}`); + const path = url.pathname; + + res.setHeader("Access-Control-Allow-Origin", "*"); + res.setHeader("Access-Control-Allow-Headers", "*"); + + // ── /api/accounts ── + if (path === "/api/accounts") { + if (req.method === "GET") { + // The management panel needs to see disabled accounts; add ?all=1 to return everything; by default only enabled accounts are returned + const includeDisabled = url.searchParams.get("all") === "1"; + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(loadAccounts(includeDisabled))); + return; + } + if (req.method === "PUT") { + try { + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + const body = JSON.parse(Buffer.concat(chunks).toString("utf-8")); + if (!Array.isArray(body)) throw new Error("body must be an array"); + const seen = new Set(); + const cleaned: Account[] = []; + for (const a of body) { + if (!a || typeof a.name !== "string" || typeof a.port !== "number") { + throw new Error("each item must contain name(string) and port(number)"); + } + const name = a.name.trim(); + const port = Math.floor(a.port); + if (!name) throw new Error("name cannot be empty"); + if (port < 1 || port > 65535) throw new Error(`port out of range: ${port}`); + if (seen.has(port)) throw new Error(`duplicate port: ${port}`); + seen.add(port); + const item: Account = { name, port }; + if (a.enabled === false) item.enabled = false; // Only write the field when explicitly disabled, keep enabled items clean + cleaned.push(item); + } + writeFileSync(ACCOUNTS_FILE, JSON.stringify(cleaned, null, 2) + "\n", "utf-8"); + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(cleaned)); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ error: "Save failed", detail: msg })); + } + return; + } + res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" }); + res.end("Method Not Allowed"); + return; + } + + // ── /api/proxy ── + if (path === "/api/proxy") { + const port = Number(url.searchParams.get("port")); + if (!port || port < 1 || port > 65535) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ error: "invalid port" })); + return; + } + const result = await fetchAccountState(port); + if (result.ok) { + res.writeHead(200, { + "Content-Type": "application/json; charset=utf-8", + ...(result.stale ? { "X-State-Stale": "1", "X-State-Age-Ms": String(result.staleAgeMs || 0) } : {}), + }); + res.end(JSON.stringify(result.state)); + } else { + res.writeHead(503, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ error: "Upstream unreachable", detail: result.error })); + } + return; + } + + // ── /api/tg/config GET / POST ── + if (path === "/api/tg/config") { + if (req.method === "GET") { + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(tgConfigPublic())); + return; + } + if (req.method === "POST") { + try { + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + const body = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as Record; + if (typeof body.enabled === "boolean") tgConfig.enabled = body.enabled; + // botToken containing *** means the frontend didn't modify it (mask echoed back), skip + if (typeof body.botToken === "string" && !body.botToken.includes("***")) { + const oldToken = tgConfig.botToken; + tgConfig.botToken = body.botToken.trim(); + if (oldToken !== tgConfig.botToken) tgPoller.resetOnTokenChange(); + } + if (typeof body.chatId === "string") tgConfig.chatId = body.chatId.trim(); + saveTgConfig(tgConfig); + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(tgConfigPublic())); + } catch (e) { + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ error: "Save failed", detail: e instanceof Error ? e.message : String(e) })); + } + return; + } + } + + // ── /api/tg/auto-chat: fetch chat_id from getUpdates ── + if (path === "/api/tg/auto-chat" && req.method === "POST") { + try { + const chunks: Buffer[] = []; + for await (const c of req) chunks.push(c as Buffer); + const body = JSON.parse(Buffer.concat(chunks).toString("utf-8")) as Record; + // Prefer the token in the body (the frontend may have typed it and clicked fetch before saving) + let token = typeof body.botToken === "string" && !body.botToken.includes("***") ? body.botToken.trim() : ""; + if (!token) token = tgConfig.botToken; + if (!token) { + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: false, error: "Bot Token not configured" })); + return; + } + const r = await autoDetectChatId(token); + res.writeHead(r.ok ? 200 : 400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(r)); + } catch (e) { + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: false, error: e instanceof Error ? e.message : String(e) })); + } + return; + } + + // ── /api/balance-history: return balance snapshots for all dates ── + if (path === "/api/balance-history" && req.method === "GET") { + const history = loadBalanceHistory(); + // Wrap once so the frontend can sort by date + const items = Object.entries(history) + .map(([date, snap]) => ({ date, ...snap })) + .sort((a, b) => a.date.localeCompare(b.date)); + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(items)); + return; + } + + // ── /api/balance-history/now: trigger a snapshot immediately (for debugging/manual backfill) ── + if (path === "/api/balance-history/now" && req.method === "POST") { + void recordBalanceSnapshot("manual trigger"); + res.writeHead(202, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: true, message: "Snapshot triggered, query /api/balance-history in 30 seconds" })); + return; + } + + // ── /api/balance-history?date=YYYY-MM-DD: delete the snapshot for the given date ── + if (path === "/api/balance-history" && req.method === "DELETE") { + const date = url.searchParams.get("date") || ""; + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: false, error: "date parameter missing or malformed (need YYYY-MM-DD)" })); + return; + } + const history = loadBalanceHistory(); + if (!(date in history)) { + res.writeHead(404, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: false, error: "No snapshot record for that date" })); + return; + } + delete history[date]; + saveBalanceHistory(history); + console.log(`[monitor] Deleted balance snapshot: ${date}`); + res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: true, date })); + return; + } + + // ── /api/tg/test: push an actual overview immediately (same as /status, with refresh button) ── + if (path === "/api/tg/test" && req.method === "POST") { + if (!tgConfig.botToken || !tgConfig.chatId) { + res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify({ ok: false, error: "Bot Token or Chat ID not configured" })); + return; + } + const status = await buildStatusText(1); + const r = await sendTgMessage(tgConfig, status.text, { + inlineKeyboard: buildStatusKeyboard(status.page, status.totalPages), + }); + res.writeHead(r.ok ? 200 : 400, { "Content-Type": "application/json; charset=utf-8" }); + res.end(JSON.stringify(r)); + return; + } + + // ── Static files ── + if (path === "/" || path === "/monitor.html") { + if (existsSync(HTML_FILE)) { + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(readFileSync(HTML_FILE)); + } else { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.end("monitor.html does not exist"); + } + return; + } + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); +}); + +server.listen(PORT, () => { + const accounts = loadAccounts(); + console.log(`✓ Monitor server started`); + console.log(` URL: http://localhost:${PORT}`); + console.log(` Accounts: ${accounts.length}`); + if (accounts.length > 0) { + accounts.forEach(a => console.log(` - ${a.name} (port=${a.port})`)); + } else { + console.log(` ⚠ accounts.json is empty or missing, please edit ${ACCOUNTS_FILE}`); + } + if (tgConfig.enabled && tgConfig.botToken) { + console.log(`✓ TG bot started (chatId=${tgConfig.chatId || "not configured"})`); + } else { + console.log(` TG bot not enabled (configure it in the top-right of the monitor page)`); + } + // Balance history collection: backfill today on startup + schedule tomorrow's auto snapshot at 0:05 + void bootstrapBalanceSnapshot(); + scheduleDailyBalanceSnapshot(); +}); + +process.on("SIGINT", () => { + console.log("\n[monitor] Received SIGINT, exiting"); + tgPoller.stop(); + server.close(); + process.exit(0); +}); diff --git a/monitor/monitor.html b/monitor/monitor.html new file mode 100644 index 0000000..a2d184c --- /dev/null +++ b/monitor/monitor.html @@ -0,0 +1,1691 @@ + + + + + +Polymarket Multi-Account Monitor + + + + +
+

📊 Polymarket Multi-Account Monitor

+
+ Machines + Online + Offline + Total Balance + Today's PnL + Total Trades + Total Win Rate + Next Refresh5s + + + + + +
+
+ +
Loading account list...
+
+ + + + + + + + +
+ + + + + + + + diff --git a/monitor/start.sh b/monitor/start.sh new file mode 100755 index 0000000..c4b6bd1 --- /dev/null +++ b/monitor/start.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Start the Polymarket multi-account monitor page +# Default port 8080; edit the MONITOR_PORT environment variable to change it +cd "$(dirname "$0")" +exec npx tsx ./monitor-server.ts diff --git a/monitor/tg-push.ts b/monitor/tg-push.ts new file mode 100644 index 0000000..7d1f2cb --- /dev/null +++ b/monitor/tg-push.ts @@ -0,0 +1,303 @@ +/** + * Monitor Telegram module (independent of the main project's tg-push) + * + * Features: + * - Config persisted to monitor/.tg-config.json + * - Auto-fetch chat_id (only available after the user has messaged the bot) + * - Long-poll getUpdates to receive and respond to commands + * - Commands: /status overview, /d single account, /help + */ + +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TG_CONFIG_FILE = resolve(__dirname, ".tg-config.json"); + +export interface TgConfig { + enabled: boolean; + botToken: string; + chatId: string; +} + +const DEFAULT_CONFIG: TgConfig = { + enabled: false, + botToken: "", + chatId: "", +}; + +export function loadTgConfig(): TgConfig { + try { + if (!existsSync(TG_CONFIG_FILE)) return { ...DEFAULT_CONFIG }; + const raw = JSON.parse(readFileSync(TG_CONFIG_FILE, "utf-8")); + return { + enabled: !!raw.enabled, + botToken: typeof raw.botToken === "string" ? raw.botToken : "", + chatId: typeof raw.chatId === "string" ? raw.chatId : "", + }; + } catch (err) { + console.warn(`[monitor.TG] Failed to load config: ${err instanceof Error ? err.message : String(err)}`); + return { ...DEFAULT_CONFIG }; + } +} + +export function saveTgConfig(cfg: TgConfig): void { + try { + writeFileSync(TG_CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn(`[monitor.TG] Failed to save config: ${err instanceof Error ? err.message : String(err)}`); + } +} + +export function maskToken(token: string): string { + if (!token) return ""; + if (token.length < 14) return token.slice(0, 4) + "***"; + return token.slice(0, 10) + "***" + token.slice(-4); +} + +/** Auto-fetch chat_id: the user must have sent the bot any message (e.g. /start) before updates exist */ +export async function autoDetectChatId(botToken: string): Promise<{ ok: boolean; chatId?: string; error?: string }> { + if (!botToken) return { ok: false, error: "Bot Token is empty" }; + const url = `https://api.telegram.org/bot${botToken}/getUpdates`; + try { + const res = await fetch(url); + const data = await res.json() as { ok: boolean; result?: Array<{ message?: { chat?: { id?: number } } }>; description?: string }; + if (!data.ok) return { ok: false, error: data.description || `HTTP ${res.status}` }; + const results = data.result || []; + if (results.length === 0) return { ok: false, error: "No message found. Send the bot a message in Telegram first (e.g. /start), then try again" }; + for (let i = results.length - 1; i >= 0; i--) { + const id = results[i].message?.chat?.id; + if (typeof id === "number") return { ok: true, chatId: String(id) }; + } + return { ok: false, error: "chat.id not found in the message" }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +export interface InlineButton { text: string; callback_data: string; } +export interface SendOpts { + parseMode?: "Markdown" | "MarkdownV2" | "HTML"; + inlineKeyboard?: InlineButton[][]; + /** Specify a chatId to override cfg.chatId (used when responding to someone else's command) */ + toChatId?: string; +} + +/** Send a message */ +export async function sendTgMessage(cfg: TgConfig, text: string, opts: SendOpts = {}): Promise<{ ok: boolean; error?: string; messageId?: number }> { + const targetChat = opts.toChatId || cfg.chatId; + if (!cfg.botToken || !targetChat) return { ok: false, error: "Bot Token or Chat ID not configured" }; + const url = `https://api.telegram.org/bot${cfg.botToken}/sendMessage`; + try { + const body: Record = { + chat_id: targetChat, + text, + disable_web_page_preview: true, + }; + if (opts.parseMode) body.parse_mode = opts.parseMode; + if (opts.inlineKeyboard) body.reply_markup = { inline_keyboard: opts.inlineKeyboard }; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const data = await res.json() as { ok: boolean; description?: string; result?: { message_id?: number } }; + if (!data.ok) return { ok: false, error: data.description || `HTTP ${res.status}` }; + return { ok: true, messageId: data.result?.message_id }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +/** Delete a message (used by the "🔄 Refresh" button: delete the old one then send a new one, triggering TG's animation to signal a successful refresh) */ +export async function deleteTgMessage(cfg: TgConfig, chatId: string, messageId: number): Promise<{ ok: boolean; error?: string }> { + const url = `https://api.telegram.org/bot${cfg.botToken}/deleteMessage`; + try { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ chat_id: chatId, message_id: messageId }), + }); + const data = await res.json() as { ok: boolean; description?: string }; + if (!data.ok) return { ok: false, error: data.description || `HTTP ${res.status}` }; + return { ok: true }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +/** Edit an existing message (kept as a fallback; currently refresh uses the delete+send path) */ +export async function editTgMessage(cfg: TgConfig, chatId: string, messageId: number, text: string, opts: { inlineKeyboard?: InlineButton[][]; parseMode?: "Markdown" | "MarkdownV2" | "HTML" } = {}): Promise<{ ok: boolean; error?: string }> { + const url = `https://api.telegram.org/bot${cfg.botToken}/editMessageText`; + try { + const body: Record = { + chat_id: chatId, + message_id: messageId, + text, + disable_web_page_preview: true, + }; + if (opts.parseMode) body.parse_mode = opts.parseMode; + if (opts.inlineKeyboard) body.reply_markup = { inline_keyboard: opts.inlineKeyboard }; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + const data = await res.json() as { ok: boolean; description?: string }; + // "message is not modified" is not an error (TG rejects the edit when content is identical, but semantically it succeeded) + if (!data.ok) { + const desc = data.description || ""; + if (desc.includes("message is not modified")) return { ok: true }; + return { ok: false, error: desc || `HTTP ${res.status}` }; + } + return { ok: true }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +/** Answer a callback_query (must ack after a button press, otherwise the TG client keeps spinning) */ +export async function answerCallbackQuery(cfg: TgConfig, callbackQueryId: string, text?: string): Promise { + if (!cfg.botToken) return; + const url = `https://api.telegram.org/bot${cfg.botToken}/answerCallbackQuery`; + try { + await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ callback_query_id: callbackQueryId, text }), + }); + } catch { + // Ignore ack errors + } +} + +// ── Long polling ── +export interface TgUpdate { + update_id: number; + message?: { + message_id: number; + chat: { id: number; type: string }; + from?: { id: number; username?: string }; + text?: string; + date: number; + }; + callback_query?: { + id: string; + from: { id: number; username?: string }; + message?: { message_id: number; chat: { id: number; type: string } }; + data?: string; + }; +} + +export interface CallbackContext { + callbackQueryId: string; + fromChatId: number; + messageId?: number; + data: string; +} + +/** Long-poll getUpdates */ +export class TgPoller { + private offset = 0; + private stopped = false; + private timer: NodeJS.Timeout | null = null; + private currentToken = ""; + + constructor( + private readonly getCfg: () => TgConfig, + private readonly onCommand: (cmd: string, args: string, fromChatId: number) => Promise | void, + private readonly onCallback?: (ctx: CallbackContext) => Promise | void, + ) {} + + start(): void { + this.stopped = false; + void this.loop(); + } + + stop(): void { + this.stopped = true; + if (this.timer) { clearTimeout(this.timer); this.timer = null; } + } + + /** Called when the token changes - resets offset to avoid getting stuck on 401 */ + resetOnTokenChange(): void { + this.offset = 0; + } + + private async loop(): Promise { + while (!this.stopped) { + const cfg = this.getCfg(); + // Idle when config is invalid, check every 2s + if (!cfg.enabled || !cfg.botToken) { + if (this.currentToken !== cfg.botToken) this.offset = 0; + this.currentToken = cfg.botToken; + await this.sleep(2000); + continue; + } + // token changed → reset offset + if (this.currentToken !== cfg.botToken) { + this.offset = 0; + this.currentToken = cfg.botToken; + } + try { + const url = `https://api.telegram.org/bot${cfg.botToken}/getUpdates?timeout=25&offset=${this.offset}`; + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 30_000); + const res = await fetch(url, { signal: ctrl.signal }); + clearTimeout(timer); + const data = await res.json() as { ok: boolean; result?: TgUpdate[]; description?: string }; + if (!data.ok) { + console.warn(`[monitor.TG] getUpdates failed: ${data.description || res.status}`); + await this.sleep(5000); + continue; + } + for (const update of data.result || []) { + this.offset = Math.max(this.offset, update.update_id + 1); + // callback_query: button click + if (update.callback_query && this.onCallback) { + const cq = update.callback_query; + try { + await this.onCallback({ + callbackQueryId: cq.id, + fromChatId: cq.from.id, + messageId: cq.message?.message_id, + data: cq.data || "", + }); + } catch (err) { + console.warn(`[monitor.TG] Error handling callback: ${err instanceof Error ? err.message : String(err)}`); + } + continue; + } + const msg = update.message; + if (!msg || typeof msg.text !== "string") continue; + const text = msg.text.trim(); + if (!text.startsWith("/")) continue; + const space = text.indexOf(" "); + // Command may carry an @bot suffix (in groups), strip it + let cmd = (space === -1 ? text : text.slice(0, space)).toLowerCase(); + const at = cmd.indexOf("@"); + if (at !== -1) cmd = cmd.slice(0, at); + const args = space === -1 ? "" : text.slice(space + 1).trim(); + try { + await this.onCommand(cmd, args, msg.chat.id); + } catch (err) { + console.warn(`[monitor.TG] Error handling command: ${err instanceof Error ? err.message : String(err)}`); + } + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (!this.stopped) { + console.warn(`[monitor.TG] Long-polling error: ${msg}`); + await this.sleep(5000); + } + } + } + } + + private sleep(ms: number): Promise { + return new Promise(resolve => { + this.timer = setTimeout(() => { this.timer = null; resolve(); }, ms); + }); + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0632672 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2799 @@ +{ + "name": "btc5m-web", + "version": "5.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "btc5m-web", + "version": "5.0.0", + "dependencies": { + "@polymarket/clob-client-v2": "^1.0.3", + "dotenv": "^16.0.0", + "ethers": "^6.0.0", + "express": "^5.2.1", + "undici": "^6.25.0", + "ws": "^8.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.0.0", + "@types/ws": "^8.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/basex": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/basex/-/basex-5.8.0.tgz", + "integrity": "sha512-PIgTszMlDRmNwW9nhS6iqtVfdTAKosA7llYXNmGPw4YAI1PUyMv28988wAb41/gHF/WqGdoLv0erHaRcHRKW2Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/hdnode": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hdnode/-/hdnode-5.8.0.tgz", + "integrity": "sha512-4bK1VF6E83/3/Im0ERnnUeWOY3P1BZml4ZD3wcH8Ys0/d1h1xaFt6Zc+Dh9zXf9TapGro0T4wvO71UTCp3/uoA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/json-wallets": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/json-wallets/-/json-wallets-5.8.0.tgz", + "integrity": "sha512-HxblNck8FVUtNxS3VTEYJAcwiKYsBIF77W15HufqlBF9gGfhmYOJtYZp8fSDZtn9y5EaXTE87zDwzxRoTFk11w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/pbkdf2": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "aes-js": "3.0.0", + "scrypt-js": "3.0.1" + } + }, + "node_modules/@ethersproject/json-wallets/node_modules/aes-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz", + "integrity": "sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==", + "license": "MIT" + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/pbkdf2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/pbkdf2/-/pbkdf2-5.8.0.tgz", + "integrity": "sha512-wuHiv97BrzCmfEaPbUFpMjlVg/IDkZThp9Ri88BpjRleg4iePJaj2SW8AIyE8cXn5V1tuAaMj6lzvsGJkGWskg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/sha2": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/providers": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/providers/-/providers-5.8.0.tgz", + "integrity": "sha512-3Il3oTzEx3o6kzcg9ZzbE+oCZYyY+3Zh83sKkn4s1DZfTUjIegHnN2Cm0kbn9YFy45FDVcuCLLONhU7ny0SsCw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/basex": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/sha2": "^5.8.0", + "@ethersproject/strings": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0", + "bech32": "1.1.4", + "ws": "8.18.0" + } + }, + "node_modules/@ethersproject/providers/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@ethersproject/random": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/random/-/random-5.8.0.tgz", + "integrity": "sha512-E4I5TDl7SVqyg4/kkA/qTfuLWAQGXmSOgYyO01So8hLfwgKvYK5snIlzxJMk72IFdG/7oh8yuSqY2KX7MMwg+A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/sha2": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/sha2/-/sha2-5.8.0.tgz", + "integrity": "sha512-dDOUrXr9wF/YFltgTBYS0tKslPEKr6AekjqDW2dbn1L1xmjGR+9GiKu4ajxovnrDbwxAKdHjW8jNcwfz8PAz4A==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/wallet": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wallet/-/wallet-5.8.0.tgz", + "integrity": "sha512-G+jnzmgg6UxurVKRKvw27h0kvG75YKXZKdlLYmAHeF32TGUzHkOFd7Zn6QHOTYRFWnfjtSSFjBowKo7vfrXzPA==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/hdnode": "^5.8.0", + "@ethersproject/json-wallets": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/random": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/wordlists": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/wordlists": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/wordlists/-/wordlists-5.8.0.tgz", + "integrity": "sha512-2df9bbXicZws2Sb5S6ET493uJ0Z84Fjr3pC4tu/qlnZERibZCeUVuqdtt+7Tv9xxhUxHoIekIA7avrKUWHrezg==", + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/hash": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@polymarket/clob-client-v2": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@polymarket/clob-client-v2/-/clob-client-v2-1.0.3.tgz", + "integrity": "sha512-dA2q/IoDh2u0CdEBRSvmbdC/+e+oxbRz1KeuydL3Yv3oGSudG0PAyyK7n/I80n/RV+5ZwreUKjb0pYT1aadVtg==", + "license": "MIT", + "dependencies": { + "@ethersproject/providers": "^5.8.0", + "@ethersproject/wallet": "^5.8.0", + "axios": "^1.0.0", + "browser-or-node": "^3.0.0", + "tslib": "^2.8.1", + "viem": "^2.46.3" + } + }, + "node_modules/@polymarket/clob-client-v2/node_modules/browser-or-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-3.0.0.tgz", + "integrity": "sha512-iczIdVJzGEYhP5DqQxYM9Hh7Ztpqqi+CXZpSmX8ALFs9ecXkQIeqRyM6TfxEfMVpwhl3dSuDvxdzzo9sUOIVBQ==", + "license": "MIT" + }, + "node_modules/@polymarket/clob-client-v2/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abitype": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.3.tgz", + "integrity": "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "license": "MIT" + }, + "node_modules/bn.js": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ethers": { + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", + "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT" + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/ox": { + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.7.tgz", + "integrity": "sha512-zSQ/cfBdolj7U4++NAvH7sI+VG0T3pEohITCgcQj8KlawvTDY4vGVhDT64Atsm0d6adWfIYHDpu88iUBMMp+AQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.2.3", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/ox/node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", + "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scrypt-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", + "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz", + "integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/viem": { + "version": "2.47.6", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.47.6.tgz", + "integrity": "sha512-zExmbI99NGvMdYa7fmqSTLgkwh48dmhgEqFrUgkpL4kfG4XkVefZ8dZqIKVUhZo6Uhf0FrrEXOsHm9LUyIvI2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.2.3", + "isows": "1.0.7", + "ox": "0.14.7", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/viem/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2de44f7 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "btc5m-web", + "version": "5.0.0", + "description": "BTC 5-minute up/down order book monitor - standalone deployment edition", + "type": "module", + "scripts": { + "start": "npx tsx ./server.ts", + "collect": "npx tsx ./data-collector.ts" + }, + "dependencies": { + "@polymarket/clob-client-v2": "^1.0.3", + "dotenv": "^16.0.0", + "ethers": "^6.0.0", + "express": "^5.2.1", + "undici": "^6.25.0", + "ws": "^8.0.0" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.0.0", + "@types/ws": "^8.0.0", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + } +} diff --git a/polymarket-pnl.ts b/polymarket-pnl.ts new file mode 100644 index 0000000..9fd85a1 --- /dev/null +++ b/polymarket-pnl.ts @@ -0,0 +1,654 @@ +/** + * Polymarket real PnL module + * + * Data sources: + * - /activity?type=TRADE all CLOB buy/sell fills (both maker / taker, covers cases like t8 limit orders being taken) + * - /activity?type=REDEEM Claim credited + * - /positions current unprocessed positions (not sold / not claimed) + * + * Note: earlier we used the /trades endpoint, but it only returns fills from the taker's perspective, + * so limit orders like t8 that get taken as maker would be missing; therefore we switched to /activity?type=TRADE. + * + * Design: + * - Full load on startup (paginate to the end) + * - Incremental sync filtered by lastSyncTs, only pulling new data + * - A full refresh every 5 minutes as a fallback + * - Pair by (conditionId, outcome) to compute the full PnL of each position + */ + +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// ── Constants ─────────────────────────────────────────────── +const API_BASE = "https://data-api.polymarket.com"; +const API_HEADERS = { "User-Agent": "Mozilla/5.0" }; +const PAGE_SIZE = 100; +const REQUEST_TIMEOUT_MS = 15000; +// Polymarket official fee formula: fee = C × feeRate × p × (1 - p) +// For crypto markets (BTC 5m etc.) feeRate = 7.2% (max fee $1.80 / 100 shares @ p=0.5) +// Reference: https://docs.polymarket.com/trading/fees +const CRYPTO_FEE_RATE = 0.072; + +// ── Types ─────────────────────────────────────────────────── +export interface PmTrade { + proxyWallet: string; + side: "BUY" | "SELL"; + asset: string; + conditionId: string; + size: number; + price: number; + timestamp: number; // Unix seconds + outcome: string; // "Up" / "Down" + outcomeIndex: number; + title: string; + slug: string; + eventSlug: string; + transactionHash: string; +} + +export interface PmRedeem { + proxyWallet: string; + conditionId: string; + timestamp: number; // Unix seconds + size: number; + usdcSize: number; // Claim credited amount + transactionHash: string; + title: string; + slug: string; + eventSlug: string; +} + +export interface PmPosition { + proxyWallet: string; + conditionId: string; + asset: string; + size: number; + avgPrice: number; + initialValue: number; + currentValue: number; + cashPnl: number; + realizedPnl: number; + redeemable: boolean; + outcome: string; + outcomeIndex: number; + title: string; + endDate: string; +} + +export interface PositionSummary { + conditionId: string; + outcome: string; + outcomeIndex: number; + title: string; + slug: string; + windowStart: number; // parsed from slug + firstTs: number; // first trade time (seconds) + lastTs: number; // last trade time (seconds) + buys: PmTrade[]; + sells: PmTrade[]; + redeems: PmRedeem[]; + buyCost: number; // total buy spend (excluding fee) + sellRevenue: number; // total sell revenue (excluding fee) + redeemRevenue: number; // total Claim payback + totalFee: number; // total fee + netPnl: number; // real net PnL = sell + Claim - buy - fee + status: "claimed" | "sold" | "pending" | "settled_lost"; + strategySource?: string; // source from local .strategy-sources.json + // Extra info for unsettled positions (from /positions) + currentValue?: number; + currentRedeemable?: boolean; +} + +/** A flattened single row (one per BUY/SELL/REDEEM/LOST) */ +export interface PnlEvent { + ts: number; // seconds + kind: "BUY" | "SELL" | "REDEEM" | "LOST"; // LOST = settled to zero (virtual event) + outcome: string; // Up / Down + outcomeIndex: number; + conditionId: string; + title: string; + slug: string; // market slug (e.g. "btc-updown-5m-1777139100"), used by frontend to filter by market + size: number; + price: number; // = 1 for REDEEM (payout at 1 USDC/share) + cost: number; // BUY=spend, SELL=revenue, REDEEM=credited + fee: number; // BUY/SELL fee, REDEEM=0 + netAmount: number; // net cash change (out=negative, in=positive, fee included) + transactionHash: string; + strategySource?: string; + positionPnl?: number; // position settlement PnL, attached only to the last exit row (SELL/REDEEM/LOST) + positionStatus?: "claimed" | "sold" | "pending" | "settled_lost"; + pending?: true; // locally pre-inserted, shows "pending calibration" before API data returns +} + +// ── Network utilities ──────────────────────────────────────── +async function fetchJson(url: string): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + try { + const res = await fetch(url, { headers: API_HEADERS, signal: controller.signal }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + return (await res.json()) as T; + } finally { + clearTimeout(timer); + } +} + +async function fetchPaged(path: string, extraQs: string = ""): Promise { + const items: T[] = []; + let offset = 0; + while (true) { + const qs = `limit=${PAGE_SIZE}&offset=${offset}${extraQs ? "&" + extraQs : ""}`; + const url = `${API_BASE}/${path}${path.includes("?") ? "&" : "?"}${qs}`; + const batch = await fetchJson(url); + if (!Array.isArray(batch) || batch.length === 0) break; + items.push(...batch); + if (batch.length < PAGE_SIZE) break; + offset += PAGE_SIZE; + } + return items; +} + +// ── API wrappers ───────────────────────────────────────────── +// Use /activity?type=TRADE instead of /trades: the former covers fills from the maker's perspective (e.g. t8 limit orders being taken), +// while the latter only returns taker fills and would miss records of being passively filled as the resting order side. +export async function fetchAllTrades(proxy: string): Promise { + return fetchPaged(`activity?user=${proxy}&type=TRADE`); +} + +export async function fetchAllRedeems(proxy: string): Promise { + // Polymarket returns many empty redeem records with size=0 (multi-direction split noise from the same tx), filter them out + const all = await fetchPaged(`activity?user=${proxy}&type=REDEEM`); + return all.filter(r => (r.usdcSize > 0) || (r.size > 0)); +} + +export async function fetchAllPositions(proxy: string): Promise { + return fetchPaged(`positions?user=${proxy}`); +} + +/** Incremental fetch: only data with timestamp > sinceSec */ +export async function fetchTradesSince(proxy: string, sinceSec: number): Promise { + // The Polymarket API returns in reverse chronological order. Fetch the first page; if the last item is still > sinceSec, continue to the next page + const collected: PmTrade[] = []; + let offset = 0; + while (true) { + const url = `${API_BASE}/activity?user=${proxy}&type=TRADE&limit=${PAGE_SIZE}&offset=${offset}`; + const batch = await fetchJson(url); + if (!Array.isArray(batch) || batch.length === 0) break; + const fresh = batch.filter(t => t.timestamp > sinceSec); + collected.push(...fresh); + if (fresh.length < batch.length) break; // old data appeared, stop paging + if (batch.length < PAGE_SIZE) break; + offset += PAGE_SIZE; + } + return collected; +} + +export async function fetchRedeemsSince(proxy: string, sinceSec: number): Promise { + const collected: PmRedeem[] = []; + let offset = 0; + while (true) { + const url = `${API_BASE}/activity?user=${proxy}&type=REDEEM&limit=${PAGE_SIZE}&offset=${offset}`; + const batch = await fetchJson(url); + if (!Array.isArray(batch) || batch.length === 0) break; + const fresh = batch.filter(r => r.timestamp > sinceSec); + collected.push(...fresh); + if (fresh.length < batch.length) break; + if (batch.length < PAGE_SIZE) break; + offset += PAGE_SIZE; + } + return collected.filter(r => (r.usdcSize > 0) || (r.size > 0)); +} + +// ── Fee formula ────────────────────────────────────────────── +// Official formula: fee = C × feeRate × p × (1 - p), symmetric for buy/sell +// Makers are not charged, only the taker pays; our FOK orders are all takers +function feeOf(_side: "BUY" | "SELL", size: number, price: number): number { + return size * CRYPTO_FEE_RATE * price * (1 - price); +} + +/** Return the Unix seconds of today 0:00 in CST (UTC+8) */ +function getCstDayStartSec(): number { + const offsetMs = 8 * 3600_000; + const cstMs = Date.now() + offsetMs; + const cstDay = new Date(cstMs); + cstDay.setUTCHours(0, 0, 0, 0); + return Math.floor(cstDay.getTime() / 1000) - 8 * 3600; +} + +// ── Position pairing ───────────────────────────────────────── +/** + * Group trades + redeems by (conditionId, outcome), computing the full PnL for each group + * + * Note: redeem events do not contain outcome info; they are attributed to this market via conditionId. + * If you bought both Up and Down under the same conditionId (rare), the redeem + * is attributed to every outcome that appeared (only one side can win, the other has usdcSize=0 and has no effect). + */ +export function summarizePositions( + trades: PmTrade[], + redeems: PmRedeem[], + positions: PmPosition[], + strategySources: Map, +): PositionSummary[] { + type Key = string; + const mk = (c: string, o: string): Key => `${c}::${o}`; + const groups = new Map(); + + // 1. First group all trades by (conditionId, outcome) + for (const t of trades) { + const k = mk(t.conditionId, t.outcome); + let g = groups.get(k); + if (!g) { + const ws = parseWindowStartFromSlug(t.slug); + g = { + conditionId: t.conditionId, + outcome: t.outcome, + outcomeIndex: t.outcomeIndex, + title: t.title, + slug: t.slug, + windowStart: ws, + firstTs: t.timestamp, + lastTs: t.timestamp, + buys: [], + sells: [], + redeems: [], + buyCost: 0, sellRevenue: 0, redeemRevenue: 0, totalFee: 0, netPnl: 0, + status: "pending", + }; + groups.set(k, g); + } + if (t.side === "BUY") g.buys.push(t); + else g.sells.push(t); + g.firstTs = Math.min(g.firstTs, t.timestamp); + g.lastTs = Math.max(g.lastTs, t.timestamp); + } + + // 2. Attribute redeems by conditionId (a single conditionId may have multiple outcome groups) + const redeemsByCond = new Map(); + for (const r of redeems) { + const arr = redeemsByCond.get(r.conditionId) ?? []; + arr.push(r); + redeemsByCond.set(r.conditionId, arr); + } + + // 3. Compute the PnL of each group + for (const g of groups.values()) { + const rs = redeemsByCond.get(g.conditionId) ?? []; + // All redeems of the same conditionId are attached here (the winning side) + g.redeems = rs; + if (rs.length) g.lastTs = Math.max(g.lastTs, ...rs.map(r => r.timestamp)); + + g.buyCost = g.buys.reduce((s, b) => s + b.size * b.price, 0); + g.sellRevenue = g.sells.reduce((s, x) => s + x.size * x.price, 0); + g.redeemRevenue = rs.reduce((s, r) => s + r.usdcSize, 0); + g.totalFee = + g.buys.reduce((s, b) => s + feeOf("BUY", b.size, b.price), 0) + + g.sells.reduce((s, x) => s + feeOf("SELL", x.size, x.price), 0); + + g.netPnl = g.sellRevenue + g.redeemRevenue - g.buyCost - g.totalFee; + + // Determine status + if (rs.length > 0) g.status = "claimed"; + else if (g.sells.length > 0) g.status = "sold"; + else g.status = "pending"; + + // Strategy source: look up by the txHash of the first buy + if (g.buys.length) { + const src = strategySources.get(g.buys[0].transactionHash.toLowerCase()); + if (src) g.strategySource = src; + } + } + + // 4. Unsettled position info: supplement from /positions + for (const p of positions) { + const k = mk(p.conditionId, p.outcome); + const g = groups.get(k); + if (!g) continue; + g.currentValue = p.currentValue; + g.currentRedeemable = p.redeemable; + // Settled but zeroed out: upgrade from pending to settled_lost + if (g.status === "pending" && p.redeemable && p.currentValue === 0) { + g.status = "settled_lost"; + // In this case cashPnl is -initialValue (position value goes to zero) + // Already reflected in g.netPnl (sell=0, redeem=0, buyCost - fee is the loss) + } + } + + // 5. Return sorted by most recent time descending + return [...groups.values()].sort((a, b) => b.lastTs - a.lastTs); +} + +function parseWindowStartFromSlug(slug: string): number { + // slug format "btc-updown-5m-1776762000" + const m = slug.match(/(\d{10,})$/); + return m ? parseInt(m[1], 10) : 0; +} + +// ── Strategy source mapping (local persistence) ────────────── +const STRATEGY_SOURCES_FILE = resolve(__dirname, ".strategy-sources.json"); + +export function loadStrategySources(): Map { + try { + if (!existsSync(STRATEGY_SOURCES_FILE)) return new Map(); + const data = JSON.parse(readFileSync(STRATEGY_SOURCES_FILE, "utf-8")); + if (typeof data !== "object" || data == null) return new Map(); + return new Map(Object.entries(data as Record).map(([k, v]) => [k.toLowerCase(), v])); + } catch (err) { + console.warn(`[PmPnl] Failed to load strategy-sources: ${err instanceof Error ? err.message : String(err)}`); + return new Map(); + } +} + +export function saveStrategySources(map: Map): void { + try { + const obj: Record = {}; + for (const [k, v] of map) obj[k] = v; + writeFileSync(STRATEGY_SOURCES_FILE, JSON.stringify(obj, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn(`[PmPnl] Failed to save strategy-sources: ${err instanceof Error ? err.message : String(err)}`); + } +} + +// ── Manager: state + sync ──────────────────────────────────── +export class PmPnlManager { + private trades: PmTrade[] = []; + private redeems: PmRedeem[] = []; + private positions: PmPosition[] = []; + private strategySources: Map = loadStrategySources(); + private initialized = false; + private refreshing = false; + private lastRefreshAt = 0; // timestamp of the most recent successful full fetch (ms) + + constructor(private proxy: string) {} + + /** Record the strategy source of a trade (txHash → source) */ + recordStrategySource(txHash: string, source: string): void { + if (!txHash) return; + this.strategySources.set(txHash.toLowerCase(), source); + saveStrategySources(this.strategySources); + } + + /** Startup load (fetch today's CST data, equivalent to fetchAll) */ + async init(): Promise { + if (this.initialized) return; + await this.fetchAll(); + this.initialized = true; + } + + /** + * Fetch today's (from CST 0:00) trades/redeems + current positions, overwriting the local cache + * + * Note: although the function is named fetchAll, it actually only fetches "today's" fills, not the full history. + * Design reason: refreshing every 5 minutes + checking cross-day data on the Polymarket website is enough, no need to cache all history on the backend. + */ + async fetchAll(): Promise { + if (!this.proxy || this.refreshing) return false; + this.refreshing = true; + try { + const sinceSec = getCstDayStartSec(); + const [tradesRes, redeemsRes, positionsRes] = await Promise.allSettled([ + fetchTradesSince(this.proxy, sinceSec), + fetchRedeemsSince(this.proxy, sinceSec), + fetchAllPositions(this.proxy), + ]); + if (tradesRes.status === "fulfilled") this.trades = tradesRes.value; + else console.warn(`[PmPnl] today's trades failed: ${tradesRes.reason?.message ?? tradesRes.reason}`); + if (redeemsRes.status === "fulfilled") this.redeems = redeemsRes.value; + else console.warn(`[PmPnl] today's redeems failed: ${redeemsRes.reason?.message ?? redeemsRes.reason}`); + if (positionsRes.status === "fulfilled") this.positions = positionsRes.value; + else console.warn(`[PmPnl] positions failed: ${positionsRes.reason?.message ?? positionsRes.reason}`); + this.lastRefreshAt = Date.now(); + console.log(`[PmPnl] refresh (today CST): trades ${this.trades.length} / redeems ${this.redeems.length} / positions ${this.positions.length}`); + return true; + } catch (err) { + console.warn(`[PmPnl] refresh exception: ${err instanceof Error ? err.message : String(err)}`); + return false; + } finally { + this.refreshing = false; + } + } + + getLastRefreshAt(): number { return this.lastRefreshAt; } + + /** Return a snapshot aggregated by position */ + getSummaries(limit?: number): PositionSummary[] { + const all = summarizePositions(this.trades, this.redeems, this.positions, this.strategySources); + return limit ? all.slice(0, limit) : all; + } + + /** Return flattened per-event rows, in reverse chronological order. Returns only the last 7 days by default. */ + getEvents(opts?: { limit?: number; sinceDays?: number }): PnlEvent[] { + const sinceDays = opts?.sinceDays ?? 7; + const limit = opts?.limit; + const nowSec = Math.floor(Date.now() / 1000); + const sinceSec = sinceDays > 0 ? nowSec - sinceDays * 86400 : 0; + const summaries = summarizePositions(this.trades, this.redeems, this.positions, this.strategySources); + // Split each position: BUY + SELL + REDEEM each become a row, with position info attached + const events: PnlEvent[] = []; + for (const s of summaries) { + for (const b of s.buys) { + const fee = feeOf("BUY", b.size, b.price); + const cost = b.size * b.price; + events.push({ + ts: b.timestamp, + kind: "BUY", + outcome: b.outcome, + outcomeIndex: b.outcomeIndex, + conditionId: b.conditionId, + title: b.title, + slug: s.slug, + size: b.size, + price: b.price, + cost, fee, + netAmount: -(cost + fee), + transactionHash: b.transactionHash, + strategySource: s.strategySource, + }); + } + // Position settlement PnL is attached only to the last exit row (the one with the largest ts among SELL/REDEEM) + // To avoid showing the same netPnl value repeatedly when a position has multiple exits + const lastExitTs = Math.max( + ...s.sells.map(x => x.timestamp), + ...s.redeems.map(r => r.timestamp), + -Infinity, + ); + let pnlAttached = false; // attach only once when multiple rows share the same ts + for (const x of s.sells) { + const fee = feeOf("SELL", x.size, x.price); + const revenue = x.size * x.price; + const isLastExit = !pnlAttached && x.timestamp === lastExitTs; + if (isLastExit) pnlAttached = true; + events.push({ + ts: x.timestamp, + kind: "SELL", + outcome: x.outcome, + outcomeIndex: x.outcomeIndex, + conditionId: x.conditionId, + title: x.title, + slug: s.slug, + size: x.size, + price: x.price, + cost: revenue, fee, + netAmount: revenue - fee, + transactionHash: x.transactionHash, + strategySource: s.strategySource, + ...(isLastExit ? { positionPnl: s.netPnl, positionStatus: s.status } : {}), + }); + } + for (const r of s.redeems) { + const isLastExit = !pnlAttached && r.timestamp === lastExitTs; + if (isLastExit) pnlAttached = true; + events.push({ + ts: r.timestamp, + kind: "REDEEM", + outcome: s.outcome, + outcomeIndex: s.outcomeIndex, + conditionId: r.conditionId, + title: r.title, + slug: s.slug, + size: r.size, + price: 1, + cost: r.usdcSize, fee: 0, + netAmount: r.usdcSize, + transactionHash: r.transactionHash, + strategySource: s.strategySource, + ...(isLastExit ? { positionPnl: s.netPnl, positionStatus: s.status } : {}), + }); + } + + // Virtual "settled to zero" event: BUY exists + no SELL + no REDEEM + the window's settlement time has passed + // windowStart is parsed from slug, settlement time = windowStart + 300 seconds + if (s.buys.length > 0 && s.sells.length === 0 && s.redeems.length === 0 && s.windowStart > 0) { + const settleTs = s.windowStart + 300; + if (nowSec >= settleTs) { + // Synthesize a LOST row + const totalSize = s.buys.reduce((sum, b) => sum + b.size, 0); + events.push({ + ts: settleTs, + kind: "LOST", + outcome: s.outcome, + outcomeIndex: s.outcomeIndex, + conditionId: s.conditionId, + title: s.title, + slug: s.slug, + size: totalSize, + price: 0, + cost: 0, fee: 0, + netAmount: 0, // zeroing out produces no cash flow (the money was already spent at buy time) + transactionHash: s.buys[0].transactionHash, + strategySource: s.strategySource, + positionPnl: s.netPnl, // real PnL of this position = -buy cost - fee + positionStatus: "settled_lost", + }); + } + } + } + const filtered = sinceSec > 0 ? events.filter(e => e.ts >= sinceSec) : events; + filtered.sort((a, b) => b.ts - a.ts); + return limit ? filtered.slice(0, limit) : filtered; + } + + /** + * Unified stats snapshot (frontend panel / monitor page / TG share the same definition) + * + * Rules: + * - One trade = one trading window (deduped by conditionId) that has had a BUY + * - Settled = the window has either a SELL/REDEEM, or satisfies "window settlement time has passed + no sell, no redeem" (fallback, to handle the case where PM /positions does not return small positions) + * - Win = the window's net PnL > 0 + * + * sinceSec=0 means all history; other values are Unix seconds, counting only positions with firstTs >= sinceSec + */ + computeSnapshot(sinceSec: number = 0): { + positions: number; // total count (number of windows, including unsettled) + closedPositions: number; // number of settled trades + wins: number; // number of settled trades with net PnL > 0 + netPnl: number; // net PnL (sum of settled positions + unsettled floating loss i.e. -buyCost-fee also counted, consistent with the frontend recalcTotal behavior) + totalFee: number; + buyCost: number; + sellRevenue: number; + redeemRevenue: number; + } { + const summaries = summarizePositions(this.trades, this.redeems, this.positions, this.strategySources); + const nowSec = Math.floor(Date.now() / 1000); + + // Dedupe by conditionId into "windows", merging stats of multiple outcomes within the same cond + interface WinRow { + conditionId: string; + firstTs: number; + hasBuy: boolean; + hasSettled: boolean; // sells/redeems/settlement time has passed + buyCost: number; + sellRevenue: number; + redeemRevenue: number; + totalFee: number; + } + const windows = new Map(); + + for (const s of summaries) { + // Skip those never bought (defensive) + if (s.buys.length === 0) continue; + const cond = s.conditionId; + let row = windows.get(cond); + if (!row) { + row = { + conditionId: cond, + firstTs: s.firstTs, + hasBuy: false, + hasSettled: false, + buyCost: 0, + sellRevenue: 0, + redeemRevenue: 0, + totalFee: 0, + }; + windows.set(cond, row); + } + row.hasBuy = true; + row.firstTs = Math.min(row.firstTs, s.firstTs); + row.buyCost += s.buyCost; + row.sellRevenue += s.sellRevenue; + row.redeemRevenue += s.redeemRevenue; + row.totalFee += s.totalFee; + + // Whether this outcome is settled + const outcomeSettled = + s.sells.length > 0 || + s.redeems.length > 0 || + s.status === "settled_lost" || + // Fallback: windowStart has passed + no sell, no redeem (PM /positions may not return small positions settled to zero) + (s.windowStart > 0 && nowSec >= s.windowStart + 300 && s.sells.length === 0 && s.redeems.length === 0); + if (outcomeSettled) row.hasSettled = true; + } + + // Apply the sinceSec filter + const filtered = sinceSec > 0 + ? [...windows.values()].filter(w => w.firstTs >= sinceSec) + : [...windows.values()]; + + let positions = 0, closedPositions = 0, wins = 0; + let netPnl = 0, totalFee = 0, buyCost = 0, sellRevenue = 0, redeemRevenue = 0; + for (const w of filtered) { + if (!w.hasBuy) continue; + positions++; + buyCost += w.buyCost; + sellRevenue += w.sellRevenue; + redeemRevenue += w.redeemRevenue; + totalFee += w.totalFee; + const winNet = w.sellRevenue + w.redeemRevenue - w.buyCost - w.totalFee; + netPnl += winNet; + if (w.hasSettled) { + closedPositions++; + if (winNet > 0) wins++; + } + } + return { positions, closedPositions, wins, netPnl, totalFee, buyCost, sellRevenue, redeemRevenue }; + } + + /** Total PnL (last 7 days only by default; pass sinceDays=0 for all) */ + getTotalPnl(sinceDays: number = 7): { totalBuy: number; totalSell: number; totalRedeem: number; totalFee: number; netPnl: number; positionCount: number } { + const sinceSec = sinceDays > 0 ? Math.floor(Date.now() / 1000) - sinceDays * 86400 : 0; + let totalBuy = 0, totalSell = 0, totalRedeem = 0, totalFee = 0; + let count = 0; + for (const t of this.trades) { + if (sinceSec > 0 && t.timestamp < sinceSec) continue; + if (t.side === "BUY") totalBuy += t.size * t.price; + else totalSell += t.size * t.price; + totalFee += feeOf(t.side, t.size, t.price); + count++; + } + for (const r of this.redeems) { + if (sinceSec > 0 && r.timestamp < sinceSec) continue; + totalRedeem += r.usdcSize; + } + return { + totalBuy, totalSell, totalRedeem, totalFee, + netPnl: totalSell + totalRedeem - totalBuy - totalFee, + positionCount: count, + }; + } + + isInitialized(): boolean { + return this.initialized; + } +} diff --git a/scripts/check-adapter.ts b/scripts/check-adapter.ts new file mode 100644 index 0000000..50c124b --- /dev/null +++ b/scripts/check-adapter.ts @@ -0,0 +1,54 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const RPC = 'https://polygon-bor-rpc.publicnode.com'; +const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!; +const ADAPTER = '0xADa100874d00e3331D00F2007a9c336a65009718'; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; +const PUSD = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB'; + +const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + +const ctfRO = new ethers.Contract(CTF, [ + 'function getCollectionId(bytes32, bytes32, uint256) view returns (bytes32)', + 'function getPositionId(address, bytes32) view returns (uint256)', + 'function balanceOf(address, uint256) view returns (uint256)', + 'function payoutDenominator(bytes32) view returns (uint256)', + 'function payoutNumerators(bytes32, uint256) view returns (uint256)', +], provider); + +async function main() { + console.log('Safe:', SAFE); + console.log('CtfCollateralAdapter:', ADAPTER); + + // Fetch all claimable (do not filter curPrice, see the full set) + const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`); + const arr: any[] = await r.json(); + console.log(`data-api reports: ${arr.length} candidates\n`); + + // For each conditionId, test both collateralToken types: CTF direct PUSD vs adapter + // But actually the V2 token uses adapter as the collateralToken to create the positionId, so: + // Old positions (V1, pre-migration) use PUSD as collateralToken + // New positions (V2 post-migration) use adapter as collateralToken + for (const p of arr.slice(0, 5)) { + console.log(`\n=== ${p.title} (cond=${p.conditionId.slice(0,12)}...) ===`); + const denom = await ctfRO.payoutDenominator(p.conditionId); + if (denom === 0n) { console.log('Not resolved'); continue; } + const num0 = await ctfRO.payoutNumerators(p.conditionId, 0); + const num1 = await ctfRO.payoutNumerators(p.conditionId, 1); + console.log(`payout: [${num0}, ${num1}] denom=${denom}`); + + // Compute positionId using PUSD as collateralToken + for (const collat of [PUSD, ADAPTER]) { + console.log(` collateral=${collat === PUSD ? 'PUSD' : 'ADAPTER'}`); + for (const idx of [1, 2]) { + const collId = await ctfRO.getCollectionId(ethers.ZeroHash, p.conditionId, idx); + const posId = await ctfRO.getPositionId(collat, collId); + const bal = await ctfRO.balanceOf(SAFE, posId); + console.log(` indexSet=${idx} balance=${ethers.formatUnits(bal, 6)}`); + } + } + } +} + +main().catch(e => console.error(e)); diff --git a/scripts/check-balance.ts b/scripts/check-balance.ts new file mode 100644 index 0000000..42605ef --- /dev/null +++ b/scripts/check-balance.ts @@ -0,0 +1,64 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const RPC = 'https://polygon-bor-rpc.publicnode.com'; +const SAFE = '0xeCbD41A018cAD2BdD3Fd560b40b472f6ff54c336'; +const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB'; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; +const CONDITION_ID = '0x104f27e82a923cf3832854ae080bd8838ad288ee719e5cb1ac140e3b982d8f3d'; + +const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + +const erc20 = new ethers.Contract(COLLATERAL, [ + 'function balanceOf(address) view returns (uint256)', + 'function decimals() view returns (uint8)', + 'function symbol() view returns (string)', +], provider); + +const ctfIface = new ethers.Interface([ + 'function getPositionId(address collateralToken, bytes32 collectionId) view returns (uint256)', + 'function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint256 indexSet) view returns (bytes32)', + 'function balanceOf(address account, uint256 id) view returns (uint256)', + 'function payoutDenominator(bytes32 conditionId) view returns (uint256)', + 'function payoutNumerators(bytes32 conditionId, uint256 index) view returns (uint256)', +]); +const ctf = new ethers.Contract(CTF, ctfIface, provider); + +async function main() { + const sym = await erc20.symbol(); + const dec = await erc20.decimals(); + const bal = await erc20.balanceOf(SAFE); + console.log(`Safe ${SAFE}`); + console.log(`${sym} balance: ${ethers.formatUnits(bal, dec)} (decimals=${dec})`); + + // Query the token balance on each outcome (real on-chain data) + for (const indexSet of [1, 2]) { + const collId = await ctf.getCollectionId(ethers.ZeroHash, CONDITION_ID, indexSet); + const posId = await ctf.getPositionId(COLLATERAL, collId); + const tokBal = await ctf.balanceOf(SAFE, posId); + const human = ethers.formatUnits(tokBal, 6); + console.log(`indexSet=${indexSet} (${indexSet === 1 ? 'No' : 'Yes/Down?'}) tokenBalance=${human}`); + } + + // payouts + const denom = await ctf.payoutDenominator(CONDITION_ID); + console.log(`payoutDenominator: ${denom}`); + for (let i = 0; i < 2; i++) { + const num = await ctf.payoutNumerators(CONDITION_ID, i); + console.log(`payoutNumerators[${i}]: ${num} (${num > 0n ? 'win' : 'lose'})`); + } + + // Check whether data-api still lists it as redeemable + console.log('\n--- data-api positions ---'); + const r1 = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`); + const a1: any[] = await r1.json(); + console.log('redeemable=true returned:', a1.length, 'items'); + a1.forEach(p => console.log(` curPrice=${p.curPrice} size=${p.size} value=${p.currentValue} outcome=${p.outcome} title=${p.title}`)); + + console.log('\n--- Recent activity ---'); + const r2 = await fetch(`https://data-api.polymarket.com/activity?user=${SAFE}&limit=10`); + const a2: any[] = await r2.json(); + a2.forEach(a => console.log(` ${a.type} ${a.title || ''} size=${a.size || ''} ts=${a.timestamp}`)); +} + +main().catch(e => console.error(e)); diff --git a/scripts/check-claim-tx.ts b/scripts/check-claim-tx.ts new file mode 100644 index 0000000..abee079 --- /dev/null +++ b/scripts/check-claim-tx.ts @@ -0,0 +1,84 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const RPC = 'https://polygon-bor-rpc.publicnode.com'; +const TX = '0x098206326325e231fc5068c46fb7fabd5930b3789f0ed3a99cf2e0415a97a4ef'; +const SAFE = '0xeCbD41A018cAD2BdD3Fd560b40b472f6ff54c336'; +const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB'; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; +const CONDITION_ID = '0x104f27e82a923cf3832854ae080bd8838ad288ee719e5cb1ac140e3b982d8f3d'; + +const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + +const ctfIface = new ethers.Interface([ + 'event PayoutRedemption(address indexed redeemer, address indexed collateralToken, bytes32 indexed parentCollectionId, bytes32 conditionId, uint256[] indexSets, uint256 payout)', + 'function payoutNumerators(bytes32 conditionId, uint256 index) view returns (uint256)', + 'function payoutDenominator(bytes32 conditionId) view returns (uint256)', + 'function getOutcomeSlotCount(bytes32 conditionId) view returns (uint256)', + 'function getPositionId(address collateralToken, bytes32 collectionId) view returns (uint256)', + 'function getCollectionId(bytes32 parentCollectionId, bytes32 conditionId, uint256 indexSet) view returns (bytes32)', + 'function balanceOf(address account, uint256 id) view returns (uint256)', +]); +const safeIface = new ethers.Interface([ + 'event ExecutionSuccess(bytes32 txHash, uint256 payment)', + 'event ExecutionFailure(bytes32 txHash, uint256 payment)', +]); +const erc20Iface = new ethers.Interface([ + 'event Transfer(address indexed from, address indexed to, uint256 value)', +]); + +async function main() { + const receipt = await provider.getTransactionReceipt(TX); + if (!receipt) { console.error('Could not fetch receipt'); return; } + console.log('Block:', receipt.blockNumber, 'Status:', receipt.status, 'Logs count:', receipt.logs.length); + + // Parse all logs + for (const log of receipt.logs) { + console.log('\n---'); + console.log('addr:', log.address); + // Try to parse PayoutRedemption + try { + const parsed = ctfIface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed) { console.log('CTF event:', parsed.name, parsed.args); continue; } + } catch {} + try { + const parsed = safeIface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed) { console.log('Safe event:', parsed.name, parsed.args); continue; } + } catch {} + try { + const parsed = erc20Iface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed) { + console.log('ERC20 Transfer:', parsed.args.from, '→', parsed.args.to, ethers.formatUnits(parsed.args.value, 6), '(assuming 6 decimals)'); + continue; + } + } catch {} + console.log('Unknown event topics[0]:', log.topics[0]); + } + + // Query condition status on-chain + console.log('\n--- Condition status ---'); + const ctf = new ethers.Contract(CTF, ctfIface, provider); + const denom = await ctf.payoutDenominator(CONDITION_ID); + console.log('payoutDenominator:', denom.toString()); + if (denom > 0n) { + const slot = await ctf.getOutcomeSlotCount(CONDITION_ID); + console.log('outcomeSlotCount:', slot.toString()); + for (let i = 0; i < Number(slot); i++) { + const num = await ctf.payoutNumerators(CONDITION_ID, i); + console.log(` slot[${i}] payout:`, num.toString()); + } + } else { + console.log('⚠️ payoutDenominator=0 means the market has not resolved / has not reportPayouts yet'); + } + + // Query the Safe's previous token balances on both outcomes (after redeem they should theoretically both be 0) + console.log('\n--- Safe current token balances ---'); + for (const indexSet of [1, 2]) { + const collId = await ctf.getCollectionId(ethers.ZeroHash, CONDITION_ID, indexSet); + const posId = await ctf.getPositionId(COLLATERAL, collId); + const bal = await ctf.balanceOf(SAFE, posId); + console.log(`indexSet=${indexSet} positionId=${posId.toString().slice(0,20)}... balance=${bal}`); + } +} + +main().catch(e => console.error(e)); diff --git a/scripts/check-claimable-onchain.ts b/scripts/check-claimable-onchain.ts new file mode 100644 index 0000000..f07b49a --- /dev/null +++ b/scripts/check-claimable-onchain.ts @@ -0,0 +1,54 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const RPC = 'https://polygon-bor-rpc.publicnode.com'; +const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!; +const COLLATERAL = '0xC011a7E12a19f7B1f670d46F03B03f3342E82DFB'; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; + +const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); +const ctf = new ethers.Contract(CTF, [ + 'function payoutDenominator(bytes32) view returns (uint256)', + 'function payoutNumerators(bytes32, uint256) view returns (uint256)', + 'function getCollectionId(bytes32, bytes32, uint256) view returns (bytes32)', + 'function getPositionId(address, bytes32) view returns (uint256)', + 'function balanceOf(address, uint256) view returns (uint256)', +], provider); + +async function main() { + console.log('Safe:', SAFE); + + // data-api candidates + const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`); + const arr: any[] = await r.json(); + console.log(`data-api reports: ${arr.length} redeemable, of which curPrice=1: ${arr.filter(p=>p.curPrice===1).length}\n`); + + let realTotal = 0; + let fakeCount = 0; + for (const p of arr) { + const denom = await ctf.payoutDenominator(p.conditionId); + if (denom === 0n) { + console.log(`✗ ${p.title} | market not resolved`); + continue; + } + let realPayout = 0n; + for (const idx of [1, 2]) { + const collId = await ctf.getCollectionId(ethers.ZeroHash, p.conditionId, idx); + const posId = await ctf.getPositionId(COLLATERAL, collId); + const bal = await ctf.balanceOf(SAFE, posId); + const num = await ctf.payoutNumerators(p.conditionId, idx - 1); + realPayout += bal * num / denom; + } + const real = Number(ethers.formatUnits(realPayout, 6)); + if (real > 0) { + console.log(`✓ Claimable $${real.toFixed(4)} | ${p.title} | data-api reports $${p.currentValue.toFixed(4)}`); + realTotal += real; + } else { + console.log(`✗ Already claimed $0 | ${p.title} | data-api still reports $${p.currentValue.toFixed(4)} (index not refreshed)`); + fakeCount++; + } + } + console.log(`\n=== On-chain real claimable: $${realTotal.toFixed(4)} | data-api stale data: ${fakeCount} items ===`); +} + +main().catch(e => console.error(e)); diff --git a/scripts/check-real.ts b/scripts/check-real.ts new file mode 100644 index 0000000..b97dab8 --- /dev/null +++ b/scripts/check-real.ts @@ -0,0 +1,45 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; +const provider = new ethers.JsonRpcProvider('https://polygon-bor-rpc.publicnode.com', 137, { staticNetwork: true }); +const ctf = new ethers.Contract(CTF, [ + 'function balanceOfBatch(address[], uint256[]) view returns (uint256[])', +], provider); + +async function main() { + console.log('Safe:', SAFE); + const r = await fetch(`https://data-api.polymarket.com/positions?user=${SAFE}&sizeThreshold=.01&redeemable=true&limit=100`); + const arr: any[] = await r.json(); + + const allTokens = new Set(); + for (const p of arr) { + if (p.asset) allTokens.add(p.asset); + if (p.oppositeAsset) allTokens.add(p.oppositeAsset); + } + const ids = [...allTokens].map(s => BigInt(s)); + console.log(`Checking ${ids.length} tokenIds (from data-api asset/oppositeAsset fields)\n`); + + const accounts = ids.map(() => SAFE); + const balances: bigint[] = await ctf.balanceOfBatch(accounts, ids); + + let nonZero = 0; + let totalRedeemable = 0; + for (let i = 0; i < ids.length; i++) { + if (balances[i] > 0n) { + nonZero++; + const p = arr.find(x => BigInt(x.asset) === ids[i] || (x.oppositeAsset && BigInt(x.oppositeAsset) === ids[i])); + const isMain = p && BigInt(p.asset) === ids[i]; + const outcome = isMain ? p?.outcome : p?.oppositeOutcome; + const bal = Number(ethers.formatUnits(balances[i], 6)); + console.log(`✓ balance=${bal.toFixed(4)} | ${p?.title} | ${outcome} | conditionId=${p?.conditionId.slice(0,12)}...`); + // Is this outcome the winner? data-api does not give payout directly, infer from curPrice + if (isMain && p.curPrice === 1) totalRedeemable += bal; + if (!isMain && p.curPrice === 0) totalRedeemable += bal; // opposite is the winner + } + } + console.log(`\n=== Safe holds ${nonZero} non-zero tokens, total winning shares $${totalRedeemable.toFixed(4)} ===`); +} + +main().catch(e => console.error(e)); diff --git a/scripts/claim-once.ts b/scripts/claim-once.ts new file mode 100644 index 0000000..70189a0 --- /dev/null +++ b/scripts/claim-once.ts @@ -0,0 +1,110 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; +import { getContractConfig } from '@polymarket/clob-client-v2'; + +const RPC = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'; +const PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY!; +const PROXY = process.env.POLYMARKET_PROXY_ADDRESS!; +const DRY_RUN = process.argv.includes('--dry-run'); + +if (!PRIVATE_KEY || !PROXY) { console.error('Missing env'); process.exit(1); } + +async function main() { + const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + const contracts = getContractConfig(137); + const CTF = contracts.conditionalTokens; + const COLLATERAL = contracts.collateral; + // V2: redeem goes through CtfCollateralAdapter, which internally redeems CTF + auto-wraps pUSD + const ADAPTER = '0xADa100874d00e3331D00F2007a9c336a65009718'; + const ZERO_BYTES32 = '0x' + '0'.repeat(64); + + console.log('EOA:', wallet.address); + console.log('Safe:', PROXY); + console.log('CTF:', CTF); + console.log('Adapter:', ADAPTER); + console.log('Collateral:', COLLATERAL); + console.log('RPC:', RPC); + console.log('DRY_RUN:', DRY_RUN); + + const ctfIface = new ethers.Interface([ + 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)' + ]); + const safeIface = new ethers.Interface([ + 'function nonce() view returns (uint256)', + 'function getOwners() view returns (address[])', + 'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce) view returns (bytes32)', + 'function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures) public payable returns (bool)', + ]); + const safe = new ethers.Contract(PROXY, safeIface, wallet); + + const owners: string[] = await safe.getOwners(); + console.log('Safe owners:', owners); + const isOwner = owners.map(o => o.toLowerCase()).includes(wallet.address.toLowerCase()); + console.log('EOA is owner?:', isOwner); + if (!isOwner) { console.error('❌ EOA is not a Safe owner'); process.exit(1); } + + const bal = await provider.getBalance(wallet.address); + console.log('EOA POL balance:', ethers.formatEther(bal)); + + // Query claimable positions + const res = await fetch(`https://data-api.polymarket.com/positions?user=${PROXY}&sizeThreshold=.01&redeemable=true&limit=100&offset=0`); + const arr: any[] = await res.json(); + const claimable = arr.filter(p => p.curPrice === 1); + console.log(`\nClaimable position count: ${claimable.length}`); + claimable.forEach(p => console.log(` - ${p.title} | $${p.currentValue.toFixed(4)} | ${p.conditionId}`)); + if (!claimable.length) { console.log('Nothing to claim, exiting'); return; } + + for (let i = 0; i < claimable.length; i++) { + const p = claimable[i]; + console.log(`\n[${i+1}/${claimable.length}] ${p.title}`); + try { + const calldata = ctfIface.encodeFunctionData('redeemPositions', [ + COLLATERAL, ZERO_BYTES32, p.conditionId, [1, 2] + ]); + const nonce: bigint = await safe.nonce(); + console.log(` nonce: ${nonce}`); + const txHash: string = await safe.getTransactionHash( + ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce + ); + const sig = await wallet.signMessage(ethers.getBytes(txHash)); + const v = parseInt(sig.slice(-2), 16) + 4; + const adjustedSig = sig.slice(0, -2) + v.toString(16).padStart(2, '0'); + + // First verify with estimateGas / staticCall + try { + await safe.execTransaction.staticCall( + ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig + ); + console.log(' ✓ staticCall passed'); + } catch (e: any) { + console.error(` ✗ staticCall failed: ${e.shortMessage || e.message}`); + continue; + } + + const gasEst = await safe.execTransaction.estimateGas( + ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig + ); + console.log(` estimateGas: ${gasEst}`); + + if (DRY_RUN) { + console.log(' (DRY_RUN, skipping send)'); + continue; + } + + const tx = await safe.execTransaction( + ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig + ); + console.log(` Sent txHash: ${tx.hash}`); + const receipt = await Promise.race([ + tx.wait(), + new Promise((_, reject) => setTimeout(() => reject(new Error('On-chain timeout 60s')), 60000)) + ]) as any; + console.log(` ✓ Success block:${receipt.blockNumber} gasUsed:${receipt.gasUsed}`); + } catch (e: any) { + console.error(` ✗ Failed: ${e.shortMessage || e.message}`); + } + } +} + +main().catch(e => { console.error(e); process.exit(1); }); diff --git a/scripts/claim-relayer.ts b/scripts/claim-relayer.ts new file mode 100644 index 0000000..6b579f1 --- /dev/null +++ b/scripts/claim-relayer.ts @@ -0,0 +1,151 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; +import { getContractConfig } from '@polymarket/clob-client-v2'; + +const PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY!; +const PROXY = process.env.POLYMARKET_PROXY_ADDRESS!; +const RPC = process.env.POLYGON_RPC_URL || 'https://polygon-bor-rpc.publicnode.com'; +const RELAYER = 'https://relayer-v2.polymarket.com'; +const MULTISEND = '0xA238CBeb142c10Ef7Ad8442C6D1f9E89e07e7761'; +const DRY_RUN = process.argv.includes('--dry-run'); + +if (!PRIVATE_KEY || !PROXY) { console.error('Missing env'); process.exit(1); } + +const ZERO32 = '0x' + '0'.repeat(64); +const ctfIface = new ethers.Interface([ + 'function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)' +]); +const safeIface = new ethers.Interface([ + 'function nonce() view returns (uint256)', + 'function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce) view returns (bytes32)', +]); + +// Pack redeemPositions into a single multiSend item: [op(1)][to(20)][value(32)][dataLen(32)][data] +function encodeMultiSendItem(to: string, data: string): string { + const op = '00'; // CALL + const toHex = to.toLowerCase().replace(/^0x/, '').padStart(40, '0'); + const valueHex = '0'.repeat(64); + const dataBytes = ethers.getBytes(data); + const lenHex = dataBytes.length.toString(16).padStart(64, '0'); + const dataHex = data.replace(/^0x/, ''); + return op + toHex + valueHex + lenHex + dataHex; +} + +async function main() { + const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + const contracts = getContractConfig(137); + const CTF = contracts.conditionalTokens; + const COLLATERAL = contracts.collateral; + + console.log('EOA:', wallet.address); + console.log('Proxy(Safe):', PROXY); + console.log('CTF:', CTF, ' Collateral:', COLLATERAL); + + // 1. Query claimable + const res = await fetch(`https://data-api.polymarket.com/positions?user=${PROXY}&sizeThreshold=.01&redeemable=true&limit=100&offset=0`); + const arr: any[] = await res.json(); + const claimable = arr.filter(p => p.curPrice === 1); + console.log(`Claimable position count: ${claimable.length}`); + claimable.forEach(p => console.log(` - ${p.title} | $${p.currentValue.toFixed(4)}`)); + if (!claimable.length) { console.log('Nothing to claim, exiting'); return; } + const total = claimable.reduce((s, p) => s + p.currentValue, 0); + console.log(`Total: $${total.toFixed(4)}`); + + // 2. Single redeemPositions (per the official docs example, operation=0 CALL sent directly to CTF) + const p = claimable[0]; + const data = ctfIface.encodeFunctionData('redeemPositions', [ + COLLATERAL, ZERO32, p.conditionId, [1, 2] + ]); + console.log('redeemPositions data length:', data.length / 2 - 1, 'bytes'); + + // 3. Compute Safe txHash + signature + const safe = new ethers.Contract(PROXY, safeIface, provider); + const nonce: bigint = await safe.nonce(); + console.log('Safe nonce:', nonce.toString()); + const txHash: string = await safe.getTransactionHash( + CTF, 0, data, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce + ); + console.log('Safe txHash:', txHash); + const sig = await wallet.signMessage(ethers.getBytes(txHash)); + const v = parseInt(sig.slice(-2), 16) + 4; + const adjustedSig = sig.slice(0, -2) + v.toString(16).padStart(2, '0'); + + // 4. Build the request body (per the official docs: operation=0 CALL, to=CTF, no metadata) + const body = { + from: wallet.address, + to: CTF, + proxyWallet: PROXY, + data, + nonce: nonce.toString(), + signature: adjustedSig, + signatureParams: { + gasPrice: '0', + operation: '0', + safeTxnGas: '0', + baseGas: '0', + gasToken: ethers.ZeroAddress, + refundReceiver: ethers.ZeroAddress, + }, + type: 'SAFE', + }; + + if (DRY_RUN) { + console.log('\n--- DRY_RUN body ---'); + console.log(JSON.stringify(body, null, 2)); + return; + } + + // 5. Submit + const RELAYER_API_KEY = process.env.RELAYER_API_KEY!; + const RELAYER_API_KEY_ADDRESS = process.env.RELAYER_API_KEY_ADDRESS!; + if (!RELAYER_API_KEY || !RELAYER_API_KEY_ADDRESS) { + console.error('Missing RELAYER_API_KEY / RELAYER_API_KEY_ADDRESS env'); + process.exit(1); + } + console.log('\nSubmitting to relayer...'); + // Use undici fetch to explicitly preserve header case + const { fetch: undiciFetch } = await import('undici'); + const submitRes: any = await undiciFetch(`${RELAYER}/submit`, { + method: 'POST', + headers: [ + ['Content-Type', 'application/json'], + ['RELAYER_API_KEY', RELAYER_API_KEY], + ['RELAYER_API_KEY_ADDRESS', RELAYER_API_KEY_ADDRESS], + ], + body: JSON.stringify(body), + }); + const submitText = await submitRes.text(); + console.log('Status:', submitRes.status); + console.log('Body:', submitText); + if (submitRes.status === 401) { + // Retry with curl to confirm whether the server really cannot see our headers + console.log('\n[debug] Retrying with curl'); + const curlCmd = `curl -s -i -X POST '${RELAYER}/submit' -H 'Content-Type: application/json' -H 'RELAYER_API_KEY: ${RELAYER_API_KEY}' -H 'RELAYER_API_KEY_ADDRESS: ${RELAYER_API_KEY_ADDRESS}' -d '${JSON.stringify(body)}'`; + const { execSync } = await import('child_process'); + try { + const out = execSync(curlCmd, { encoding: 'utf8' }); + console.log(out); + } catch (e: any) { + console.error(e.message); + } + return; + } + if (!submitRes.ok) { process.exit(1); } + const { transactionID, transactionHash, state } = JSON.parse(submitText); + console.log(`✓ Submitted successfully id:${transactionID} txHash:${transactionHash} state:${state}`); + + // 6. Poll + for (let i = 0; i < 20; i++) { + await new Promise(r => setTimeout(r, 2000)); + const r = await fetch(`${RELAYER}/transaction?id=${transactionID}`); + const j = await r.json(); + console.log(`[${i+1}] state:${j.state} txHash:${j.transactionHash}`); + if (j.state && j.state !== 'STATE_NEW' && j.state !== 'STATE_PENDING') { + console.log('Final state:', j); + break; + } + } +} + +main().catch(e => { console.error('Error:', e); process.exit(1); }); diff --git a/scripts/list-tokens.ts b/scripts/list-tokens.ts new file mode 100644 index 0000000..1dd2fd6 --- /dev/null +++ b/scripts/list-tokens.ts @@ -0,0 +1,66 @@ +import 'dotenv/config'; +import { ethers } from 'ethers'; + +const RPC = 'https://polygon-bor-rpc.publicnode.com'; +const SAFE = process.env.POLYMARKET_PROXY_ADDRESS!; +const CTF = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045'; + +const provider = new ethers.JsonRpcProvider(RPC, 137, { staticNetwork: true }); + +// Scan all tokens held by the Safe via TransferSingle/TransferBatch events +const ctfIface = new ethers.Interface([ + 'event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value)', + 'event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values)', + 'function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])', +]); +const ctf = new ethers.Contract(CTF, ctfIface, provider); + +async function main() { + console.log('Safe:', SAFE); + const latest = await provider.getBlockNumber(); + console.log('Current block:', latest); + + // Scan the last 50000 blocks (~30 hours, polygon ~2.3s/block) + const fromBlock = latest - 50000; + console.log(`Scanning ${fromBlock} → ${latest}`); + + const safePadded = ethers.zeroPadValue(SAFE, 32); + const TRANSFER_SINGLE = ctfIface.getEvent('TransferSingle')!.topicHash; + + // Scan in segments + const tokenIds = new Set(); + const STEP = 10000; + for (let from = fromBlock; from <= latest; from += STEP) { + const to = Math.min(from + STEP - 1, latest); + // to = SAFE + const logsIn = await provider.getLogs({ + address: CTF, + topics: [TRANSFER_SINGLE, null, null, safePadded], + fromBlock: from, toBlock: to, + }); + for (const log of logsIn) { + const parsed = ctfIface.parseLog({ topics: [...log.topics], data: log.data }); + if (parsed) tokenIds.add(parsed.args.id.toString()); + } + process.stdout.write(`.`); + } + console.log(`\nFound ${tokenIds.size} distinct tokenIds (received within the last 30h)`); + + if (tokenIds.size === 0) return; + + // Batch query balances + const ids = [...tokenIds]; + const accounts = ids.map(() => SAFE); + const balances: bigint[] = await ctf.balanceOfBatch(accounts, ids); + + let nonZero = 0; + for (let i = 0; i < ids.length; i++) { + if (balances[i] > 0n) { + nonZero++; + console.log(`tokenId=${ids[i]} balance=${ethers.formatUnits(balances[i], 6)}`); + } + } + console.log(`\n=== Total ${nonZero} tokens with balance > 0 ===`); +} + +main().catch(e => console.error(e)); diff --git a/scripts/test-withdraw.ts b/scripts/test-withdraw.ts new file mode 100644 index 0000000..9b25831 --- /dev/null +++ b/scripts/test-withdraw.ts @@ -0,0 +1,198 @@ +/** + * Polymarket Proxy Wallet withdraw test script + * + * Uses Gnosis Safe execTransaction: EOA signs SafeTx EIP-712 hash -> call Proxy to transfer out USDC + * Does not depend on the official Polymarket API, interacts directly with the on-chain Safe + USDC contracts + * + * Usage: + * 1. Fill in CONFIG below (private key / Proxy / target address / withdraw amount) + * 2. tsx scripts/test-withdraw.ts + * + * Safety: DRY_RUN=true by default, only prints, does not send transactions. Change to false after confirming the params are correct. + */ + +import { ethers } from "ethers"; +import { readFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +// Simple .env loader (no third-party package) +const __dirname = dirname(fileURLToPath(import.meta.url)); +const ENV_FILE = resolve(__dirname, "..", ".env"); +if (existsSync(ENV_FILE)) { + for (const line of readFileSync(ENV_FILE, "utf-8").split("\n")) { + const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/); + if (!m) continue; + let val = m[2]; + if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) { + val = val.slice(1, -1); + } + if (process.env[m[1]] === undefined) process.env[m[1]] = val; + } +} + +// ── Config ────────────────────────────────────────────────── +// Private key and Proxy are read from .env (POLYMARKET_PRIVATE_KEY / POLYMARKET_PROXY_ADDRESS) +// This test script only tests one wallet; for production batch withdraws write a separate script that reads multi-wallet config +const CONFIG = { + // Withdraw target wallet (required) + TO_ADDRESS: "0x__TARGET_ADDRESS__", + // Withdraw amount (USDC, 6 decimals). Use 0.01 for testing + AMOUNT_USDC: "0.01", + // true = only print, do not send on-chain; false = actually send the transaction + DRY_RUN: true, +} as const; + +const EOA_PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY || ""; +const PROXY_ADDRESS = process.env.POLYMARKET_PROXY_ADDRESS || ""; +if (!EOA_PRIVATE_KEY || !PROXY_ADDRESS) { + console.error("✗ .env is missing POLYMARKET_PRIVATE_KEY or POLYMARKET_PROXY_ADDRESS"); + process.exit(1); +} + +// ── Constants ──────────────────────────────────────────────────── +const POLYGON_RPC = "https://polygon-bor-rpc.publicnode.com"; +const POLYGON_CHAIN_ID = 137; +// Polymarket uses USDC.e (PoS bridge USDC), not native USDC +const USDC_ADDRESS = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174"; +const USDC_DECIMALS = 6; + +// Gnosis Safe ABI (only the parts used) +const SAFE_ABI = [ + "function nonce() view returns (uint256)", + "function getThreshold() view returns (uint256)", + "function getOwners() view returns (address[])", + "function execTransaction(address to, uint256 value, bytes data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, bytes signatures) payable returns (bool)", + "function VERSION() view returns (string)", +]; +const ERC20_ABI = [ + "function balanceOf(address) view returns (uint256)", + "function transfer(address,uint256)", + "function decimals() view returns (uint8)", +]; + +// SafeTx EIP-712 type +const EIP712_SAFE_TX_TYPE = { + SafeTx: [ + { type: "address", name: "to" }, + { type: "uint256", name: "value" }, + { type: "bytes", name: "data" }, + { type: "uint8", name: "operation" }, + { type: "uint256", name: "safeTxGas" }, + { type: "uint256", name: "baseGas" }, + { type: "uint256", name: "gasPrice" }, + { type: "address", name: "gasToken" }, + { type: "address", name: "refundReceiver" }, + { type: "uint256", name: "nonce" }, + ], +}; + +async function main() { + console.log("─── Polymarket Proxy withdraw test ───"); + console.log(`DRY_RUN: ${CONFIG.DRY_RUN}`); + + const provider = new ethers.JsonRpcProvider(POLYGON_RPC, POLYGON_CHAIN_ID); + const eoa = new ethers.Wallet(EOA_PRIVATE_KEY, provider); + console.log(`EOA: ${eoa.address}`); + console.log(`Proxy: ${PROXY_ADDRESS}`); + console.log(`To: ${CONFIG.TO_ADDRESS}`); + + // 1. Check Safe status + const safe = new ethers.Contract(PROXY_ADDRESS, SAFE_ABI, provider); + const [owners, threshold, safeNonce] = await Promise.all([ + safe.getOwners() as Promise, + safe.getThreshold() as Promise, + safe.nonce() as Promise, + ]); + console.log(`Owners: ${owners.join(", ")}`); + console.log(`Threshold: ${threshold} Nonce: ${safeNonce}`); + if (!owners.map(o => o.toLowerCase()).includes(eoa.address.toLowerCase())) { + throw new Error("EOA is not an owner of the Proxy, misconfigured"); + } + if (threshold !== 1n) { + throw new Error(`Proxy threshold=${threshold}, this script only supports 1/1 Safe`); + } + + // 2. Check USDC balance + const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, provider); + const balance = await usdc.balanceOf(PROXY_ADDRESS) as bigint; + const balanceHuman = ethers.formatUnits(balance, USDC_DECIMALS); + console.log(`Proxy USDC balance: ${balanceHuman}`); + + const amountWei = ethers.parseUnits(CONFIG.AMOUNT_USDC, USDC_DECIMALS); + if (amountWei > balance) { + throw new Error(`Insufficient balance: trying to withdraw ${CONFIG.AMOUNT_USDC}, only have ${balanceHuman}`); + } + console.log(`Withdraw amount: ${CONFIG.AMOUNT_USDC} USDC (${amountWei} wei)`); + + // 3. Check EOA MATIC (pays gas) + const maticBalance = await provider.getBalance(eoa.address); + console.log(`EOA MATIC: ${ethers.formatEther(maticBalance)}`); + if (maticBalance < ethers.parseEther("0.01")) { + console.warn("⚠ EOA MATIC < 0.01, may not have enough gas"); + } + + // 4. Build the callData for USDC.transfer(to, amount) + const usdcIface = new ethers.Interface(ERC20_ABI); + const transferData = usdcIface.encodeFunctionData("transfer", [CONFIG.TO_ADDRESS, amountWei]); + console.log(`transferData: ${transferData}`); + + // 5. Assemble SafeTx + const safeTx = { + to: USDC_ADDRESS, + value: 0n, + data: transferData, + operation: 0, // CALL + safeTxGas: 0n, + baseGas: 0n, + gasPrice: 0n, + gasToken: ethers.ZeroAddress, + refundReceiver: ethers.ZeroAddress, + nonce: safeNonce, + }; + + // 6. EIP-712 signature + const domain = { + chainId: POLYGON_CHAIN_ID, + verifyingContract: PROXY_ADDRESS, + }; + const signature = await eoa.signTypedData(domain, EIP712_SAFE_TX_TYPE, safeTx); + console.log(`SafeTx signature: ${signature}`); + + if (CONFIG.DRY_RUN) { + console.log("\n✓ DRY_RUN: all params OK, no transaction sent"); + console.log(" Set CONFIG.DRY_RUN = false and run again to actually withdraw"); + return; + } + + // 7. Send on-chain: call Proxy.execTransaction(...) + const safeWithSigner = safe.connect(eoa) as ethers.Contract; + console.log("\nSending execTransaction…"); + const tx = await safeWithSigner.execTransaction( + safeTx.to, + safeTx.value, + safeTx.data, + safeTx.operation, + safeTx.safeTxGas, + safeTx.baseGas, + safeTx.gasPrice, + safeTx.gasToken, + safeTx.refundReceiver, + signature + ); + console.log(`tx hash: ${tx.hash}`); + console.log("Waiting for confirmation…"); + const rec = await tx.wait(); + if (rec?.status === 1) { + console.log(`✓ Success block=${rec.blockNumber} gas=${rec.gasUsed}`); + const newBal = await usdc.balanceOf(PROXY_ADDRESS) as bigint; + console.log(`Proxy new balance: ${ethers.formatUnits(newBal, USDC_DECIMALS)} USDC`); + } else { + console.log("✗ tx mined but status=0 (execTransaction reverted internally)"); + } +} + +main().catch(err => { + console.error("✗ Failed:", err instanceof Error ? err.message : err); + process.exit(1); +}); diff --git a/server.ts b/server.ts new file mode 100644 index 0000000..6bff58d --- /dev/null +++ b/server.ts @@ -0,0 +1,6559 @@ +/** + * BTC 5-minute up/down order book monitor - standalone server + * Start: npx tsx server.ts + */ + +import express from "express"; +import { createServer } from "http"; +import { WebSocketServer, WebSocket } from "ws"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; +import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, readdirSync, unlinkSync, statSync, renameSync } from "fs"; +import { ethers } from "ethers"; +import dotenv from "dotenv"; +import { ClobClient, Side, OrderType, Chain, SignatureTypeV2 as SignatureType, AssetType, getContractConfig } from "@polymarket/clob-client-v2"; +import { getAllStrategies, getStrategy, getAllDescriptions, initStrategies } from "./strategies/registry.js"; +import type { StrategyNumber, StrategyDirection, StrategyLifecycleState, StrategyKey, TunableParam } from "./strategies/types.js"; +import { ALL_STRATEGY_KEYS } from "./strategies/types.js"; +import { getFairProb, hasFairProbTable, setActiveMarket as setFairProbMarket } from "./strategies/_core/fair-prob.js"; +import { setActiveMarket as setDiffExtremesMarket } from "./strategies/_core/diff-extremes.js"; +import { getRealFillFromTx } from "./chain-watcher.js"; +import { PmPnlManager } from "./polymarket-pnl.js"; +import { loadTgConfig, saveTgConfig, sendTgMessage, autoDetectChatId, type TgConfig } from "./tg-push.js"; +import { MARKETS, DEFAULT_KEY, isValidKey, isLegacySymbol, getBinanceWsUrl, priceDecimals, ALL_PERIODS, ALL_SYMBOLS, type MarketKey, type MarketConfig, type MarketSymbol } from "./market-configs.js"; +import { Agent, setGlobalDispatcher } from "undici"; +import * as httpMod from "http"; +import * as httpsMod from "https"; + +// Project version - keep in sync with package.json when changed +export const APP_VERSION = "5.0.0"; + +// -- HTTP Keep-Alive connection optimization ----------------------------- +// Measured: Polymarket server closes idle connections between 30-60 seconds +// - cold start: 689 ms +// - connection reuse: 350 ms (saves 340 ms) +// - reconnect: 1586 ms (actually slower!) +// Use keep-alive + 20s heartbeat to keep the connection alive, stabilizing order latency +// +// The two HTTP channels each have an independent connection pool, configured separately: +// 1) undici (Node built-in fetch) - fetchBookTopOfBook, Gamma API, TG push +setGlobalDispatcher(new Agent({ + keepAliveTimeout: 60000, // client timeout 60s (server closes first) + keepAliveMaxTimeout: 600000, + connections: 10, + pipelining: 1, +})); +// 2) axios (clob-client place order / query open orders / cancel) uses Node native http/https globalAgent +// Node 19+ globalAgent.keepAlive defaults to true, but keepAliveMsecs defaults to only 1000ms, +// when creating a socket the Agent reads the internal options object, so options.keepAliveMsecs must be changed. +// 30000 = send a TCP keep-alive probe every 30s, combined with the 20s application-layer heartbeat keeps the connection stable. +{ + type AgentInternals = { keepAlive: boolean; options: { keepAlive: boolean; keepAliveMsecs: number } }; + const httpAg = httpMod.globalAgent as unknown as AgentInternals; + const httpsAg = httpsMod.globalAgent as unknown as AgentInternals; + httpAg.keepAlive = true; + httpAg.options.keepAlive = true; + httpAg.options.keepAliveMsecs = 30000; + httpsAg.keepAlive = true; + httpsAg.options.keepAlive = true; + httpsAg.options.keepAliveMsecs = 30000; +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: resolve(__dirname, ".env") }); + +// All logs automatically get a UTC+8 time prefix, for easier sequencing +// +// Three outputs: +// 1. Terminal: clean version (filters out lines marked [RAW]) +// 2. logs/trade-YYYY-MM-DD.log: clean version (same as terminal) +// 3. logs/trade-raw-YYYY-MM-DD.log: full version (includes full raw data, for debugging) +// +// Example of a line with [RAW] prefix: console.log("[RAW]", "[Trade.Market] postOrder raw result:", JSON.stringify(result)) +// Lines without the prefix (summary/progress/warning) go to terminal + clean log. +const LOG_DIR = resolve(__dirname, "logs"); +const LOG_RETENTION_DAYS = 1; +const CLEAN_LOG_PREFIX = "trade-"; +const RAW_LOG_PREFIX = "trade-raw-"; +const LOG_FILE_SUFFIX = ".log"; +/** + * US stock market pause status: Fri 20:00 ET after-hours wrap-up ~ Mon 4:00 ET pre-market start is paused + * (includes pre-market/after-hours liquidity periods, only avoids the truly low-liquidity weekend window, 56 hours total) + * Returns { paused, secondsUntilNext }, secondsUntilNext is the seconds until the next status switch + */ +const WK_MAP: Record = { Mon: 0, Tue: 1, Wed: 2, Thu: 3, Fri: 4, Sat: 5, Sun: 6 }; +const PAUSE_START_SEC = 20 * 3600; // pause starts Fri 20:00 ET +const PAUSE_END_SEC = 4 * 3600; // pause ends Mon 4:00 ET +function getUsMarketStatus(d = new Date()): { paused: boolean; secondsUntilNext: number } { + try { + const parts = new Intl.DateTimeFormat("en-US", { + timeZone: "America/New_York", + weekday: "short", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }).formatToParts(d); + let wk = "", hour = 0, minute = 0, second = 0; + for (const p of parts) { + if (p.type === "weekday") wk = p.value; + else if (p.type === "hour") hour = parseInt(p.value, 10) || 0; + else if (p.type === "minute") minute = parseInt(p.value, 10) || 0; + else if (p.type === "second") second = parseInt(p.value, 10) || 0; + } + if (hour === 24) hour = 0; + const idx = WK_MAP[wk]; + if (idx == null) return { paused: false, secondsUntilNext: 0 }; + const hms = hour * 3600 + minute * 60 + second; + const paused = + (idx === 5 || idx === 6) || + (idx === 4 && hms >= PAUSE_START_SEC) || + (idx === 0 && hms < PAUSE_END_SEC); + + let secondsUntilNext: number; + if (paused) { + // compute to next Mon 4:00:00 ET + if (idx === 4) secondsUntilNext = (86400 - hms) + 2 * 86400 + PAUSE_END_SEC; // Fri ≥ 20:00 + else if (idx === 5) secondsUntilNext = (86400 - hms) + 1 * 86400 + PAUSE_END_SEC; // Sat + else if (idx === 6) secondsUntilNext = (86400 - hms) + PAUSE_END_SEC; // Sun + else secondsUntilNext = PAUSE_END_SEC - hms; // Mon < 4:00 + } else { + // compute to next Fri 20:00:00 ET + if (idx === 0) secondsUntilNext = (86400 - hms) + 3 * 86400 + PAUSE_START_SEC; // Mon ≥ 4:00 + else if (idx === 1) secondsUntilNext = (86400 - hms) + 2 * 86400 + PAUSE_START_SEC; // Tue + else if (idx === 2) secondsUntilNext = (86400 - hms) + 1 * 86400 + PAUSE_START_SEC; // Wed + else if (idx === 3) secondsUntilNext = (86400 - hms) + PAUSE_START_SEC; // Thu + else secondsUntilNext = PAUSE_START_SEC - hms; // Fri < 20:00 + } + return { paused, secondsUntilNext: Math.max(0, secondsUntilNext) }; + } catch { + return { paused: false, secondsUntilNext: 0 }; + } +} +function isUsWeekend(d = new Date()): boolean { + return getUsMarketStatus(d).paused; +} +// throttle: weekend pause log prints at most once every 30 minutes (avoids 250ms tick flooding) +let _weekendPauseLogAt = 0; +function logWeekendPauseOnce(): void { + const now = Date.now(); + if (now - _weekendPauseLogAt < 30 * 60 * 1000) return; + _weekendPauseLogAt = now; + console.log("[Strategy.AntiManip] US stock low-liquidity period (Fri 20:00 ET ~ Mon 4:00 ET), pausing all new entries (positions/TP/SL run normally)"); +} + +function getCnDateString(d = new Date()): string { + // compute date by UTC+8 to avoid midnight-switch errors + const cn = new Date(d.getTime() + 8 * 3600 * 1000); + return cn.toISOString().slice(0, 10); // YYYY-MM-DD +} +function getCleanLogFile(): string { + return resolve(LOG_DIR, `${CLEAN_LOG_PREFIX}${getCnDateString()}${LOG_FILE_SUFFIX}`); +} +function getRawLogFile(): string { + return resolve(LOG_DIR, `${RAW_LOG_PREFIX}${getCnDateString()}${LOG_FILE_SUFFIX}`); +} +function cleanupOldLogs(): void { + try { + if (!existsSync(LOG_DIR)) return; + const files = readdirSync(LOG_DIR); + const today = getCnDateString(); + const todayMs = Date.parse(today + "T00:00:00+08:00"); + const cutoffMs = todayMs - LOG_RETENTION_DAYS * 24 * 3600 * 1000; + for (const f of files) { + // handle both .log and rotated backup .log.1 + let base = f; + if (base.endsWith(".1")) base = base.slice(0, -2); + if (!base.endsWith(LOG_FILE_SUFFIX)) continue; + let dateStr = ""; + if (base.startsWith(RAW_LOG_PREFIX)) dateStr = base.slice(RAW_LOG_PREFIX.length, base.length - LOG_FILE_SUFFIX.length); + else if (base.startsWith(CLEAN_LOG_PREFIX)) dateStr = base.slice(CLEAN_LOG_PREFIX.length, base.length - LOG_FILE_SUFFIX.length); + else continue; + const fileMs = Date.parse(dateStr + "T00:00:00+08:00"); + if (!Number.isFinite(fileMs)) continue; + if (fileMs < cutoffMs) { + try { unlinkSync(resolve(LOG_DIR, f)); } catch { /* ignore */ } + } + } + } catch { /* ignore */ } +} +try { mkdirSync(LOG_DIR, { recursive: true }); } catch { /* ignore */ } +cleanupOldLogs(); +// at 0:01 switch date file + clean up old files (extra 1 minute tolerance for cross-day timezone jitter) +function scheduleDailyLogRotate(): void { + const now = new Date(); + const cnNow = now.getTime() + 8 * 3600 * 1000; + const next = new Date(cnNow); + next.setUTCHours(0, 1, 0, 0); + next.setUTCDate(next.getUTCDate() + 1); + const delayMs = next.getTime() - cnNow; + setTimeout(() => { + cleanupOldLogs(); + scheduleDailyLogRotate(); + }, Math.max(60_000, delayMs)).unref?.(); +} +scheduleDailyLogRotate(); +// rotate when a single log file exceeds this size (rename to .1, overwriting old .1; new writes start from 0) +const LOG_MAX_BYTES = 10 * 1024 * 1024; +// RAW log switch (detailed raw data, large size): off by default, set RAW_LOG_ENABLED=true in .env when debugging +const RAW_LOG_ENABLED = String(process.env.RAW_LOG_ENABLED ?? "").toLowerCase() === "true"; +function rotateIfTooLarge(file: string): void { + try { + if (!existsSync(file)) return; + const sz = statSync(file).size; + if (sz < LOG_MAX_BYTES) return; + const rotated = file + ".1"; + try { if (existsSync(rotated)) unlinkSync(rotated); } catch { /* ignore */ } + renameSync(file, rotated); + } catch { /* ignore */ } +} +function appendCleanLog(line: string): void { + try { + const f = getCleanLogFile(); + rotateIfTooLarge(f); + appendFileSync(f, line + "\n"); + } catch { /* ignore */ } +} +function appendRawLog(line: string): void { + if (!RAW_LOG_ENABLED) return; + try { + const f = getRawLogFile(); + rotateIfTooLarge(f); + appendFileSync(f, line + "\n"); + } catch { /* ignore */ } +} +(["log", "warn", "error"] as const).forEach(method => { + const orig = console[method].bind(console); + console[method] = (...args: unknown[]) => { + const ts = new Date().toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false }); + // detect whether the first arg is the "[RAW]" marker + const isRaw = args.length > 0 && args[0] === "[RAW]"; + let realArgs = isRaw ? args.slice(1) : args; + // The SDK's "[CLOB Client] request error" log dumps the entire axios request/response into an extremely long JSON, flooding badly + // after intercepting: terminal only shows a short summary (status + url + error), the full object goes to raw log + let isClobErrorDigest = false; + if (!isRaw && realArgs.length >= 2 && realArgs[0] === "[CLOB Client] request error") { + try { + const obj = typeof realArgs[1] === "string" ? JSON.parse(realArgs[1] as string) : realArgs[1]; + const status = (obj as any)?.status ?? "?"; + const errMsg = (obj as any)?.data?.error ?? (obj as any)?.error ?? "?"; + const url = (obj as any)?.config?.url ?? "?"; + // generate a short summary for terminal + clean log; full body goes to raw log + appendRawLog(`[${ts}] [${method.toUpperCase()}] [CLOB Client] request error full: ${typeof realArgs[1] === "string" ? realArgs[1] : JSON.stringify(obj)}`); + realArgs = [`[CLOB Client] ${status} ${url.split("?")[0]} - ${errMsg}`]; + isClobErrorDigest = true; + } catch { /* on parse failure pass through as-is */ } + } + const text = realArgs.map(a => { + if (typeof a === "string") return a; + if (a instanceof Error) return a.stack || a.message; + try { return typeof a === "object" ? JSON.stringify(a) : String(a); } catch { return String(a); } + }).join(" "); + if (!isRaw) { + // clean: terminal + clean log + orig(`[${ts}]`, ...realArgs); + appendCleanLog(`[${ts}] [${method.toUpperCase()}] ${text}`); + } + // raw always goes to raw log (clean lines are mirrored too, to keep raw log context complete) + // if the full CLOB body was already written to raw log above, skip the corresponding line here to avoid duplication + if (!isClobErrorDigest) { + appendRawLog(`[${ts}] [${method.toUpperCase()}]${isRaw ? " [RAW]" : ""} ${text}`); + } + }; +}); + +// prevent uncaught Promise rejections (e.g. RPC timeouts) from killing the process +process.on('unhandledRejection', (reason) => { + console.error('[System.Exception]', reason instanceof Error ? reason.message : reason); +}); + +type AppMode = "full" | "headless"; +type ClientDataMode = "full" | "low"; + +interface StrategyConfig { + enabled: Record; + amount: Record; + shares: Record; // share config for limit strategies (minimum 5) + /** strategy tunable params (e.g. l1's tpDelta/slDiff), indexed by strategyKey -> paramKey -> value */ + params: Record>; + slippage: number; + autoClaimEnabled: boolean; + maxRoundEntries: number; + marketHoursOnly: boolean; // momentum strategies only enter during US market hours + weekendPause: boolean; // US Eastern weekend (Sat/Sun) pauses all strategy entries +} + +interface StrategyConfigUpdate { + enabled?: Partial>; + amount?: Partial>; + shares?: Partial>; + params?: Partial>; + slippage?: unknown; + autoClaimEnabled?: unknown; + maxRoundEntries?: unknown; + marketHoursOnly?: unknown; + weekendPause?: unknown; +} + +interface StrategyRuntimeState { + state: StrategyLifecycleState; + activeStrategy: StrategyKey | null; + direction: StrategyDirection | null; + buyAmount: number; + posBeforeBuy: number; + posBeforeSell: number; + waitVerifyAfterSell: boolean; + cleanupAfterVerify: boolean; + actionTs: number; + prevUpPct: number | null; + buyLockUntil: number; + positionsReady: boolean; + roundEntryCount: number; +} + +interface Kline { + openTime: number; + open: number; + high: number; + low: number; + close: number; + volume: number; + closed: boolean; +} + +interface PendingTradeMeta { + key: string; + orderId?: string; + ts: number; + windowStart: number; + side: "buy" | "sell"; + direction: StrategyDirection; + amount: number; + worstPrice: number; + source: string; + exitReason?: string; + roundEntry?: string; + // only on buy: may carry these; after fill, create conditional orders based on these params + stopProfit?: { pctDelta?: number; targetPrice?: number }; + stopLoss?: { pctDelta?: number; diffValue?: number; slippage?: number }; +} + +interface ClientSession { + dataMode: ClientDataMode; + lastStateSentAt: number; + stateTimer: NodeJS.Timeout | null; + stateDirty: boolean; + stateIncludeHistory: boolean; +} + +interface StatePayloadOptions { + includeHistory?: boolean; + simple?: boolean; +} + +function parseBooleanEnv(name: string, fallback: boolean): boolean { + const raw = process.env[name]?.trim().toLowerCase(); + if (!raw) return fallback; + if (["1", "true", "yes", "on"].includes(raw)) return true; + if (["0", "false", "no", "off"].includes(raw)) return false; + return fallback; +} + +function parseNumberEnv(name: string, fallback: number, minimum?: number): number { + const raw = process.env[name]?.trim(); + if (!raw) return fallback; + const value = Number(raw); + if (!Number.isFinite(value)) return fallback; + if (minimum != null && value < minimum) return fallback; + return value; +} + +function parseBooleanLike(value: unknown): boolean | null { + if (typeof value === "boolean") return value; + if (typeof value === "string") { + const normalized = value.trim().toLowerCase(); + if (["1", "true", "yes", "on"].includes(normalized)) return true; + if (["0", "false", "no", "off"].includes(normalized)) return false; + } + return null; +} + +function parseNumberLike(value: unknown, minimum: number): number | null { + const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; + if (!Number.isFinite(parsed) || parsed < minimum) return null; + return parsed; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value != null && !Array.isArray(value); +} + +const PORT_BASE = Number(process.env.PORT) || 3456; +const PORT_MAX_TRIES = 10; +let PORT = PORT_BASE; +const MARKET_WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/market"; +const CHAINLINK_WS_URL = "wss://ws-live-data.polymarket.com"; +const USER_WS_URL = "wss://ws-subscriptions-clob.polymarket.com/ws/user"; +const COINBASE_WS_URL = "wss://ws-feed.exchange.coinbase.com"; +// Note: Binance WS URL, Coinbase product, slug prefix all depend on the current activeMarket, see market-configs.ts below +const GAMMA_URL = "https://gamma-api.polymarket.com"; +const CLOB_URL = "https://clob.polymarket.com"; +const HISTORY_RETENTION_MS = 130000; +const MAX_CHAINLINK_HISTORY_POINTS = 2000; +const MAX_BINANCE_HISTORY_POINTS = 4000; +const MAX_COINBASE_HISTORY_POINTS = 4000; +const MAX_KLINE_1M = 200; // keep 200 1-minute klines +const MAX_KLINE_5M = 50; // keep 50 5-minute klines +const MAX_CONFIRMED_TRADE_IDS = 2000; +const CLAIM_CYCLE_DELAY_MS = 15000; // query loop interval 15s (frontend amount refresh + on-chain balance check) +const CLAIM_COOLDOWN_MS = 5 * 60 * 1000; // Claim execution cooldown: can only claim again 5 minutes after success/failure +const UNVERIFIED_SELL_BUFFER = 0.05; +const POST_TRADE_CALIBRATION_MS = 18000; // post-order calibration wait duration; the buy lock also uses this value +const STRAT_BUY_LOCK_MS = POST_TRADE_CALIBRATION_MS; +const STRATEGY_TICK_MS = 250; +const WAIT_FILL_TIMEOUT_MS = 10000; +const FILL_RECONCILE_TIMEOUT_MS = POST_TRADE_CALIBRATION_MS + 2000; // wait 2 more seconds after calibration completes to confirm +const BINANCE_ALIGN_WINDOW_MS = 60000; +const BINANCE_ALIGN_MIN_SPAN_MS = 10000; +const BINANCE_ALIGN_BUCKET_MS = 500; +const BINANCE_ALIGN_REFRESH_MS = 30000; +const BINANCE_OFFSET_EPSILON = 0.01; +const FULL_DATA_STATE_INTERVAL_MS = 200; +const LOW_DATA_STATE_INTERVAL_MS = 2000; +const MAX_WS_BUFFERED_BYTES = 512 * 1024; +const STRATEGY_CONFIG_FILE = resolve(__dirname, ".strategy-config.json"); +const ACTIVE_MARKET_FILE = resolve(__dirname, ".active-market.json"); +const BACKTEST_DATA_DIR = resolve(__dirname, "backtest-data"); + +function loadActiveMarketKey(): MarketKey { + try { + if (existsSync(ACTIVE_MARKET_FILE)) { + const data = JSON.parse(readFileSync(ACTIVE_MARKET_FILE, "utf-8")); + // new format: { key: "btc-5m" } + if (data && typeof data.key === "string" && isValidKey(data.key)) return data.key; + // compat with old format: { symbol: "btc" } -> default upgrade to 5m + if (data && typeof data.symbol === "string" && isLegacySymbol(data.symbol)) { + return `${data.symbol}-5m` as MarketKey; + } + } + } catch { /* ignore */ } + return DEFAULT_KEY; +} + +function saveActiveMarketKey(key: MarketKey): void { + try { + writeFileSync(ACTIVE_MARKET_FILE, JSON.stringify({ key }, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn(`[System.Market] failed to save current market: ${(err as Error).message}`); + } +} + +let activeMarket: MarketConfig = MARKETS[loadActiveMarketKey()]; +setFairProbMarket(activeMarket.symbol, activeMarket.period); +setDiffExtremesMarket(activeMarket.symbol, activeMarket.period); +console.log(`[System.Market] current market: ${activeMarket.displayName} (key=${activeMarket.key})`); +const PENDING_TRADE_META_MAX_AGE_MS = 15 * 60 * 1000; + +const PRIVATE_KEY = process.env.POLYMARKET_PRIVATE_KEY || ""; +const PROXY_ADDRESS = process.env.POLYMARKET_PROXY_ADDRESS || ""; +let accountName = ""; + +async function fetchAccountNameOnce(): Promise { + try { + const res = await fetch( + `https://polymarket.com/api/profile/userData?address=${PROXY_ADDRESS.toLowerCase()}`, + { signal: AbortSignal.timeout(5000) } + ); + if (!res.ok) return false; + const data = await res.json() as { name?: string; pseudonym?: string }; + const name = data.name || data.pseudonym || ""; + if (!name) return false; + accountName = name; + saveAccountNameToCreds(name); + console.log(`[System.Account] ${name} (${PROXY_ADDRESS.slice(0, 6)}...${PROXY_ADDRESS.slice(-4)})`); + return true; + } catch (e) { + console.log(`[System.Account] failed to get username:`, (e as Error).message); + return false; + } +} + +async function ensureAccountName(): Promise { + if (accountName || !PROXY_ADDRESS) return; + // backoff: 5s / 30s / 5min, give up after all fail, retry on next startup + const delays = [5_000, 30_000, 300_000]; + if (await fetchAccountNameOnce()) return; + for (const ms of delays) { + await new Promise(r => setTimeout(r, ms)); + if (accountName) return; // filled by another path in the meantime + if (await fetchAccountNameOnce()) return; + } +} +const APP_MODE: AppMode = process.env.APP_MODE === "headless" ? "headless" : "full"; +const IS_FULL_MODE = APP_MODE === "full"; + +function createEnvStrategyConfig(): StrategyConfig { + const enabled = {} as Record; + const amount = {} as Record; + const shares = {} as Record; + const params = {} as Record>; + for (const key of ALL_STRATEGY_KEYS) { + const upper = key.toUpperCase(); + enabled[key] = parseBooleanEnv(`STRATEGY_${upper}_ENABLED`, false); + amount[key] = parseNumberEnv(`STRATEGY_${upper}_AMOUNT`, 1, 0.01); + shares[key] = parseNumberEnv(`STRATEGY_${upper}_SHARES`, 5, 5); + params[key] = {}; + } + return { + enabled, + amount, + shares, + params, + slippage: parseNumberEnv("ORDER_DEFAULT_SLIPPAGE", 0.06, 0), + autoClaimEnabled: parseBooleanEnv("AUTO_CLAIM_ENABLED", true), + maxRoundEntries: parseNumberEnv("MAX_ROUND_ENTRIES", 1, 1), + marketHoursOnly: parseBooleanEnv("MARKET_HOURS_ONLY", false), + weekendPause: parseBooleanEnv("WEEKEND_PAUSE", false), + }; +} + +function cloneStrategyConfig(config: StrategyConfig): StrategyConfig { + const params = {} as Record>; + for (const key of Object.keys(config.params)) { + params[key] = { ...config.params[key] }; + } + return { + enabled: { ...config.enabled }, + amount: { ...config.amount }, + shares: { ...config.shares }, + params, + slippage: config.slippage, + autoClaimEnabled: config.autoClaimEnabled, + maxRoundEntries: config.maxRoundEntries, + marketHoursOnly: config.marketHoursOnly, + weekendPause: config.weekendPause, + }; +} + +function loadPersistedStrategyConfig(config: StrategyConfig): void { + if (!existsSync(STRATEGY_CONFIG_FILE)) return; + try { + const raw = JSON.parse(readFileSync(STRATEGY_CONFIG_FILE, "utf-8")); + if (typeof raw.maxRoundEntries === "number" && raw.maxRoundEntries >= 1) { + config.maxRoundEntries = Math.floor(raw.maxRoundEntries); + } + if (typeof raw.marketHoursOnly === "boolean") { + config.marketHoursOnly = raw.marketHoursOnly; + } + if (typeof raw.weekendPause === "boolean") { + config.weekendPause = raw.weekendPause; + } + if (typeof raw.autoClaimEnabled === "boolean") { + config.autoClaimEnabled = raw.autoClaimEnabled; + } + if (isRecord(raw.enabled)) { + for (const key of Object.keys(raw.enabled)) { + if (typeof raw.enabled[key] === "boolean") { + config.enabled[key] = raw.enabled[key] as boolean; + } + } + } + if (isRecord(raw.amount)) { + for (const key of Object.keys(raw.amount)) { + const v = raw.amount[key]; + if (typeof v === "number" && v >= 0.01) { + config.amount[key] = v; + } + } + } + if (isRecord(raw.shares)) { + for (const key of Object.keys(raw.shares)) { + const v = raw.shares[key]; + if (typeof v === "number" && v >= 5) { + config.shares[key] = v; + } + } + } + if (isRecord(raw.params)) { + for (const key of Object.keys(raw.params)) { + const sub = raw.params[key]; + if (!isRecord(sub)) continue; + if (config.params[key] == null) config.params[key] = {}; + for (const pk of Object.keys(sub)) { + const v = sub[pk]; + if (typeof v === "number" && Number.isFinite(v)) { + config.params[key][pk] = v; + } + } + } + } + } catch { + // ignore + } +} + +function savePersistedStrategyConfig(config: StrategyConfig): void { + try { + writeFileSync(STRATEGY_CONFIG_FILE, JSON.stringify({ + maxRoundEntries: config.maxRoundEntries, + marketHoursOnly: config.marketHoursOnly, + weekendPause: config.weekendPause, + autoClaimEnabled: config.autoClaimEnabled, + enabled: config.enabled, + amount: config.amount, + shares: config.shares, + params: config.params, + }, null, 2)); + } catch (err) { + console.warn(`[Strategy.Config] persist save failed: ${err instanceof Error ? err.message : String(err)}`); + } +} + +function getStrategyTunableParams(key: StrategyKey): TunableParam[] { + const s = getStrategy(key); + if (!s) return []; + try { + return s.getDescription().tunableParams ?? []; + } catch { + return []; + } +} + +/** inject the current value of strategyConfig.params[key] into the strategy instance field (call before each tick / at startup) */ +function applyTunableParamsToStrategy(s: { key: StrategyKey }): void { + const defs = getStrategyTunableParams(s.key); + if (!defs.length) return; + const stored = strategyConfig.params[s.key]; + for (const def of defs) { + const v = stored?.[def.key]; + if (typeof v === "number" && Number.isFinite(v)) { + (s as any)[def.key] = v; + } + } +} + +function applyStrategyConfigUpdate(current: StrategyConfig, rawUpdate: unknown): { config?: StrategyConfig; error?: string } { + if (!isRecord(rawUpdate)) return { error: "invalid config format" }; + const next = cloneStrategyConfig(current); + + if ("enabled" in rawUpdate) { + if (!isRecord(rawUpdate.enabled)) return { error: "invalid enabled config format" }; + for (const key of ALL_STRATEGY_KEYS) { + if (!(key in rawUpdate.enabled)) continue; + const parsed = parseBooleanLike(rawUpdate.enabled[key]); + if (parsed == null) return { error: `${key} switch must be a boolean` }; + next.enabled[key] = parsed; + } + } + + if ("amount" in rawUpdate) { + if (!isRecord(rawUpdate.amount)) return { error: "invalid amount config format" }; + for (const key of ALL_STRATEGY_KEYS) { + if (!(key in rawUpdate.amount)) continue; + const parsed = parseNumberLike(rawUpdate.amount[key], 0.01); + if (parsed == null) return { error: `${key} amount must be >= 0.01` }; + next.amount[key] = parsed; + } + } + + if ("shares" in rawUpdate) { + if (!isRecord(rawUpdate.shares)) return { error: "invalid shares config format" }; + for (const key of ALL_STRATEGY_KEYS) { + if (!(key in rawUpdate.shares)) continue; + const parsed = parseNumberLike(rawUpdate.shares[key], 5); + if (parsed == null) return { error: `${key} shares must be >= 5` }; + next.shares[key] = parsed; + } + } + + if ("params" in rawUpdate) { + if (!isRecord(rawUpdate.params)) return { error: "invalid params config format" }; + for (const key of ALL_STRATEGY_KEYS) { + if (!(key in rawUpdate.params)) continue; + const sub = rawUpdate.params[key]; + if (!isRecord(sub)) return { error: `${key} invalid params format` }; + const desc = getStrategyTunableParams(key); + if (next.params[key] == null) next.params[key] = {}; + for (const pk of Object.keys(sub)) { + const def = desc.find((d: TunableParam) => d.key === pk); + const min = def?.min ?? -Infinity; + const parsed = parseNumberLike(sub[pk], min === -Infinity ? -Number.MAX_SAFE_INTEGER : min); + if (parsed == null) return { error: `${key}.${pk} must be >= ${min}` }; + if (def?.max != null && parsed > def.max) return { error: `${key}.${pk} must be <= ${def.max}` }; + next.params[key][pk] = parsed; + } + // L1 cross-param constraints + if (key === "l1") { + const p = next.params.l1 ?? {}; + const defaults: Record = {}; + for (const d of desc) defaults[d.key] = d.defaultValue; + const entryDiff = p.entryDiff ?? defaults.entryDiff ?? 40; + const cancelDiff = p.cancelDiff ?? defaults.cancelDiff ?? 30; + const limitPrice = p.limitPrice ?? defaults.limitPrice ?? 0.5; + const tpDelta = p.tpDelta ?? defaults.tpDelta ?? 0.15; + const remMax = p.remMax ?? defaults.remMax ?? 300; + const remMin = p.remMin ?? defaults.remMin ?? 10; + if (cancelDiff >= entryDiff) return { error: `L1 cancel threshold (${cancelDiff}) must be less than entry threshold (${entryDiff})` }; + if (limitPrice + tpDelta > 0.99 + 1e-9) return { error: `L1 entry price + TP delta (${(limitPrice + tpDelta).toFixed(2)}) must be <= 0.99` }; + if (remMin >= remMax) return { error: `L1 entry time-remaining lower bound (${remMin}) must be less than upper bound (${remMax})` }; + } + } + } + + if ("slippage" in rawUpdate) { + const parsed = parseNumberLike(rawUpdate.slippage, 0); + if (parsed == null) return { error: "slippage must be >= 0" }; + next.slippage = parsed; + } + + if ("autoClaimEnabled" in rawUpdate) { + const parsed = parseBooleanLike(rawUpdate.autoClaimEnabled); + if (parsed == null) return { error: "autoClaimEnabled must be a boolean" }; + next.autoClaimEnabled = parsed; + } + + if ("maxRoundEntries" in rawUpdate) { + const parsed = parseNumberLike(rawUpdate.maxRoundEntries, 1); + if (parsed == null || !Number.isInteger(parsed)) return { error: "maxRoundEntries must be an integer >= 1" }; + next.maxRoundEntries = parsed; + } + + if ("marketHoursOnly" in rawUpdate) { + const parsed = parseBooleanLike(rawUpdate.marketHoursOnly); + if (parsed == null) return { error: "marketHoursOnly must be a boolean" }; + next.marketHoursOnly = parsed; + } + + if ("weekendPause" in rawUpdate) { + const parsed = parseBooleanLike(rawUpdate.weekendPause); + if (parsed == null) return { error: "weekendPause must be a boolean" }; + next.weekendPause = parsed; + } + + return { config: next }; +} + +// strategyConfig is first initialized as an empty shell; the strategy loader fills it after (after initStrategies) +let strategyConfig: StrategyConfig = { + enabled: {}, amount: {}, shares: {}, params: {}, + slippage: parseNumberEnv("ORDER_DEFAULT_SLIPPAGE", 0.06, 0), + autoClaimEnabled: parseBooleanEnv("AUTO_CLAIM_ENABLED", true), + maxRoundEntries: parseNumberEnv("MAX_ROUND_ENTRIES", 1, 1), + marketHoursOnly: parseBooleanEnv("MARKET_HOURS_ONLY", false), + weekendPause: parseBooleanEnv("WEEKEND_PAUSE", false), +}; +function initStrategyConfig(): void { + strategyConfig = createEnvStrategyConfig(); + loadPersistedStrategyConfig(strategyConfig); +} +const pendingTradeMeta = new Map(); + +// -- Manual order config (amount / slippage / TP-SL) persisted to .manual-config.json -- +const MANUAL_CONFIG_FILE = resolve(__dirname, ".manual-config.json"); +interface ManualConfig { + amount: number; // USDC + slippage: number; // 0-0.99 (percentage / 100) + cond: { + tpEnabled: boolean; + tpPct: number; // 0-99 (percentage points) + slEnabled: boolean; + slMode: "price" | "diff"; // price=by fill-price percentage points / diff=by diff crossing threshold + slPct: number; // used when mode=price, 0-99 (percentage points) + slDiff: number; // used when mode=diff, positive number (e.g. 5 means: when buying up triggers at diff<=-5 / when buying down triggers at diff>=+5) + slSlippage: number; // 0-99 + collapsed: boolean; + }; +} +const MANUAL_DEFAULT: ManualConfig = { + amount: 1, + slippage: 0.06, + cond: { tpEnabled: false, tpPct: 20, slEnabled: false, slMode: "price", slPct: 20, slDiff: 5, slSlippage: 15, collapsed: true }, +}; +let manualConfig: ManualConfig = structuredClone(MANUAL_DEFAULT); +function loadManualConfig(): void { + if (!existsSync(MANUAL_CONFIG_FILE)) return; + try { + const raw = JSON.parse(readFileSync(MANUAL_CONFIG_FILE, "utf-8")) as Partial; + if (typeof raw.amount === "number" && raw.amount >= 0) manualConfig.amount = raw.amount; + if (typeof raw.slippage === "number" && raw.slippage >= 0 && raw.slippage <= 1) manualConfig.slippage = raw.slippage; + if (raw.cond && typeof raw.cond === "object") { + const c = raw.cond as Partial; + if (typeof c.tpEnabled === "boolean") manualConfig.cond.tpEnabled = c.tpEnabled; + if (typeof c.tpPct === "number" && c.tpPct >= 0 && c.tpPct <= 99) manualConfig.cond.tpPct = c.tpPct; + if (typeof c.slEnabled === "boolean") manualConfig.cond.slEnabled = c.slEnabled; + if (c.slMode === "price" || c.slMode === "diff") manualConfig.cond.slMode = c.slMode; + if (typeof c.slPct === "number" && c.slPct >= 0 && c.slPct <= 99) manualConfig.cond.slPct = c.slPct; + if (typeof c.slDiff === "number" && c.slDiff >= -200 && c.slDiff <= 200) manualConfig.cond.slDiff = c.slDiff; + if (typeof c.slSlippage === "number" && c.slSlippage >= 0 && c.slSlippage <= 99) manualConfig.cond.slSlippage = c.slSlippage; + if (typeof c.collapsed === "boolean") manualConfig.cond.collapsed = c.collapsed; + } + } catch (err) { + console.warn(`[Manual.Config] load failed: ${err instanceof Error ? err.message : String(err)}`); + } +} +function saveManualConfig(): void { + try { + writeFileSync(MANUAL_CONFIG_FILE, JSON.stringify(manualConfig, null, 2)); + } catch (err) { + console.warn(`[Manual.Config] save failed: ${err instanceof Error ? err.message : String(err)}`); + } +} +loadManualConfig(); + +function applyManualConfigUpdate(raw: unknown): string | null { + if (!isRecord(raw)) return "invalid config format"; + const next = structuredClone(manualConfig); + if ("amount" in raw) { + const v = Number(raw.amount); + if (!Number.isFinite(v) || v < 0) return "amount invalid"; + next.amount = v; + } + if ("slippage" in raw) { + const v = Number(raw.slippage); + if (!Number.isFinite(v) || v < 0 || v > 1) return "slippage must be within 0-1"; + next.slippage = v; + } + if ("cond" in raw && isRecord(raw.cond)) { + const c = raw.cond; + if ("tpEnabled" in c) next.cond.tpEnabled = !!c.tpEnabled; + if ("tpPct" in c) { const v = Number(c.tpPct); if (Number.isFinite(v) && v >= 0 && v <= 99) next.cond.tpPct = v; } + if ("slEnabled" in c) next.cond.slEnabled = !!c.slEnabled; + if ("slMode" in c && (c.slMode === "price" || c.slMode === "diff")) next.cond.slMode = c.slMode; + if ("slPct" in c) { const v = Number(c.slPct); if (Number.isFinite(v) && v >= 0 && v <= 99) next.cond.slPct = v; } + if ("slDiff" in c) { const v = Number(c.slDiff); if (Number.isFinite(v) && v >= -200 && v <= 200) next.cond.slDiff = v; } + if ("slSlippage" in c) { const v = Number(c.slSlippage); if (Number.isFinite(v) && v >= 0 && v <= 99) next.cond.slSlippage = v; } + if ("collapsed" in c) next.cond.collapsed = !!c.collapsed; + } + manualConfig = next; + saveManualConfig(); + return null; +} + +// -- Polymarket real PnL manager ----------------------------------------- +const pmPnlManager = new PmPnlManager(PROXY_ADDRESS); + +/** after a fill lands, only record the strategy source (PnL stats rely on the full refresh every 5 minutes, no longer incremental sync) */ +function notifyTradeForPnl(txHash: string | undefined, source: string | undefined): void { + if (txHash && source) { + pmPnlManager.recordStrategySource(txHash, source); + } +} + +// -- HTTP heartbeat status --------------------------------------- +// the two channels are timed independently, reflecting the real connection-reuse status of the order path: +// - undici: fetch() related (order book query / Gamma API / TG) +// - axios: clob-client place order / query and cancel open orders +const httpHeartbeat = { + latencyMs: -1, // undici (fetch) last heartbeat latency (-1 = not tested/failed) + lastAt: 0, + ok: false, +}; +const axiosHeartbeat = { + latencyMs: -1, // axios (Node native https) last heartbeat latency + lastAt: 0, + ok: false, +}; + +function broadcastHttpHeartbeat(): void { + broadcast("httpHeartbeat", { + latencyMs: httpHeartbeat.latencyMs, + lastAt: httpHeartbeat.lastAt, + ok: httpHeartbeat.ok, + }); +} + +function broadcastAxiosHeartbeat(): void { + broadcast("axiosHeartbeat", { + latencyMs: axiosHeartbeat.latencyMs, + lastAt: axiosHeartbeat.lastAt, + ok: axiosHeartbeat.ok, + }); +} + +function sendHttpHeartbeatToClient(ws: WebSocket): void { + send(ws, "httpHeartbeat", { + latencyMs: httpHeartbeat.latencyMs, + lastAt: httpHeartbeat.lastAt, + ok: httpHeartbeat.ok, + }); + send(ws, "axiosHeartbeat", { + latencyMs: axiosHeartbeat.latencyMs, + lastAt: axiosHeartbeat.lastAt, + ok: axiosHeartbeat.ok, + }); +} + +// -- Order latency monitoring ----------------------------------------- +// the only metric: the moment this machine initiates postOrder -> the moment UserWS receives the MATCHED event for that order +// reflects the real elapsed time from order to match receipt (network uplink + match decision + WS downlink) +// +// difficulty: WS MATCHED sometimes arrives before the HTTP response (WS is faster than HTTP), at which point the orderID is not yet known +// solution: bidirectional cache. Whichever of HTTP and WS arrives first is recorded first, when the other arrives later it is matched and settled +const ORDER_LATENCY_HISTORY_MAX = 10; +const ORDER_LATENCY_TIMEOUT_MS = 15000; + +interface PendingByOrderId { + t0: number; // moment postOrder was initiated + timeoutTimer: ReturnType; +} +interface EarlyWsEntry { + orderId: string; + tWs: number; // moment WS MATCHED arrived +} +interface LatencySample { + ts: number; // moment of completion + latencyMs: number; // order -> WS MATCHED total elapsed time; -1 = timeout +} + +// in-flight timings with known orderID (registered after postOrder returns) +const pendingByOrderId = new Map(); +// WS events that arrived before the HTTP response (cached, looked up by orderID after HTTP returns) +const earlyWsCache = new Map(); + +const latencyHistory: LatencySample[] = []; +const orderLatencyStatus = { + lastMs: -1, // last latency; -1 = not tested + lastAt: 0, + timeouts: 0, // cumulative WS timeouts + wsDisconnects: 0, +}; + +function pushLatencySample(sample: LatencySample): void { + latencyHistory.unshift(sample); + if (latencyHistory.length > ORDER_LATENCY_HISTORY_MAX) latencyHistory.length = ORDER_LATENCY_HISTORY_MAX; +} + +function computeAvg(): number { + const valid = latencyHistory.filter(s => s.latencyMs >= 0); + if (!valid.length) return -1; + return Math.round(valid.reduce((a, b) => a + b.latencyMs, 0) / valid.length); +} + +function buildOrderLatencyPayload(): Record { + return { + lastMs: orderLatencyStatus.lastMs, + lastAt: orderLatencyStatus.lastAt, + avgMs: computeAvg(), + timeouts: orderLatencyStatus.timeouts, + wsDisconnects: orderLatencyStatus.wsDisconnects, + history: latencyHistory.slice(0, ORDER_LATENCY_HISTORY_MAX), + }; +} + +function broadcastOrderLatency(): void { + broadcast("orderLatency", buildOrderLatencyPayload()); +} + +function sendOrderLatencyToClient(ws: WebSocket): void { + send(ws, "orderLatency", buildOrderLatencyPayload()); +} + +function recordLatencySuccess(latencyMs: number, orderId: string): void { + orderLatencyStatus.lastMs = latencyMs; + orderLatencyStatus.lastAt = Date.now(); + pushLatencySample({ ts: Date.now(), latencyMs }); + console.log(`[Trade.Latency] order latency ${latencyMs}ms orderID:${fmtOid(orderId)}`); + broadcastOrderLatency(); +} + +/** + * called after postOrder returns; registers orderID and checks earlyWsCache + * if WS already arrived -> settle immediately; otherwise start a 15s timeout waiting for WS + */ +function registerOrderLatencyStart(orderId: string, t0: number): void { + if (!orderId) return; + // WS already arrived? settle directly + const early = earlyWsCache.get(orderId); + if (early) { + earlyWsCache.delete(orderId); + recordLatencySuccess(early.tWs - t0, orderId); + return; + } + // set timeout + const timeoutTimer = setTimeout(() => { + if (!pendingByOrderId.has(orderId)) return; + pendingByOrderId.delete(orderId); + orderLatencyStatus.timeouts++; + orderLatencyStatus.lastMs = -1; + orderLatencyStatus.lastAt = Date.now(); + pushLatencySample({ ts: Date.now(), latencyMs: -1 }); + console.warn(`[Trade.Latency] orderID=${fmtOid(orderId)} WS MATCHED timeout (>${ORDER_LATENCY_TIMEOUT_MS}ms)`); + broadcastOrderLatency(); + }, ORDER_LATENCY_TIMEOUT_MS); + pendingByOrderId.set(orderId, { t0, timeoutTimer }); +} + +/** + * called when UserWS receives a MATCHED event + * if HTTP already returned and registered -> settle; otherwise cache in earlyWsCache waiting for HTTP return + */ +function onWsMatched(orderId: string | undefined): void { + if (!orderId) return; + const pending = pendingByOrderId.get(orderId); + if (pending) { + clearTimeout(pending.timeoutTimer); + pendingByOrderId.delete(orderId); + recordLatencySuccess(Date.now() - pending.t0, orderId); + return; + } + // WS arrived before HTTP, cache it (EarlyWsCache also set to expire in 15s) + earlyWsCache.set(orderId, { orderId, tWs: Date.now() }); + setTimeout(() => earlyWsCache.delete(orderId), ORDER_LATENCY_TIMEOUT_MS); +} + +function invalidatePendingLatencyOnWsDisconnect(): void { + const total = pendingByOrderId.size + earlyWsCache.size; + if (!total) return; + for (const p of pendingByOrderId.values()) clearTimeout(p.timeoutTimer); + pendingByOrderId.clear(); + earlyWsCache.clear(); + orderLatencyStatus.wsDisconnects += total; + console.warn(`[Trade.Latency] UserWS disconnected, discarding ${total} in-flight timings`); + broadcastOrderLatency(); +} + +// -- Conditional orders (TP GTC limit resting order + SL local monitoring) -------------- +// all state lives in memory; lost on service restart - GTC resting orders stay on Polymarket's side, +// after restart the frontend list will be empty (invisible), but it does not affect the fill of the real resting orders. +// on window switch all GTC are automatically cancelled and the SL list is cleared. +type CondKind = "tp" | "sl" | "sl-diff"; +type CondStatus = "open" | "filled" | "canceled" | "triggered" | "failed"; + +interface ConditionOrder { + id: string; // internal ID (uuid-ish) + groupId: string; // shared by the TP+SL produced by the same buy; used to clean up the same group when one triggers + kind: CondKind; // tp=TP limit resting order sl=SL local monitoring + direction: StrategyDirection; // position direction + assetId: string; // the asset of this position + windowStart: number; // the window it belongs to + createdAt: number; + size: number; // original shares + remainingSize: number; // match-layer unfilled shares (decremented by MATCHED push; display only, not used for fill decision) + chainFilledSize?: number; // on-chain confirmed filled shares (accumulated by MINED push; only counts as a real fill when it reaches zero) + entryPrice: number; // buy fill price (the baseline relative to delta) + triggerPrice: number; // trigger price (TP=resting price; SL=the probability that triggers closing) + slippage?: number; // SL only + status: CondStatus; + // TP only: the real Polymarket orderID (filled after resting order succeeds) + polymarketOrderId?: string; + pendingGtc?: boolean; // GTC resting order request in progress, local trigger forbidden during tick + failReason?: string; +} + +const activeConditionOrders = new Map(); + +function makeCondId(): string { + return `c${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`; +} + +function clampPrice(p: number): number { + return Math.min(0.99, Math.max(0.01, p)); +} + +function serializeConditionOrders(): ConditionOrder[] { + return [...activeConditionOrders.values()].sort((a, b) => a.createdAt - b.createdAt); +} + +function broadcastConditionOrders(): void { + broadcast("conditionOrders", { list: serializeConditionOrders() }); +} + +function sendConditionOrdersToClient(ws: WebSocket): void { + send(ws, "conditionOrders", { list: serializeConditionOrders() }); +} + +/** + * place a GTC limit sell order on Polymarket as take-profit + * returns: { orderID } on success | { error: string } on failure (passes through the Polymarket error message directly) + */ +async function placeTakeProfitOrder( + assetId: string, + sellSize: number, + targetPrice: number, +): Promise<{ orderID?: string; error?: string }> { + if (!clobClient) return { error: "Clob not initialized" }; + try { + const tickSize = getCachedTickSize(assetId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedSize = floorToDecimals(sellSize, 2); + const normalizedPrice = floorToDecimals(targetPrice, priceDecimals); + if (normalizedSize <= 0 || normalizedPrice <= 0 || normalizedPrice >= 1) { + return { error: `invalid params size=${normalizedSize} price=${normalizedPrice}` }; + } + const tpArgs = { tokenID: assetId, side: Side.SELL, price: normalizedPrice, size: normalizedSize }; + const tpOpts = { tickSize, negRisk: false }; + console.log("[RAW]", `[Cond.TP] createOrder args:`, JSON.stringify(tpArgs), "opts:", JSON.stringify(tpOpts), "ctx:", JSON.stringify({ assetId, sellSize, targetPrice, normalizedSize, normalizedPrice, tickSize, priceDecimals })); + const signed = await clobClient.createOrder(tpArgs, tpOpts); + console.log("[RAW]", `[Cond.TP] signedOrder:`, JSON.stringify(signed)); + const tpT0 = Date.now(); + const result = await clobClient.postOrder(signed, OrderType.GTC); + const tpHttpMs = Date.now() - tpT0; + console.log("[RAW]", `[Cond.TP] postOrder GTC raw result:`, JSON.stringify(result), `HTTP:${tpHttpMs}ms`); + const orderID = typeof result?.orderID === "string" ? result.orderID : ""; + if (!orderID) { + const errMsg = typeof result?.error === "string" ? result.error : "response has no orderID"; + return { error: errMsg }; + } + console.log(`[Cond.TP] ➕ resting order sell ${normalizedSize}@${normalizedPrice} orderID=${fmtOid(orderID)}`); + registerOrderConfirm(orderID, "cond-tp", { side: "sell", size: normalizedSize, price: normalizedPrice }); + return { orderID }; + } catch (err) { + return { error: err instanceof Error ? err.message : String(err) }; + } +} + +/** + * cancel an already-placed take-profit GTC order + */ +async function cancelTakeProfitOrder(polymarketOrderId: string): Promise { + if (!clobClient || !polymarketOrderId) return false; + registerCancelConfirm(polymarketOrderId, "cond-tp"); + for (let attempt = 1; attempt <= 2; attempt++) { + const t0 = Date.now(); + console.log("[RAW]", `[Cond.TP] cancelOrder args:`, JSON.stringify({ orderID: polymarketOrderId }), `attempt:${attempt}/2`); + try { + const result = await clobClient.cancelOrder({ orderID: polymarketOrderId }); + const dt = Date.now() - t0; + console.log("[RAW]", `[Cond.TP] cancelOrder raw result:`, JSON.stringify(result), `HTTP:${dt}ms attempt:${attempt}`); + const canceledList = Array.isArray((result as any)?.canceled) ? (result as any).canceled as string[] : []; + const notCanceled = (result as any)?.not_canceled; + const ncReason = notCanceled && typeof notCanceled === "object" ? String((notCanceled as Record)[polymarketOrderId] ?? "") : ""; + if (canceledList.includes(polymarketOrderId)) { + console.log(`[Cond.TP] cancel resting order orderID=${fmtOid(polymarketOrderId)}${attempt > 1 ? ` (retry succeeded)` : ""}`); + return true; + } + if (ncReason) { + if (/filled|matched|not.?found|does not exist|already/i.test(ncReason)) { + console.log(`[Cond.TP] orderID=${fmtOid(polymarketOrderId)} not_canceled treated as handled: ${ncReason}`); + resolveCancelConfirm(polymarketOrderId, "HTTP not_canceled soft success"); + return true; + } + console.warn(`[Cond.TP] not_canceled retry (attempt ${attempt}/2): ${ncReason}`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const dt = Date.now() - t0; + console.log("[RAW]", `[Cond.TP] cancelOrder threw attempt:${attempt} HTTP:${dt}ms err:`, msg); + if (/not found|does not exist|already/i.test(msg)) { + console.log(`[Cond.TP] cancel orderID=${fmtOid(polymarketOrderId)} order no longer exists, treated as handled`); + resolveCancelConfirm(polymarketOrderId, "HTTP threw not found"); + return true; + } + console.warn(`[Cond.TP] cancel failed (attempt ${attempt}/2):`, msg); + } + if (attempt < 2) await new Promise(r => setTimeout(r, 500)); + } + return false; +} + +/** clean up all active conditional orders for the given direction (called after a sell fills) */ +async function clearConditionOrdersForDirection( + assetId: string, + direction: StrategyDirection, + reason: string, +): Promise { + const toClean: ConditionOrder[] = []; + for (const cond of activeConditionOrders.values()) { + if (cond.direction !== direction) continue; + if (cond.assetId !== assetId) continue; // do not clean same-direction in a different window (different assetId) + toClean.push(cond); + } + for (const cond of toClean) { + if (cond.kind === "tp" && cond.polymarketOrderId) { + // asynchronously cancel the Polymarket GTC, non-blocking + void cancelTakeProfitOrder(cond.polymarketOrderId); + } + activeConditionOrders.delete(cond.id); + console.log(`[Cond] cleaned up ${cond.kind === "tp" ? "TP" : "SL"} ${direction} (${reason})`); + } + broadcastConditionOrders(); +} + +/** clean up the other conditional orders in the same group (the TP+SL from one buy share a groupId). + * when TP triggers, use excludeId=the triggered TP, to avoid cancelling itself (a filled one cannot be cancelled). */ +async function clearConditionOrdersByGroup( + groupId: string, + excludeId: string | null, + reason: string, +): Promise { + if (!groupId) { + broadcastConditionOrders(); + return; + } + const toClean: ConditionOrder[] = []; + for (const cond of activeConditionOrders.values()) { + if (cond.groupId !== groupId) continue; + if (excludeId && cond.id === excludeId) continue; + toClean.push(cond); + } + for (const cond of toClean) { + if (cond.kind === "tp" && cond.polymarketOrderId) { + void cancelTakeProfitOrder(cond.polymarketOrderId); + } + activeConditionOrders.delete(cond.id); + console.log(`[Cond] cleaned up same group ${cond.kind === "tp" ? "TP" : "SL"} ${cond.direction} (${reason})`); + } + broadcastConditionOrders(); +} + +/** after buy MINED, create conditional orders (TP GTC + SL local record) */ +async function createConditionOrdersAfterFill(opts: { + direction: StrategyDirection; + assetId: string; + entryPrice: number; + filledSize: number; + windowStart: number; + stopProfit?: { pctDelta?: number; targetPrice?: number }; + stopLoss?: { pctDelta?: number; diffValue?: number; slippage?: number }; +}): Promise { + const { direction, assetId, entryPrice, filledSize, windowStart, stopProfit, stopLoss } = opts; + if (!stopProfit && !stopLoss) return; + + // shared slippage for market orders (SL + fallback): prefer the SL setting, otherwise default 15% + const marketSlippage = stopLoss?.slippage ?? 0.15; + // the TP+SL produced by the same buy share one groupId; when either triggers only the same group is cleaned up + const groupId = makeCondId(); + + // TP GTC: supports two modes - pctDelta (relative to entry price) / targetPrice (absolute price 0~1) + const tpTargetPrice = stopProfit + ? (stopProfit.targetPrice != null && stopProfit.targetPrice > 0 + ? clampPrice(stopProfit.targetPrice) + : (stopProfit.pctDelta != null && stopProfit.pctDelta > 0 + ? clampPrice(entryPrice + stopProfit.pctDelta) + : null)) + : null; + if (tpTargetPrice != null) { + const targetPrice = tpTargetPrice; + const cond: ConditionOrder = { + id: makeCondId(), + groupId, + kind: "tp", + direction, + assetId, + windowStart, + createdAt: Date.now(), + size: filledSize, + remainingSize: filledSize, + entryPrice, + triggerPrice: targetPrice, + status: "open", + }; + activeConditionOrders.set(cond.id, cond); + broadcastConditionOrders(); + // GTC minimum size is 5 shares; if insufficient, go straight to local monitoring + const gtcSize = floorToDecimals(filledSize, 2); + if (gtcSize < 5) { + console.log(`[Cond.TP] size ${gtcSize} < 5 (GTC minimum), skip limit and go straight to local monitoring`); + cond.failReason = `size below 5 shares -> local monitoring`; + broadcastConditionOrders(); + } else { + // async resting order: first mark pendingGtc, to prevent tick from falsely triggering local market TP during resting + cond.pendingGtc = true; + void (async () => { + const r = await placeTakeProfitOrder(assetId, filledSize, targetPrice); + const latest = activeConditionOrders.get(cond.id); + if (!latest) return; // already cancelled + latest.pendingGtc = false; + if (r.orderID) { + latest.polymarketOrderId = r.orderID; + // write pendingTradeMeta: used for MINED side=sell reverse lookup of source=cond-tp, tracking the real on-chain fill + rememberPendingTradeMeta({ + orderId: r.orderID, + ts: Date.now(), + windowStart: latest.windowStart, + side: "sell", + direction: latest.direction, + amount: filledSize, + worstPrice: targetPrice, + source: "cond-tp", + }); + // placeTakeProfitOrder already logged "➕ resting order sell" internally, do not repeat here + broadcastConditionOrders(); + } else { + // GTC resting order failed -> downgrade to local-monitoring market TP + console.warn(`[Cond.TP] TP resting order failed, downgrading to local monitoring (size=${filledSize} price=${targetPrice}): ${r.error}`); + latest.failReason = `limit failed -> local monitoring: ${r.error || "resting order failed"}`; + broadcastConditionOrders(); + } + })(); + } // end else (size >= 5) + } + + // SL (local monitoring) + // the two modes are mutually exclusive: + // - price mode: by fill-price percentage points (pctDelta), monitor odds dropping below entryPrice - pctDelta to trigger + // - diff mode: by diff crossing (diffValue positive X), when buying up trigger at diff <= -X, when buying down trigger at diff >= +X + if (stopLoss) { + if (typeof stopLoss.pctDelta === "number" && stopLoss.pctDelta > 0) { + const triggerPrice = clampPrice(entryPrice - stopLoss.pctDelta); + const cond: ConditionOrder = { + id: makeCondId(), + groupId, + kind: "sl", + direction, assetId, windowStart, + createdAt: Date.now(), + size: filledSize, remainingSize: filledSize, + entryPrice, triggerPrice, + slippage: marketSlippage, + status: "open", + }; + activeConditionOrders.set(cond.id, cond); + broadcastConditionOrders(); + } else if (typeof stopLoss.diffValue === "number" && Number.isFinite(stopLoss.diffValue)) { + // diffValue is the absolute threshold entered by the user (can be positive or negative) + // buying up: trigger at diff <= triggerPrice; buying down: trigger at diff >= -triggerPrice (symmetric) + const cond: ConditionOrder = { + id: makeCondId(), + groupId, + kind: "sl-diff", + direction, assetId, windowStart, + createdAt: Date.now(), + size: filledSize, remainingSize: filledSize, + entryPrice, + triggerPrice: stopLoss.diffValue, + slippage: marketSlippage, + status: "open", + }; + activeConditionOrders.set(cond.id, cond); + broadcastConditionOrders(); + } + } +} + +// -- Conditional order tick monitoring -------------------------------------- +// TP: when there is no polymarketOrderId (GTC failed, downgraded) monitor locally, trigger a market sell when the probability reaches the target price +// SL: always monitored locally, market sell when the probability drops below the trigger price +let condTriggerInFlight = false; // prevent concurrent triggers + +async function checkConditionOrdersTick(upPct: number, dnPct: number, diff: number | null): Promise { + if (condTriggerInFlight) return; + const toTrigger: ConditionOrder[] = []; + for (const cond of activeConditionOrders.values()) { + if (cond.status !== "open") continue; + // TP: only monitor downgraded TP orders (no GTC resting order and not in the middle of resting) + if (cond.kind === "tp" && !cond.polymarketOrderId && !cond.pendingGtc) { + const currentPct = (cond.direction === "up" ? upPct : dnPct) / 100; + if (currentPct >= cond.triggerPrice) toTrigger.push(cond); + } + // SL: always monitored locally + if (cond.kind === "sl") { + const currentPct = (cond.direction === "up" ? upPct : dnPct) / 100; + if (currentPct <= cond.triggerPrice) toTrigger.push(cond); + } + // SL-DIFF: based on the diff absolute threshold (triggerPrice stores the user-entered value, can be positive or negative) + // buying up: trigger at diff <= triggerPrice + // buying down: trigger at diff >= -triggerPrice (symmetric) + if (cond.kind === "sl-diff" && diff != null) { + const T = cond.triggerPrice; + const triggered = cond.direction === "up" ? diff <= T : diff >= -T; + if (triggered) toTrigger.push(cond); + } + } + if (!toTrigger.length) return; + + condTriggerInFlight = true; + try { + for (const cond of toTrigger) { + const latest = activeConditionOrders.get(cond.id); + if (!latest || latest.status !== "open") continue; + const kindZh = cond.kind === "tp" ? "TP" : "SL"; + const currentPct = (cond.direction === "up" ? upPct : dnPct) / 100; + if (cond.kind === "sl-diff") { + const T = cond.triggerPrice; + const want = cond.direction === "up" ? `≤${T}` : `≥${-T}`; + console.log(`[Cond.SL] SL triggered ${cond.direction} diff=${diff?.toFixed(1) ?? "?"} threshold ${want} size=${cond.size}`); + } else { + console.log(`[Cond.${cond.kind}] ${kindZh} triggered ${cond.direction} pct=${(currentPct * 100).toFixed(1)}% triggerPrice=${(cond.triggerPrice * 100).toFixed(1)}% size=${cond.size}`); + } + latest.status = "triggered"; + broadcastConditionOrders(); + // sell shares: + // sl-diff (crossing SL) -> fully clear all positions in that direction (reverse hard signal, leave nothing) + // tp / sl -> use this order's remainingSize (and take the min with actual position, to prevent overselling) + const actualSize = getDirectionLocalSize(cond.direction); + let sellSize: number; + if (cond.kind === "sl-diff") { + sellSize = actualSize; + console.log(`[Cond.SL] fully clear ${cond.direction} position size=${actualSize.toFixed(4)}`); + } else { + sellSize = actualSize > 0.01 && actualSize < cond.remainingSize + ? actualSize + : cond.remainingSize; + if (sellSize !== cond.remainingSize) { + console.log(`[Cond.${cond.kind}] actual position ${actualSize.toFixed(4)} < remainingSize ${cond.remainingSize.toFixed(4)}, using actual position`); + } + } + if (sellSize <= 0.01) { + console.log(`[Cond.${cond.kind}] actual position ${actualSize.toFixed(4)} is empty, skip`); + activeConditionOrders.delete(cond.id); + broadcastConditionOrders(); + continue; + } + const result = await placeOrder({ + direction: cond.direction, + side: "sell", + amount: sellSize, + slippage: cond.slippage ?? marketSlippageDefault(), + source: `cond-${cond.kind}-trigger`, + exitReason: cond.kind === "sl-diff" + ? `crossing SL triggered diff${cond.direction === "up" ? "<=" : ">="}${cond.direction === "up" ? cond.triggerPrice : -cond.triggerPrice}` + : `${kindZh} triggered target ${(cond.triggerPrice * 100).toFixed(1)}%`, + }); + if (result.success) { + console.log(`[Cond.${cond.kind}] ${kindZh} market sell succeeded size=${sellSize.toFixed(4)}`); + activeConditionOrders.delete(cond.id); + if (cond.kind === "sl-diff") { + // crossing SL fully cleared all positions in that direction, other same-direction conditional orders (tp/sl) must also be cleared + void clearConditionOrdersForDirection(cond.assetId, cond.direction, "crossing SL full clear"); + } else { + // clean up the other side of the same group (when SL triggers clear TP, when downgraded TP triggers clear SL) + void clearConditionOrdersByGroup(cond.groupId, cond.id, `${kindZh} triggered`); + } + // broadcast a "filled" notification to the frontend (same message type as TP GTC fill) + broadcast("condFilled", { kind: cond.kind, direction: cond.direction, price: cond.triggerPrice }); + } else { + latest.status = "failed"; + latest.failReason = `${kindZh} market sell failed: ${result.errorMessage}`; + console.error(`[Cond.${cond.kind}] ${kindZh} market sell failed: ${result.errorMessage}`); + } + broadcastConditionOrders(); + } + } finally { + condTriggerInFlight = false; + } +} + +function marketSlippageDefault(): number { + return 0.15; +} + +// -- Telegram push --------------------------------------- +let tgConfig: TgConfig = loadTgConfig(); +let tgPushTimer: ReturnType | null = null; + +/** returns the timestamp of midnight UTC+8 today (milliseconds) */ +function getCstDayStartMs(now = Date.now()): number { + const CST_OFFSET_MS = 8 * 60 * 60 * 1000; + const msSinceCstMidnight = (now + CST_OFFSET_MS) % (24 * 60 * 60 * 1000); + return now - msSinceCstMidnight; +} + +/** returns the UTC+8 date string, format YYYY-MM-DD */ +function getCstDateStr(now = Date.now()): string { + const CST_OFFSET_MS = 8 * 60 * 60 * 1000; + return new Date(now + CST_OFFSET_MS).toISOString().slice(0, 10); +} + +function buildTgMessage(): string { + const now = new Date(); + const ts = now.toLocaleString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false }); + // balance + const usdcBalance = positions.usdc != null ? `$${positions.usdc.toFixed(2)}` : "—"; + // all history + today (uniformly via computeSnapshot, ensuring consistency with frontend panel/monitor page/TG) + const today = pmPnlManager.getTotalPnl(0); // grand total (all history, only used for summary info display) + const todaySec = Math.floor(getCstDayStartMs() / 1000); + const todaySnap = pmPnlManager.computeSnapshot(todaySec); + const todayNet = todaySnap.netPnl; + const todayCount = todaySnap.positions; + const wins = todaySnap.wins; + const winRate = todaySnap.closedPositions > 0 + ? `${(wins / todaySnap.closedPositions * 100).toFixed(1)}%` + : "—"; + // strategy status + const stratLines = ALL_STRATEGY_KEYS + .map(k => `${k}${strategyConfig.enabled[k] ? "✅" : "❌"}`) + .filter(s => s.endsWith("✅")) + .join(" ") || "all off"; + // HTTP latency + const httpStatus = httpHeartbeat.ok + ? `${httpHeartbeat.latencyMs}ms ${httpHeartbeat.latencyMs < 500 ? "✅" : httpHeartbeat.latencyMs < 1000 ? "⚠️" : "❌"}` + : "down"; + // last 5 (excluding buys: only settlement-type events, PnL is more meaningful) + const recent = pmPnlManager.getEvents({ sinceDays: 7, limit: 20 }) + .filter(e => e.kind !== "BUY") + .slice(0, 5); + const recentLines = recent.map(e => { + const t = new Date(e.ts * 1000).toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false, hour: "2-digit", minute: "2-digit" }); + const kindZh = e.kind === "SELL" ? "sell" : e.kind === "REDEEM" ? "claim" : "zeroed"; + const outZh = e.outcome === "Up" ? "Up▲" : "Down▼"; + // source display: manual -> "manual"; strategyp3 -> "P3"; others keep original + const src = !e.strategySource + ? "—" + : /^manual$/i.test(e.strategySource) + ? "manual" + : e.strategySource.replace(/^strategy/i, "").toUpperCase(); + const posPnl = e.positionPnl != null + ? `${e.positionPnl >= 0 ? "+" : ""}$${e.positionPnl.toFixed(2)}` + : "—"; + return `${t} ${kindZh} ${outZh} ${src} ${posPnl}`; + }).join("\n"); + + return [ + `📊 ${activeMarket.displayName}${accountName ? ` · 👤 ${accountName}` : ""} (${ts})`, + `─────────────────`, + `💰 Balance: ${usdcBalance} USDC`, + `📈 Today: ${todayNet >= 0 ? "+" : ""}$${todayNet.toFixed(2)} (${todayCount} trades, win ${winRate})`, + `💸 Fees: $${todaySnap.totalFee.toFixed(2)}`, + `⚙️ Strategies: ${stratLines}`, + `🌐 HTTP: ${httpStatus}`, + ``, + `━━━ Last 5 ━━━`, + recentLines || "(none)", + ].join("\n"); +} + +function startTgPushLoop(): void { + if (tgPushTimer) { + clearInterval(tgPushTimer); + tgPushTimer = null; + } + if (!tgConfig.enabled || !tgConfig.scheduledEnabled || !tgConfig.botToken || !tgConfig.chatId) return; + const intervalMs = Math.max(5, tgConfig.intervalMinutes) * 60 * 1000; + console.log(`[Push] scheduled push started, every ${tgConfig.intervalMinutes} minutes`); + tgPushTimer = setInterval(async () => { + const text = buildTgMessage(); + const result = await sendTgMessage(tgConfig, text); + if (result.ok) { + console.log(`[Push] scheduled push succeeded`); + } else { + console.warn(`[Push] scheduled push failed: ${result.error}`); + } + }, intervalMs); +} + +// -- Post-trade delayed push (10 minutes after the last buy MINED) ---------------- +// single slot: each new buy cancels the previously queued push and restarts the timer. +// purpose: when placing multiple orders in a row, push one summary only at "the last order"+10 minutes. +const POST_TRADE_PUSH_DELAY_MS = 10 * 60 * 1000; +let postTradePushTimer: ReturnType | null = null; +let postTradePushCount = 0; // cumulative order count this window (used for the push header display) +let postTradePushFirstTs = 0; + +function schedulePostTradePush(meta: { direction: StrategyDirection; size: number; price: number; source: string }): void { + if (!tgConfig.enabled || !tgConfig.postTradeEnabled || !tgConfig.botToken || !tgConfig.chatId) return; + const dirZh = meta.direction === "up" ? "up" : "down"; + // cancel the previously queued push (if any), restart the timer + const replaced = postTradePushTimer != null; + if (postTradePushTimer) { + clearTimeout(postTradePushTimer); + postTradePushTimer = null; + } else { + postTradePushCount = 0; + postTradePushFirstTs = Date.now(); + } + postTradePushCount++; + console.log(`[Push] post-trade push ${replaced ? "rescheduled" : "queued"} (in ${POST_TRADE_PUSH_DELAY_MS / 60000} minutes): ${meta.source} buy ${dirZh} ${meta.size}@${meta.price} (cumulative ${postTradePushCount} trades)`); + const snapshotCount = postTradePushCount; + const snapshotFirstTs = postTradePushFirstTs; + postTradePushTimer = setTimeout(async () => { + postTradePushTimer = null; + const finalCount = postTradePushCount; + postTradePushCount = 0; + postTradePushFirstTs = 0; + if (!tgConfig.enabled || !tgConfig.postTradeEnabled || !tgConfig.botToken || !tgConfig.chatId) return; + const minsSinceFirst = Math.round((Date.now() - snapshotFirstTs) / 60000); + const header = finalCount > 1 + ? `📌 Post-trade ${POST_TRADE_PUSH_DELAY_MS / 60000}-minute review (last ${meta.source} buy ${dirZh} ${meta.size}@${meta.price.toFixed(3)}, ${finalCount} trades total, span ${minsSinceFirst} minutes)\n\n` + : `📌 Post-trade ${POST_TRADE_PUSH_DELAY_MS / 60000}-minute review (${meta.source} buy ${dirZh} ${meta.size}@${meta.price.toFixed(3)})\n\n`; + const text = header + buildTgMessage(); + const result = await sendTgMessage(tgConfig, text); + if (result.ok) { + console.log(`[Push] post-trade push succeeded (${finalCount} trades, snapshot=${snapshotCount})`); + } else { + console.warn(`[Push] post-trade push failed: ${result.error}`); + } + }, POST_TRADE_PUSH_DELAY_MS); +} + +function broadcastTgConfig(): void { + // when sending config to the frontend, hide the second half of botToken (to avoid leakage via screenshots) + const masked = tgConfig.botToken + ? tgConfig.botToken.slice(0, 10) + "***" + tgConfig.botToken.slice(-4) + : ""; + broadcast("tgConfig", { + enabled: tgConfig.enabled, + botToken: masked, + botTokenSet: !!tgConfig.botToken, + chatId: tgConfig.chatId, + intervalMinutes: tgConfig.intervalMinutes, + scheduledEnabled: tgConfig.scheduledEnabled, + postTradeEnabled: tgConfig.postTradeEnabled, + }); +} + +function sendTgConfigToClient(ws: WebSocket): void { + const masked = tgConfig.botToken + ? tgConfig.botToken.slice(0, 10) + "***" + tgConfig.botToken.slice(-4) + : ""; + send(ws, "tgConfig", { + enabled: tgConfig.enabled, + botToken: masked, + botTokenSet: !!tgConfig.botToken, + chatId: tgConfig.chatId, + intervalMinutes: tgConfig.intervalMinutes, + scheduledEnabled: tgConfig.scheduledEnabled, + postTradeEnabled: tgConfig.postTradeEnabled, + }); +} + +// full refresh cadence: once every 5 minutes +const PMPNL_REFRESH_INTERVAL_MS = 5 * 60 * 1000; +let pmPnlNextRefreshAt = 0; + +function buildPmPnlPayload(): Record { + const events = pmPnlManager.getEvents(); + const total = pmPnlManager.getTotalPnl(); + // today's snapshot (CST cutoff) -- same convention as server.ts, monitor page, TG + const todaySec = Math.floor(getCstDayStartMs() / 1000); + const today = pmPnlManager.computeSnapshot(todaySec); + return { + events, total, today, + initialized: pmPnlManager.isInitialized(), + lastRefreshAt: pmPnlManager.getLastRefreshAt(), + nextRefreshAt: pmPnlNextRefreshAt, + }; +} + +function broadcastPmPnl(): void { + broadcast("pmPnl", buildPmPnlPayload()); +} + +function sendPmPnlToClient(ws: WebSocket): void { + send(ws, "pmPnl", buildPmPnlPayload()); +} + +function cleanupPendingTradeMeta(now = Date.now()): void { + for (const [key, meta] of pendingTradeMeta) { + if (now - meta.ts > PENDING_TRADE_META_MAX_AGE_MS) { + pendingTradeMeta.delete(key); + } + } +} + +function rememberPendingTradeMeta(meta: Omit): void { + cleanupPendingTradeMeta(meta.ts); + const key = meta.orderId || `pending-${meta.ts}-${Math.random().toString(36).slice(2, 8)}`; + pendingTradeMeta.set(key, { key, ...meta }); +} + +/** called when a stratOrder is deleted, cleans up the corresponding pendingTradeMeta (fix A no longer "consume once", needs to be cleared at the end of the order lifecycle) */ +function clearPendingTradeMetaByOrderId(orderId: string | undefined): void { + if (orderId) pendingTradeMeta.delete(orderId); +} + +function normalizeTradeSide(value: unknown): "buy" | "sell" | null { + if (typeof value !== "string") return null; + const normalized = value.trim().toLowerCase(); + if (normalized === "buy") return "buy"; + if (normalized === "sell") return "sell"; + return null; +} + +function getDirectionByAssetId(assetId: string): StrategyDirection | null { + if (assetId === state.upTokenId) return "up"; + if (assetId === state.downTokenId) return "down"; + return null; +} + +// token_id -> symbol reverse lookup (used in logs to show which symbol's fill, also accurate across symbol switches) +const tokenSymbolMap = new Map(); +function rememberTokenSymbol(tokenId: string, sym: MarketSymbol): void { + if (tokenId) tokenSymbolMap.set(tokenId, sym); +} +function getSymbolByAssetId(assetId: string): string { + return tokenSymbolMap.get(assetId) || activeMarket.symbol; +} + +function parseTradeEventTimestamp(evt: Record): number { + const raw = typeof evt.match_time === "string" + ? evt.match_time + : typeof evt.last_update === "string" + ? evt.last_update + : ""; + const parsed = raw ? Date.parse(raw) : NaN; + return Number.isFinite(parsed) ? parsed : Date.now(); +} + +function consumePendingTradeMeta(evt: Record): PendingTradeMeta | null { + cleanupPendingTradeMeta(); + const candidateIds: string[] = []; + if (typeof evt.taker_order_id === "string" && evt.taker_order_id) { + candidateIds.push(evt.taker_order_id); + } + if (Array.isArray(evt.maker_orders)) { + for (const makerOrder of evt.maker_orders) { + if (!isRecord(makerOrder) || typeof makerOrder.order_id !== "string" || !makerOrder.order_id) continue; + candidateIds.push(makerOrder.order_id); + } + } + for (const id of candidateIds) { + const meta = pendingTradeMeta.get(id); + if (!meta) continue; + pendingTradeMeta.delete(id); + return meta; + } + + const side = normalizeTradeSide(evt.side); + const assetId = typeof evt.asset_id === "string" ? evt.asset_id : ""; + const size = typeof evt.size === "number" ? evt.size : Number(evt.size); + if (!side || !assetId || !Number.isFinite(size)) return null; + + // fuzzy match: only consider it our order if the size diff is within 10% and the time diff is within 30 seconds + const FUZZY_MAX_AGE_MS = 30_000; + const FUZZY_MAX_SIZE_RATIO = 0.10; + let bestKey: string | null = null; + let bestScore = Number.POSITIVE_INFINITY; + const now = Date.now(); + for (const [key, meta] of pendingTradeMeta) { + const directionTokenId = meta.direction === "up" ? state.upTokenId : state.downTokenId; + if (directionTokenId !== assetId || meta.side !== side) continue; + const ageDiff = now - meta.ts; + if (ageDiff > FUZZY_MAX_AGE_MS) continue; + const sizeRatio = size > 0 ? Math.abs(meta.amount - size) / size : Math.abs(meta.amount - size); + if (sizeRatio > FUZZY_MAX_SIZE_RATIO) continue; + const score = sizeRatio * 1000 + ageDiff / 1000; + if (score < bestScore) { + bestScore = score; + bestKey = key; + } + } + if (!bestKey) return null; + const meta = pendingTradeMeta.get(bestKey) || null; + if (meta) pendingTradeMeta.delete(bestKey); + return meta; +} + +// -- Polymarket authentication ----------------------------------------- +const CREDS_FILE = resolve(__dirname, ".polymarket-creds.json"); + +interface PolymarketCreds { + key: string; secret: string; passphrase: string; address: string; + accountName?: string; + walletType?: "safe" | "deposit"; // safe=old Gnosis Safe (POLY_GNOSIS_SAFE), deposit=new Deposit Wallet (POLY_1271) + proxyAddress?: string; // the PROXY_ADDRESS the creds were created for; re-detect if env changed +} + +/** detect via EIP-1967 implementation slot whether PROXY is a new deposit wallet or an old Gnosis Safe + * old Safe: no EIP-1967 proxy structure -> slot all 0 + * new Deposit: ERC-1967 proxy -> slot stores the implementation address + * on network failure / exception default to "deposit" (new wallet - PM's main direction going forward; misjudging an old wallet will immediately fail orders so it is detectable) + */ +async function detectWalletType(addr: string): Promise<"safe" | "deposit"> { + if (!addr) return "deposit"; + const RPC_URLS = ["https://polygon.drpc.org", "https://polygon-bor-rpc.publicnode.com"]; + const IMPL_SLOT = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; + for (const url of RPC_URLS) { + try { + const r = await fetch(url, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_getStorageAt", params: [addr, IMPL_SLOT, "latest"] }), + }); + const j = await r.json() as any; + if (j.error) throw new Error(j.error.message); + const slot = String(j.result || ""); + const implIsZero = /^0x0+$/.test(slot); + return implIsZero ? "safe" : "deposit"; + } catch { + // try the next RPC + } + } + console.warn("[System.Auth] wallet type detection: all RPCs failed, defaulting to deposit (new wallet)"); + return "deposit"; +} + +function adaptSigner(wallet: ethers.Wallet) { + return { + _signTypedData: ( + domain: Record, + types: Record, + value: Record + ) => wallet.signTypedData( + domain as ethers.TypedDataDomain, + types as Record, + value + ), + getAddress: () => Promise.resolve(wallet.address), + }; +} + +function loadCreds(): PolymarketCreds | null { + if (!existsSync(CREDS_FILE)) return null; + try { + const creds: PolymarketCreds = JSON.parse(readFileSync(CREDS_FILE, "utf-8")); + if (creds.key && creds.secret && creds.passphrase) { + if (creds.accountName) accountName = creds.accountName; + return creds; + } + } catch { /* ignore */ } + return null; +} + +function saveAccountNameToCreds(name: string): void { + if (!name || !existsSync(CREDS_FILE)) return; + try { + const creds = JSON.parse(readFileSync(CREDS_FILE, "utf-8")) as PolymarketCreds; + if (creds.accountName === name) return; + creds.accountName = name; + writeFileSync(CREDS_FILE, JSON.stringify(creds, null, 2)); + } catch { /* ignore */ } +} + +function walletTypeToSigType(walletType: "safe" | "deposit" | undefined): SignatureType { + if (!PROXY_ADDRESS) return SignatureType.EOA; + if (walletType === "deposit") return SignatureType.POLY_1271; + return SignatureType.POLY_GNOSIS_SAFE; // safe or undefined (fallback to old logic) +} + +/** decide which wallet type to use for this startup: + * 1. creds has walletType and proxyAddress matches the current PROXY_ADDRESS -> use directly + * 2. otherwise call on-chain RPC detection -> write into creds + * on failure default to "deposit" (new wallet, PM's main direction) + */ +async function resolveWalletType(saved: PolymarketCreds | null): Promise<"safe" | "deposit"> { + if (!PROXY_ADDRESS) return "deposit"; // not actually used in practice (EOA mode) + if (saved?.walletType && saved.proxyAddress?.toLowerCase() === PROXY_ADDRESS.toLowerCase()) { + return saved.walletType; + } + console.log("[System.Auth] detecting wallet type..."); + const wt = await detectWalletType(PROXY_ADDRESS); + console.log(`[System.Auth] PROXY=${PROXY_ADDRESS} type: ${wt === "deposit" ? "new deposit wallet (POLY_1271)" : "old Gnosis Safe (POLY_GNOSIS_SAFE)"}`); + return wt; +} + +function persistWalletType(saved: PolymarketCreds, walletType: "safe" | "deposit"): void { + // persist the detected walletType + current PROXY_ADDRESS into creds, skip detection on next startup + if (saved.walletType === walletType && saved.proxyAddress?.toLowerCase() === PROXY_ADDRESS.toLowerCase()) return; + try { + saved.walletType = walletType; + saved.proxyAddress = PROXY_ADDRESS; + writeFileSync(CREDS_FILE, JSON.stringify(saved, null, 2)); + } catch { /* ignore */ } +} + +async function createClobClient(): Promise { + const funderAddress = PROXY_ADDRESS || undefined; + const saved = loadCreds(); + + if (saved) { + // verify creds matches the current .env; on mismatch only log an error, do not auto re-derive (avoids infinite retries / mis-operation) + if (PRIVATE_KEY) { + const currentEoa = new ethers.Wallet(PRIVATE_KEY).address; + if (saved.address && saved.address.toLowerCase() !== currentEoa.toLowerCase()) { + console.error( + `[System.Auth] ❌ detected .env PRIVATE_KEY does not match .polymarket-creds.json:\n` + + ` creds.address: ${saved.address}\n` + + ` .env EOA: ${currentEoa}\n` + + ` fix: delete .polymarket-creds.json then restart, it will auto re-derive` + ); + } + } + if (saved.proxyAddress && PROXY_ADDRESS && saved.proxyAddress.toLowerCase() !== PROXY_ADDRESS.toLowerCase()) { + console.error( + `[System.Auth] ❌ detected .env PROXY_ADDRESS does not match .polymarket-creds.json:\n` + + ` creds.proxyAddress: ${saved.proxyAddress}\n` + + ` .env PROXY: ${PROXY_ADDRESS}\n` + + ` fix: delete .polymarket-creds.json then restart, it will auto re-derive` + ); + } + const walletType = await resolveWalletType(saved); + const sigType = walletTypeToSigType(walletType); + persistWalletType(saved, walletType); + const creds = { key: saved.key, secret: saved.secret, passphrase: saved.passphrase }; + if (PRIVATE_KEY) { + const signer = adaptSigner(new ethers.Wallet(PRIVATE_KEY)) as any; + return new ClobClient({ host: CLOB_URL, chain: Chain.POLYGON, signer, creds, signatureType: sigType, funderAddress }); + } + return new ClobClient({ host: CLOB_URL, chain: Chain.POLYGON, creds, signatureType: sigType, funderAddress }); + } + + if (!PRIVATE_KEY) { + console.warn("[System.Auth] POLYMARKET_PRIVATE_KEY not configured, ordering is unavailable"); + return null; + } + + console.log("[System.Auth] first use, generating Polymarket API credentials from private key..."); + const walletType = await resolveWalletType(null); + const sigType = walletTypeToSigType(walletType); + const wallet = new ethers.Wallet(PRIVATE_KEY); + const signer = adaptSigner(wallet) as any; + const client = new ClobClient({ host: CLOB_URL, chain: Chain.POLYGON, signer, signatureType: sigType, funderAddress }); + const creds = await client.deriveApiKey(0); + writeFileSync(CREDS_FILE, JSON.stringify({ + key: creds.key, secret: creds.secret, passphrase: creds.passphrase, + address: wallet.address, + walletType, + proxyAddress: PROXY_ADDRESS || undefined, + }, null, 2)); + console.log("[System.Auth] credentials saved to .polymarket-creds.json"); + return new ClobClient({ host: CLOB_URL, chain: Chain.POLYGON, signer, creds, signatureType: sigType, funderAddress }); +} + +// -- HTTP server --------------------------------------------------- +const app = express(); +app.use(express.json()); +if (IS_FULL_MODE) { + app.use(express.static(__dirname)); + app.get("/", (_req, res) => { + res.sendFile(resolve(__dirname, "index.html")); + }); +} else { + app.get("/", (_req, res) => { + res.json({ + name: "btc5m-web", + mode: APP_MODE, + stateUrl: "/api/state", + }); + }); +} + +const server = createServer(app); +const wss = IS_FULL_MODE ? new WebSocketServer({ server }) : null; +const clientSessions = new Map(); + +// -- CLOB Client (for ordering) ---------------------------------------- +let clobClient: ClobClient | null = null; + +async function ensureClobClient(): Promise { + if (clobClient) return true; + try { clobClient = await createClobClient(); return clobClient != null; } + catch (err) { console.error("[System.CLOB] init failed:", err); return false; } +} + +// -- Order book state ---------------------------------------------- +const state = { + windowStart: 0, + windowEnd: 0, + upTokenId: "", + downTokenId: "", + conditionId: "", + slug: "", + bids: new Map(), + asks: new Map(), + bestBid: "-", + bestAsk: "-", + lastPrice: "-", + lastSide: "", + updatedAt: 0, + priceToBeat: null as number | null, + currentPrice: null as number | null, + binanceOffset: null as number | null, + coinbaseOffset: null as number | null, + // BTC amplitude percentage within a 30-second rolling window = (max - min) / min * 100 + // updated by binance aggTrade; strategies can read ctx.volPct for decisions like "pause entry on high volatility" + volPct: null as number | null, + priceHistory: [] as Array<{ t: number; price: number }>, + binanceHistory: [] as Array<{ t: number; price: number }>, + coinbaseHistory: [] as Array<{ t: number; price: number }>, + kline1m: [] as Array, + kline5m: [] as Array, +}; + +const strategyRuntime: StrategyRuntimeState = { + state: "IDLE", + activeStrategy: null, + direction: null, + buyAmount: 0, + posBeforeBuy: 0, + posBeforeSell: 0, + waitVerifyAfterSell: false, + cleanupAfterVerify: false, + actionTs: 0, + prevUpPct: null, + buyLockUntil: 0, + positionsReady: !PROXY_ADDRESS, + roundEntryCount: 0, +}; + +// -- Position state ---------------------------------------------- +const wsStatus = { market: false, chainlink: false, user: false, binance: false, coinbase: false }; +function broadcastWsStatus() { broadcast("wsStatus", wsStatus as unknown as Record); } +const positions = { + usdc: null as number | null, + usdcAllowanceStatus: "NotApproved" as "Approved" | "NotFullyApproved" | "NotApproved", + usdcAllowanceMin: null as number | null, + usdcAllowanceDetails: [] as Array<{ spender: string; amount: number | null }>, + localSize: {} as Record, + apiSize: {} as Record, + apiVerified: {} as Record, + confirmedIds: new Set(), + confirmedIdOrder: [] as string[], + lastTradeAt: null as number | null, + lastApiSyncAt: null as number | null, +}; + +// -- Broadcast -------------------------------------------------- +function send(ws: WebSocket, type: string, data: Record): void { + if (ws.readyState !== WebSocket.OPEN) return; + ws.send(JSON.stringify({ type, ...data })); +} + +function createClientSession(dataMode: ClientDataMode): ClientSession { + return { + dataMode, + lastStateSentAt: 0, + stateTimer: null, + stateDirty: false, + stateIncludeHistory: false, + }; +} + +function normalizeClientDataMode(value: unknown): ClientDataMode { + return value === "low" ? "low" : "full"; +} + +function resolveClientDataModeFromUrl(urlValue: string | undefined): ClientDataMode { + if (!urlValue) return "full"; + try { + const url = new URL(urlValue, `http://localhost:${PORT}`); + return normalizeClientDataMode(url.searchParams.get("dataMode")); + } catch { + return "full"; + } +} + +function getClientSession(ws: WebSocket): ClientSession { + let session = clientSessions.get(ws); + if (!session) { + session = createClientSession("full"); + clientSessions.set(ws, session); + } + return session; +} + +function clearStateTimer(session: ClientSession): void { + if (session.stateTimer) { + clearTimeout(session.stateTimer); + session.stateTimer = null; + } +} + +function getStateIntervalMs(session: ClientSession): number { + return session.dataMode === "low" ? LOW_DATA_STATE_INTERVAL_MS : FULL_DATA_STATE_INTERVAL_MS; +} + +function shouldSendRealtimeEvent(type: string, ws: WebSocket, session: ClientSession): boolean { + if (session.dataMode === "low" && (type === "chainlinkPrice" || type === "binancePrice")) { + return false; + } + if ((type === "chainlinkPrice" || type === "binancePrice") && ws.bufferedAmount > MAX_WS_BUFFERED_BYTES) { + return false; + } + return true; +} + +function broadcast(type: string, data: Record): void { + if (!wss) return; + const msg = JSON.stringify({ type, ...data }); + for (const client of wss.clients) { + if (client.readyState !== WebSocket.OPEN) continue; + const session = getClientSession(client); + if (!shouldSendRealtimeEvent(type, client, session)) continue; + client.send(msg); + } +} + +function trimHistory(points: T[], cutoff: number, maxPoints: number): void { + while (points.length > 0 && points[0].t < cutoff) points.shift(); + if (points.length > maxPoints) points.splice(0, points.length - maxPoints); +} + +function rememberBounded(set: Set, order: string[], key: string, maxSize: number): boolean { + if (set.has(key)) return false; + set.add(key); + order.push(key); + while (order.length > maxSize) { + const oldest = order.shift(); + if (oldest !== undefined) set.delete(oldest); + } + return true; +} + +function prunePositionCaches(activeTokenIds: string[]): void { + const keep = new Set(activeTokenIds.filter(Boolean)); + for (const store of [positions.localSize, positions.apiSize, positions.apiVerified]) { + for (const key of Object.keys(store)) { + if (!keep.has(key)) delete store[key]; + } + } +} + +function getDirectionTokenId(direction: StrategyDirection | null): string { + if (direction === "up") return state.upTokenId; + if (direction === "down") return state.downTokenId; + return ""; +} + +function getDirectionLocalSize(direction: StrategyDirection | null): number { + const tokenId = getDirectionTokenId(direction); + return tokenId ? (positions.localSize[tokenId] ?? 0) : 0; +} + +function getDirectionApiSize(direction: StrategyDirection | null): number { + const tokenId = getDirectionTokenId(direction); + return tokenId ? (positions.apiSize[tokenId] ?? 0) : 0; +} + +function isDirectionVerified(direction: StrategyDirection | null): boolean { + const tokenId = getDirectionTokenId(direction); + return tokenId ? (positions.apiVerified[tokenId] ?? false) : false; +} + +function hasOpenPosition(): boolean { + return getDirectionLocalSize("up") > 0.01 || getDirectionLocalSize("down") > 0.01; +} + +function hasEnoughUsdcForBuy(amount: number): boolean { + if (positions.usdc == null || !Number.isFinite(amount)) return true; + return positions.usdc + 1e-6 >= amount; +} + +function hasPendingStrategyBuyLock(now = Date.now()): boolean { + return now < strategyRuntime.buyLockUntil; +} + +function getSellableShares(direction: StrategyDirection | null): number { + const localSize = getDirectionLocalSize(direction); + if (localSize <= 0) return 0; + if (isDirectionVerified(direction)) return localSize; + return Math.max(0, localSize - UNVERIFIED_SELL_BUFFER); +} + +function getLatestBinancePrice(): number | null { + const point = state.binanceHistory[state.binanceHistory.length - 1]; + return point?.price ?? null; +} + +// 30-second rolling amplitude = (max - min) / min * 100; null when points < 2 or time is less than 30s +const VOL_WINDOW_MS = 30_000; +function recomputeVolPct(now: number): void { + const cutoff = now - VOL_WINDOW_MS; + const pts = state.binanceHistory.filter(p => p.t >= cutoff); + if (pts.length < 2) { state.volPct = null; return; } + // also check whether the earliest point is really 30+ seconds ago (the window is insufficient in the early startup phase) + const span = pts[pts.length - 1].t - pts[0].t; + if (span < VOL_WINDOW_MS - 1000) { state.volPct = null; return; } + let hi = -Infinity, lo = Infinity; + for (const p of pts) { if (p.price > hi) hi = p.price; if (p.price < lo) lo = p.price; } + if (lo <= 0) { state.volPct = null; return; } + state.volPct = (hi - lo) / lo * 100; +} + +function getProbabilitySnapshot(): { upPct: number; dnPct: number } | null { + if (!isProbabilityReady()) return null; + const bid = Number(state.bestBid); + const ask = Number(state.bestAsk); + if (!Number.isFinite(bid) || !Number.isFinite(ask)) return null; + const mid = (bid + ask) / 2; + return { + upPct: Math.round(mid * 100), + dnPct: Math.round((1 - mid) * 100), + }; +} + +function getStrategyDiff(): number | null { + const latestBinancePrice = getLatestBinancePrice(); + if (latestBinancePrice == null || state.priceToBeat == null || state.binanceOffset == null) return null; + return latestBinancePrice - (state.priceToBeat - state.binanceOffset); +} + +function calcMedian(values: number[]): number | null { + if (!values.length) return null; + const sorted = values.slice().sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2; +} + +function calcTrimmedMean(values: number[], trimRatio = 0.15): number | null { + if (!values.length) return null; + const sorted = values.slice().sort((a, b) => a - b); + const trim = sorted.length >= 8 ? Math.floor(sorted.length * trimRatio) : 0; + const trimmed = trim > 0 ? sorted.slice(trim, sorted.length - trim) : sorted; + if (!trimmed.length) return null; + return trimmed.reduce((sum, value) => sum + value, 0) / trimmed.length; +} + +function calculateBinanceOffset(allowLatestFallback = false): number | null { + if (!state.binanceHistory.length || !state.priceHistory.length) { + if (!allowLatestFallback) return null; + const latestBinancePrice = getLatestBinancePrice(); + if (latestBinancePrice == null || state.currentPrice == null) return null; + return state.currentPrice - latestBinancePrice; + } + + const now = Date.now(); + const binanceRecent = state.binanceHistory.filter((point) => point.t >= now - BINANCE_ALIGN_WINDOW_MS); + const chainlinkRecent = state.priceHistory.filter((point) => point.t >= now - BINANCE_ALIGN_WINDOW_MS); + if (!binanceRecent.length || !chainlinkRecent.length) { + if (!allowLatestFallback) return null; + const latestBinancePrice = getLatestBinancePrice(); + if (latestBinancePrice == null || state.currentPrice == null) return null; + return state.currentPrice - latestBinancePrice; + } + + const binanceSpan = binanceRecent.length >= 2 + ? binanceRecent[binanceRecent.length - 1].t - binanceRecent[0].t + : 0; + const chainlinkSpan = chainlinkRecent.length >= 2 + ? chainlinkRecent[chainlinkRecent.length - 1].t - chainlinkRecent[0].t + : 0; + + if (Math.min(binanceSpan, chainlinkSpan) < BINANCE_ALIGN_MIN_SPAN_MS) { + if (!allowLatestFallback) return null; + return chainlinkRecent[chainlinkRecent.length - 1].price - binanceRecent[binanceRecent.length - 1].price; + } + + const overlapStart = Math.max(binanceRecent[0].t, chainlinkRecent[0].t); + const overlapEnd = Math.min(binanceRecent[binanceRecent.length - 1].t, chainlinkRecent[chainlinkRecent.length - 1].t); + const diffs: number[] = []; + + if (overlapEnd - overlapStart >= BINANCE_ALIGN_BUCKET_MS * 2) { + let binanceIdx = 0; + let chainlinkIdx = 0; + for (let bucketStart = overlapStart; bucketStart <= overlapEnd; bucketStart += BINANCE_ALIGN_BUCKET_MS) { + const bucketEnd = bucketStart + BINANCE_ALIGN_BUCKET_MS; + const binanceBucket: number[] = []; + const chainlinkBucket: number[] = []; + + while (binanceIdx < binanceRecent.length && binanceRecent[binanceIdx].t < bucketStart) binanceIdx++; + while (chainlinkIdx < chainlinkRecent.length && chainlinkRecent[chainlinkIdx].t < bucketStart) chainlinkIdx++; + + let i = binanceIdx; + while (i < binanceRecent.length && binanceRecent[i].t < bucketEnd) { + binanceBucket.push(binanceRecent[i].price); + i++; + } + let j = chainlinkIdx; + while (j < chainlinkRecent.length && chainlinkRecent[j].t < bucketEnd) { + chainlinkBucket.push(chainlinkRecent[j].price); + j++; + } + + const binanceMedian = calcMedian(binanceBucket); + const chainlinkMedian = calcMedian(chainlinkBucket); + if (binanceMedian != null && chainlinkMedian != null) { + diffs.push(chainlinkMedian - binanceMedian); + } + } + } + + if (!diffs.length) { + return chainlinkRecent[chainlinkRecent.length - 1].price - binanceRecent[binanceRecent.length - 1].price; + } + if (diffs.length < 5) { + return calcTrimmedMean(diffs, 0); + } + + const median = calcMedian(diffs); + if (median == null) return null; + const absDeviations = diffs.map((diff) => Math.abs(diff - median)); + const mad = calcMedian(absDeviations) ?? 0; + const threshold = Math.max(10, mad * 3); + const filtered = diffs.filter((diff) => Math.abs(diff - median) <= threshold); + const stable = filtered.length >= 3 ? filtered : diffs; + return calcTrimmedMean(stable, 0.15); +} + +function refreshBinanceOffset(reason: string, options: { allowLatestFallback?: boolean; forceLog?: boolean } = {}): boolean { + const nextOffset = calculateBinanceOffset(options.allowLatestFallback ?? false); + if (nextOffset == null) return false; + + const prevOffset = state.binanceOffset; + const changed = prevOffset == null || Math.abs(prevOffset - nextOffset) > BINANCE_OFFSET_EPSILON; + state.binanceOffset = nextOffset; + + if (!changed) return true; + + if (options.forceLog || prevOffset == null) { + const prefix = prevOffset == null ? "init offset" : `${reason} update`; + console.log(`[Price.BinanceOffset] ${prefix} ${nextOffset >= 0 ? "+" : ""}${nextOffset.toFixed(2)}`); + } + + broadcastState(); + return true; +} + +function maybeInitializeBinanceOffset(): void { + if (state.binanceOffset != null) return; + void refreshBinanceOffset("init", { allowLatestFallback: true, forceLog: true }); +} + +// -- Generic offset computation (other BTC/USD price sources like Coinbase aligned to Chainlink) -- +function calcOffsetVsChainlink( + history: Array<{ t: number; price: number }>, + allowLatestFallback: boolean, +): number | null { + if (!history.length || !state.priceHistory.length) { + if (!allowLatestFallback) return null; + const latest = history[history.length - 1]?.price ?? null; + if (latest == null || state.currentPrice == null) return null; + return state.currentPrice - latest; + } + + const now = Date.now(); + const recent = history.filter((p) => p.t >= now - BINANCE_ALIGN_WINDOW_MS); + const chainlinkRecent = state.priceHistory.filter((p) => p.t >= now - BINANCE_ALIGN_WINDOW_MS); + if (!recent.length || !chainlinkRecent.length) { + if (!allowLatestFallback) return null; + const latest = history[history.length - 1]?.price ?? null; + if (latest == null || state.currentPrice == null) return null; + return state.currentPrice - latest; + } + + const span = recent.length >= 2 ? recent[recent.length - 1].t - recent[0].t : 0; + const chSpan = chainlinkRecent.length >= 2 + ? chainlinkRecent[chainlinkRecent.length - 1].t - chainlinkRecent[0].t + : 0; + if (Math.min(span, chSpan) < BINANCE_ALIGN_MIN_SPAN_MS) { + if (!allowLatestFallback) return null; + return chainlinkRecent[chainlinkRecent.length - 1].price - recent[recent.length - 1].price; + } + + const overlapStart = Math.max(recent[0].t, chainlinkRecent[0].t); + const overlapEnd = Math.min(recent[recent.length - 1].t, chainlinkRecent[chainlinkRecent.length - 1].t); + const diffs: number[] = []; + + if (overlapEnd - overlapStart >= BINANCE_ALIGN_BUCKET_MS * 2) { + let aIdx = 0; + let cIdx = 0; + for (let bs = overlapStart; bs <= overlapEnd; bs += BINANCE_ALIGN_BUCKET_MS) { + const be = bs + BINANCE_ALIGN_BUCKET_MS; + const aBuck: number[] = []; + const cBuck: number[] = []; + while (aIdx < recent.length && recent[aIdx].t < bs) aIdx++; + while (cIdx < chainlinkRecent.length && chainlinkRecent[cIdx].t < bs) cIdx++; + let i = aIdx; + while (i < recent.length && recent[i].t < be) { aBuck.push(recent[i].price); i++; } + let j = cIdx; + while (j < chainlinkRecent.length && chainlinkRecent[j].t < be) { cBuck.push(chainlinkRecent[j].price); j++; } + const aMed = calcMedian(aBuck); + const cMed = calcMedian(cBuck); + if (aMed != null && cMed != null) diffs.push(cMed - aMed); + } + } + + if (!diffs.length) { + return chainlinkRecent[chainlinkRecent.length - 1].price - recent[recent.length - 1].price; + } + if (diffs.length < 5) return calcTrimmedMean(diffs, 0); + const median = calcMedian(diffs); + if (median == null) return null; + const absDev = diffs.map((d) => Math.abs(d - median)); + const mad = calcMedian(absDev) ?? 0; + const threshold = Math.max(10, mad * 3); + const filtered = diffs.filter((d) => Math.abs(d - median) <= threshold); + const stable = filtered.length >= 3 ? filtered : diffs; + return calcTrimmedMean(stable, 0.15); +} + +interface OffsetSpec { + name: string; + history: () => Array<{ t: number; price: number }>; + getOffset: () => number | null; + setOffset: (v: number | null) => void; +} +const COINBASE_SPEC: OffsetSpec = { + name: "CoinbaseOffset", + history: () => state.coinbaseHistory, + getOffset: () => state.coinbaseOffset, + setOffset: (v) => { state.coinbaseOffset = v; }, +}; +function refreshGenericOffset(spec: OffsetSpec, reason: string, options: { allowLatestFallback?: boolean; forceLog?: boolean } = {}): boolean { + const next = calcOffsetVsChainlink(spec.history(), options.allowLatestFallback ?? false); + if (next == null) return false; + const prev = spec.getOffset(); + const changed = prev == null || Math.abs(prev - next) > BINANCE_OFFSET_EPSILON; + spec.setOffset(next); + if (!changed) return true; + // the "init offset" on window switch is printed by the [System.Window] summary line; here only print on large changes or explicit forceLog + if (options.forceLog) { + const prefix = prev == null ? "init" : `${reason} update`; + console.log(`[${spec.name}] ${prefix} ${next >= 0 ? "+" : ""}${next.toFixed(2)}`); + } + broadcastState(); + return true; +} + +function maybeInitOffset(spec: OffsetSpec): void { + if (spec.getOffset() != null) return; + void refreshGenericOffset(spec, "init", { allowLatestFallback: true, forceLog: true }); +} + +function resetStrategyRuntime(reason?: string): void { + strategyRuntime.state = "IDLE"; + strategyRuntime.activeStrategy = null; + strategyRuntime.direction = null; + strategyRuntime.buyAmount = 0; + strategyRuntime.posBeforeBuy = 0; + strategyRuntime.posBeforeSell = 0; + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.cleanupAfterVerify = false; + strategyRuntime.actionTs = 0; + strategyRuntime.prevUpPct = null; + strategyRuntime.buyLockUntil = 0; + strategyRuntime.roundEntryCount = 0; + for (const s of getAllStrategies()) s.resetState(); + if (reason) console.log(`[Strategy] reset: ${reason}`); +} + + +function transitionToDone(): void { + if (strategyRuntime.roundEntryCount < strategyConfig.maxRoundEntries && anyStrategyEnabled()) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] done, back to scanning (${strategyRuntime.roundEntryCount}/${strategyConfig.maxRoundEntries})`); + strategyRuntime.state = "SCANNING"; + strategyRuntime.activeStrategy = null; + strategyRuntime.direction = null; + strategyRuntime.buyAmount = 0; + strategyRuntime.posBeforeBuy = 0; + strategyRuntime.posBeforeSell = 0; + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.cleanupAfterVerify = false; + strategyRuntime.actionTs = 0; + } else { + strategyRuntime.state = "DONE"; + } + broadcastState(); +} + +function anyStrategyEnabled(): boolean { + return ALL_STRATEGY_KEYS.some((key) => strategyConfig.enabled[key]); +} + +function hasConfirmedBuyPosition(): boolean { + return strategyRuntime.direction != null + && getDirectionLocalSize(strategyRuntime.direction) > strategyRuntime.posBeforeBuy + 0.01; +} + +function canReleaseUnconfirmedBuy(now = Date.now()): boolean { + if (now - strategyRuntime.actionTs < FILL_RECONCILE_TIMEOUT_MS) return false; + if (!strategyRuntime.direction) return true; + if ((positions.lastApiSyncAt ?? 0) <= strategyRuntime.actionTs) return false; + return getDirectionApiSize(strategyRuntime.direction) <= strategyRuntime.posBeforeBuy + 0.01; +} + + +function buildStrategyRuntimePayload(): Record { + const perStrategy: Record> = {}; + // observe panel data: collect as long as the strategy implements getObservePanel + const observePanels: Record = {}; + for (const s of getAllStrategies()) { + perStrategy[s.key] = s.getStatePayload(); + if (typeof s.getObservePanel === "function") { + const panel = s.getObservePanel(); + if (panel) observePanels[s.key] = panel; + } + } + return { + observePanels, + state: strategyRuntime.state, + activeStrategy: strategyRuntime.activeStrategy, + direction: strategyRuntime.direction, + buyAmount: strategyRuntime.buyAmount, + posBeforeBuy: strategyRuntime.posBeforeBuy, + posBeforeSell: strategyRuntime.posBeforeSell, + waitVerifyAfterSell: strategyRuntime.waitVerifyAfterSell, + cleanupAfterVerify: strategyRuntime.cleanupAfterVerify, + actionTs: strategyRuntime.actionTs, + prevUpPct: strategyRuntime.prevUpPct, + buyLockUntil: strategyRuntime.buyLockUntil, + positionsReady: strategyRuntime.positionsReady, + roundEntryCount: strategyRuntime.roundEntryCount, + perStrategy, + }; +} + +/** compute the current fair probability (for real-time display on the s5/s10/s11 frontend panel) */ +function computeFairProbPayload(): { + diff: number | null; + rem: number; + upPct: number | null; + fairUp: number | null; + biasUp: number | null; + symbol: MarketSymbol; + hasFairTable: boolean; +} | null { + const diff = getStrategyDiff(); + const rem = getStrategyRemainingSeconds(); + const snap = getProbabilitySnapshot(); + // whether the current market has a fair-probability table (the fair-prob module maintains one per marketKey) + const hasFairTable = hasFairProbTable(); + if (diff == null || !snap) { + return { diff, rem, upPct: snap?.upPct ?? null, fairUp: null, biasUp: null, symbol: activeMarket.symbol, hasFairTable }; + } + const fairUp = getFairProb(diff, rem); + const biasUp = fairUp != null ? fairUp - snap.upPct : null; + return { diff, rem, upPct: snap.upPct, fairUp, biasUp, symbol: activeMarket.symbol, hasFairTable }; +} + +function buildStatePayload(options: boolean | StatePayloadOptions = false): Record { + const normalized = typeof options === "boolean" ? { includeHistory: options } : options; + const includeHistory = normalized.includeHistory === true; + const simple = normalized.simple === true; + const bids = [...state.bids.entries()] + .map(([price, size]) => ({ price: Number(price), size: Number(size) })) + .sort((a, b) => b.price - a.price).slice(0, 8); + const asks = [...state.asks.entries()] + .map(([price, size]) => ({ price: Number(price), size: Number(size) })) + .sort((a, b) => a.price - b.price).slice(0, 8); + + const payload: Record = { + activeMarket: { + key: activeMarket.key, + symbol: activeMarket.symbol, + period: activeMarket.period, + periodSeconds: activeMarket.periodSeconds, + displayName: activeMarket.displayName, + priceDecimals: priceDecimals(state.priceToBeat), + }, + windowStart: state.windowStart, + windowEnd: state.windowEnd, + bestBid: state.bestBid, + bestAsk: state.bestAsk, + probabilityReady: isProbabilityReady(), + lastPrice: state.lastPrice, + lastSide: state.lastSide, + updatedAt: state.updatedAt, + priceToBeat: state.priceToBeat, + currentPrice: state.currentPrice, + binanceOffset: state.binanceOffset, + coinbaseOffset: state.coinbaseOffset, + volPct: state.volPct, + accountName: accountName, + klineCounts: { k1m: state.kline1m.length, k5m: state.kline5m.length }, + binanceDiff: getStrategyDiff(), + fairProb: computeFairProbPayload(), + usdc: positions.usdc, + usdcAllowanceStatus: positions.usdcAllowanceStatus, + usdcAllowanceMin: positions.usdcAllowanceMin, + upLocalSize: positions.localSize[state.upTokenId] ?? 0, + downLocalSize: positions.localSize[state.downTokenId] ?? 0, + upApiSize: positions.apiSize[state.upTokenId] ?? 0, + downApiSize: positions.apiSize[state.downTokenId] ?? 0, + upApiVerified: positions.apiVerified[state.upTokenId] ?? false, + downApiVerified:positions.apiVerified[state.downTokenId] ?? false, + lastTradeAt: positions.lastTradeAt, + lastApiSyncAt: positions.lastApiSyncAt, + runtimeMode: APP_MODE, + strategyConfig, + strategy: buildStrategyRuntimePayload(), + marketStatus: getUsMarketStatus(), + ts: Date.now(), + }; + if (!simple) { + payload.conditionId = state.conditionId; + payload.upTokenId = state.upTokenId; + payload.downTokenId = state.downTokenId; + payload.bids = bids; + payload.asks = asks; + payload.usdcAllowanceDetails = positions.usdcAllowanceDetails; + } + if (includeHistory && !simple) { + payload.priceHistory = state.priceHistory; + payload.binanceHistory = state.binanceHistory; + payload.coinbaseHistory = state.coinbaseHistory; + } + return payload; +} + +function sendStateToClient(ws: WebSocket, options: { includeHistory?: boolean } = {}): void { + const session = getClientSession(ws); + const simple = session.dataMode === "low"; + send(ws, "state", buildStatePayload({ + includeHistory: options.includeHistory === true && !simple, + simple, + })); + session.lastStateSentAt = Date.now(); +} + +function scheduleStateToClient(ws: WebSocket, includeHistory = false): void { + if (ws.readyState !== WebSocket.OPEN) return; + const session = getClientSession(ws); + session.stateDirty = true; + session.stateIncludeHistory = session.stateIncludeHistory || includeHistory; + if (session.stateTimer) return; + const elapsed = Date.now() - session.lastStateSentAt; + const waitMs = Math.max(0, getStateIntervalMs(session) - elapsed); + session.stateTimer = setTimeout(() => { + const latestSession = clientSessions.get(ws); + if (!latestSession) return; + latestSession.stateTimer = null; + if (!latestSession.stateDirty || ws.readyState !== WebSocket.OPEN) return; + const nextIncludeHistory = latestSession.stateIncludeHistory; + latestSession.stateDirty = false; + latestSession.stateIncludeHistory = false; + if (ws.bufferedAmount > MAX_WS_BUFFERED_BYTES) { + latestSession.stateDirty = true; + latestSession.stateIncludeHistory = nextIncludeHistory; + scheduleStateToClient(ws, nextIncludeHistory); + return; + } + sendStateToClient(ws, { includeHistory: nextIncludeHistory }); + }, waitMs); +} + +function broadcastState(includeHistory = false): void { + if (!wss) return; + for (const client of wss.clients) { + if (client.readyState !== WebSocket.OPEN) continue; + scheduleStateToClient(client, includeHistory); + } +} + +function applyClientConfig(ws: WebSocket, raw: unknown): void { + if (!isRecord(raw) || raw.type !== "clientConfig") return; + const session = getClientSession(ws); + const nextMode = normalizeClientDataMode(raw.dataMode); + if (session.dataMode === nextMode) return; + session.dataMode = nextMode; + session.stateDirty = false; + session.stateIncludeHistory = false; + clearStateTimer(session); + console.log(`[System.Frontend] client data mode switched to ${nextMode}`); + send(ws, "clientConfig", { dataMode: nextMode }); + sendStateToClient(ws, { includeHistory: true }); +} + +async function fetchBookTopOfBook(tokenId: string): Promise<{ bestBid: number; bestAsk: number }> { + const book = await fetch(`${CLOB_URL}/book?token_id=${tokenId}`).then(r => r.json()) as { + bids?: { price: string }[]; asks?: { price: string }[]; + }; + const bids = (book.bids || []).map(b => Number(b.price)).filter(p => p > 0); + const asks = (book.asks || []).map(a => Number(a.price)).filter(p => p > 0); + return { + bestBid: bids.length ? Math.max(...bids) : 0, + bestAsk: asks.length ? Math.min(...asks) : 0, + }; +} + +// ── Gamma API ───────────────────────────────────────────────── +async function fetchMarket(windowStart: number): Promise<{ + conditionId: string; upTokenId: string; downTokenId: string; + windowStart: number; windowEnd: number; + eventStartTime: string; endDate: string; +} | null> { + const slug = `${activeMarket.slugPrefix}-${windowStart}`; + const startedAt = Date.now(); + try { + const res = await fetch(`${GAMMA_URL}/events?slug=${slug}`); + const events = await res.json() as Record[]; + if (!events?.length) { + console.warn(`[System.Window] market not found slug=${slug} elapsed:${Date.now() - startedAt}ms`); + return null; + } + const event = events[0]; + const market = ((event.markets || []) as Record[])[0]; + if (!market) { + console.warn(`[System.Window] market missing order book slug=${slug} elapsed:${Date.now() - startedAt}ms`); + return null; + } + const tokens = JSON.parse(market.clobTokenIds as string || "[]") as string[]; + const outcomes = JSON.parse(market.outcomes as string || "[]") as string[]; + const upIdx = outcomes.findIndex((o) => o.toLowerCase() === "up"); + return { + conditionId: market.conditionId as string, + upTokenId: tokens[upIdx >= 0 ? upIdx : 0], + downTokenId: tokens[upIdx >= 0 ? 1 - upIdx : 1], + windowStart, + windowEnd: windowStart + activeMarket.periodSeconds, + eventStartTime: market.eventStartTime as string || new Date(windowStart * 1000).toISOString(), + endDate: market.endDate as string || new Date((windowStart + activeMarket.periodSeconds) * 1000).toISOString(), + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[System.Window] market query failed slug=${slug} elapsed:${Date.now() - startedAt}ms reason:${msg}`); + return null; + } +} + +// -- Reference price ---------------------------------------------- +async function fetchCryptoPrice(eventStartTime: string, endDate: string): Promise { + try { + const url = `https://polymarket.com/api/crypto/crypto-price?symbol=${activeMarket.cryptoPriceSymbol}&eventStartTime=${encodeURIComponent(eventStartTime)}&variant=${activeMarket.cryptoPriceVariant}&endDate=${encodeURIComponent(endDate)}`; + const data = await fetch(url).then(r => r.json()) as { openPrice?: number | null; closePrice?: number | null }; + console.log("[RAW]", `[Price.Open] fetch ${activeMarket.cryptoPriceSymbol} ${activeMarket.cryptoPriceVariant} eventStart=${eventStartTime} -> openPrice=${data.openPrice} closePrice=${data.closePrice}`); + if (data.openPrice != null) state.priceToBeat = data.openPrice; + } catch (err) { + console.warn(`[Price.Open] fetch failed: ${(err as Error).message}`); + } +} + +// -- Position API query -------------------------------------------- +async function syncPositionsFromApi(): Promise { + if (!PROXY_ADDRESS) { + strategyRuntime.positionsReady = true; + return true; + } + try { + const pos = await fetch( + `https://data-api.polymarket.com/positions?user=${PROXY_ADDRESS}&sizeThreshold=0.01` + ).then(r => r.json()) as Array<{ asset: string; size: number }>; + const apiMap: Record = {}; + for (const p of pos) { apiMap[p.asset] = p.size; positions.apiSize[p.asset] = p.size; } + for (const tokenId of [state.upTokenId, state.downTokenId]) { + if (!tokenId) continue; + if (!(tokenId in apiMap)) positions.apiSize[tokenId] = 0; + const apiVal = apiMap[tokenId] ?? 0; + const localVal = positions.localSize[tokenId] ?? 0; + const msSinceTrade = Date.now() - (positions.lastTradeAt ?? 0); + if (msSinceTrade < POST_TRADE_CALIBRATION_MS) continue; + if (Math.abs(apiVal - localVal) <= 0.5) { + positions.localSize[tokenId] = apiVal; + positions.apiVerified[tokenId] = true; + } + } + positions.lastApiSyncAt = Date.now(); + strategyRuntime.positionsReady = true; + return true; + } catch { + return false; + } +} + +// -- USDC balance query -------------------------------------------- +// stop subsequent API calls after a CLOB auth error, to avoid the 401 every 5 seconds + extremely long axios error dump flooding +let clobAuthFailed = false; +function isAuthError(err: unknown): boolean { + if (!err || typeof err !== "object") return false; + const e = err as any; + return e.response?.status === 401 || /unauthorized|invalid api key/i.test(e.message || ""); +} + +async function syncUsdcBalance(): Promise { + if (!PROXY_ADDRESS) return; + if (clobAuthFailed) return; // auth failure already reported, stop flooding + try { + if (!(await ensureClobClient())) return; + const resp = await clobClient!.getBalanceAllowance({ asset_type: AssetType.COLLATERAL }) as { + balance?: string; + allowance?: string; + allowances?: Record; + }; + + positions.usdc = resp.balance != null ? parseFloat(ethers.formatUnits(resp.balance, 6)) : null; + + const allowanceMap = resp.allowances && typeof resp.allowances === "object" + ? Object.entries(resp.allowances) + : resp.allowance != null + ? [["default", resp.allowance]] + : []; + + const details = allowanceMap.map(([spender, raw]) => { + const amount = raw != null ? parseFloat(ethers.formatUnits(raw, 6)) : null; + return { spender, amount: Number.isFinite(amount) ? amount : null }; + }); + + positions.usdcAllowanceDetails = details; + + if (!details.length) { + positions.usdcAllowanceStatus = "NotApproved"; + positions.usdcAllowanceMin = null; + return; + } + + const positiveCount = details.filter((item) => (item.amount ?? 0) > 0).length; + const minAllowance = details.reduce((min, item) => { + if (item.amount == null) return min; + return min == null ? item.amount : Math.min(min, item.amount); + }, null); + + positions.usdcAllowanceMin = minAllowance; + positions.usdcAllowanceStatus = positiveCount === 0 + ? "NotApproved" + : positiveCount === details.length + ? "Approved" + : "NotFullyApproved"; + } catch (e) { + if (isAuthError(e)) { + clobAuthFailed = true; + console.error("[System.Auth] ❌ CLOB API key auth failed (401 Unauthorized)"); + console.error("[System.Auth] possible cause: .env changed PRIVATE_KEY/PROXY_ADDRESS but .polymarket-creds.json is still the old one"); + console.error("[System.Auth] fix: delete .polymarket-creds.json then restart to auto re-derive"); + console.error("[System.Auth] subsequent API calls stopped, to avoid repeatedly flooding error logs"); + return; + } + console.error("[System.Balance] balance/allowance query failed:", e instanceof Error ? (e as any).shortMessage ?? e.message : String(e)); + } +} + +// -- Backoff reconnect utility ------------------------------------ +function backoffDelay(attempt: number): number { + const delays = [0, 1000, 2000, 4000, 8000, 30000]; + return delays[Math.min(attempt, delays.length - 1)]; +} + +// -- User WS (listen for fills) -------------------------------------- +let userWs: WebSocket | null = null; +let userWsPingTimer: ReturnType | null = null; +let userWsAttempt = 0; + +function startUserWs(): void { + if (!existsSync(CREDS_FILE)) { console.log("[WS.User] credentials file not found, skipping"); return; } + const creds = JSON.parse(readFileSync(CREDS_FILE, "utf-8")) as { + key: string; secret: string; passphrase: string; + }; + + userWs = new WebSocket(USER_WS_URL); + + userWs.on("open", () => { + console.log(userWsAttempt === 0 ? "[WS.User] connected" : "[WS.User] reconnect succeeded"); + userWsAttempt = 0; + wsStatus.user = true; broadcastWsStatus(); + userWs!.send(JSON.stringify({ + auth: { apiKey: creds.key, secret: creds.secret, passphrase: creds.passphrase }, + type: "user", + })); + userWsPingTimer = setInterval(() => { + if (userWs?.readyState === WebSocket.OPEN) userWs.send("PING"); + }, 10000); + }); + + userWs.on("message", (data) => { + const msg = data.toString(); + if (msg === "PONG") return; + try { + const arr = JSON.parse(msg); + const events = Array.isArray(arr) ? arr : [arr]; + for (const evt of events) { + if (!isRecord(evt)) continue; + // -- full event raw print (includes trade / order / any other Polymarket UserWS type) -- + // for diagnostics: confirm whether Polymarket pushes order PLACEMENT/UPDATE/CANCELLATION, trade FAILED/RETRYING/CONFIRMED, etc. + // [RAW] marker -> goes only to trade-raw-*.log, does not pollute the terminal + const evtType = (typeof evt.event_type === "string" ? evt.event_type : "") || (typeof evt.type === "string" ? evt.type : ""); + const evtStatus = typeof evt.status === "string" ? evt.status : ""; + console.log("[RAW]", `[WS.User.Event] event_type:${evtType} status:${evtStatus} raw:`, JSON.stringify(evt)); + + // -- event_type: "order" - order lifecycle (PLACEMENT / CANCELLATION / UPDATE) -- + // used for: (1) unlock pendingOrderConfirm / pendingCancelConfirm; (2) CANCELLATION immediately clears the local Map + // if pending is not yet registered (WS arrives before the HTTP response) -> write earlyCache, check on register + if (evt.event_type === "order") { + const orderID = typeof evt.id === "string" ? evt.id : ""; + const orderType = typeof evt.type === "string" ? evt.type.toUpperCase() : ""; + const orderStatus = typeof evt.status === "string" ? evt.status.toUpperCase() : ""; + if (orderID) { + if (orderType === "PLACEMENT" || orderStatus === "LIVE") { + if (pendingOrderConfirm.has(orderID)) resolveOrderConfirm(orderID, "WS PLACEMENT"); + else rememberEarlyWs(earlyOrderConfirmCache, orderID, "WS PLACEMENT"); + } + if (orderType === "CANCELLATION" || orderStatus === "CANCELED") { + if (pendingCancelConfirm.has(orderID)) resolveCancelConfirm(orderID, "WS CANCELLATION"); + else rememberEarlyWs(earlyCancelConfirmCache, orderID, "WS CANCELLATION"); + // even without a pendingCancel, the local Map must be cleared (system-initiated cancel / window-switch cleanup / insufficient balance, etc.) + if (strategyLimitOrders.has(orderID) || manualLimitOrders.has(orderID)) { + console.log(`[WS.User.Order] CANCELLATION arrived, clearing local Map orderID=${fmtOid(orderID)}`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + clearPendingTradeMetaByOrderId(orderID); + broadcastManualLimitOrders(); + } + } + // UPDATE push (partial fill size_matched increases): matchedSize is handled by trade MATCHED, here we only unlock confirm + if (orderType === "UPDATE") { + if (pendingOrderConfirm.has(orderID)) resolveOrderConfirm(orderID, "WS UPDATE"); + else rememberEarlyWs(earlyOrderConfirmCache, orderID, "WS UPDATE"); + } + } + continue; // order event handled, do not enter the trade branch + } + // -- latency monitoring: MATCHED event settles the total latency of this machine's order -> match (may arrive before the HTTP response) -- + if ((evt.type === "TRADE" || evt.event_type === "trade") && evt.status === "MATCHED") { + console.log("[RAW]", `[WS.User] MATCHED raw:`, JSON.stringify(evt)); + const mOrderId = typeof evt.taker_order_id === "string" ? evt.taker_order_id : undefined; + if (mOrderId) { + if (pendingOrderConfirm.has(mOrderId)) resolveOrderConfirm(mOrderId, "WS MATCHED(taker)"); + else rememberEarlyWs(earlyOrderConfirmCache, mOrderId, "WS MATCHED(taker)"); + } + // MAKER path: our own order is in maker_orders[], must also be unlocked + const moList = Array.isArray(evt.maker_orders) ? evt.maker_orders : []; + for (const mo of moList) { + if (!isRecord(mo)) continue; + const moOid = typeof mo.order_id === "string" ? mo.order_id : ""; + if (!moOid) continue; + if (pendingOrderConfirm.has(moOid)) resolveOrderConfirm(moOid, "WS MATCHED(maker)"); + else rememberEarlyWs(earlyOrderConfirmCache, moOid, "WS MATCHED(maker)"); + } + onWsMatched(mOrderId); + // check whether a conditional order (TP GTC) was matched in this MATCHED (as a maker) + const makerOrders = Array.isArray(evt.maker_orders) ? evt.maker_orders : []; + for (const mo of makerOrders) { + if (!isRecord(mo)) continue; + const makerOrderId = typeof mo.order_id === "string" ? mo.order_id : ""; + if (!makerOrderId) continue; + const matchedAmount = parseFloat(String(mo.matched_amount ?? "0")); + for (const cond of activeConditionOrders.values()) { + if (cond.kind !== "tp" || cond.polymarketOrderId !== makerOrderId) continue; + // MATCHED only updates the match-layer remainingSize (display only); + // the real fill decision goes through the MINED on-chain path, to avoid state divergence caused by multiple PM MATCHED pushes over-reporting + if (Number.isFinite(matchedAmount) && matchedAmount > 0) { + cond.remainingSize = Math.max(0, cond.remainingSize - matchedAmount); + console.log(`[Cond.TP] MATCHED ${cond.direction} +${matchedAmount.toFixed(4)} match remaining ${cond.remainingSize.toFixed(4)} (awaiting MINED confirmation)`); + } + } + // strategy limit order taken as maker: MATCHED = match passed (not on-chain), only update matchedSize; + // the real "fill" decision + deleting stratOrder + triggering TP, all done in the MINED branch by on-chain confirmation. + const stratOrder = strategyLimitOrders.get(makerOrderId); + if (stratOrder && Number.isFinite(matchedAmount) && matchedAmount > 0) { + stratOrder.matchedSize = Math.min(stratOrder.shares, stratOrder.matchedSize + matchedAmount); + const tag = stratOrder.side === "sell" ? "TP" : "limit"; + const moPrice = mo.price; + const moFee = mo.fee_rate_bps; + const tradeId = typeof evt.id === "string" ? evt.id : "-"; + const takerOrderId = typeof evt.taker_order_id === "string" ? evt.taker_order_id : "-"; + console.log(`[Strategy.${stratOrder.strategyKey}] ${tag} matched +${matchedAmount} shares matching ${stratOrder.matchedSize}/${stratOrder.shares} maker:${makerOrderId} price:${moPrice ?? "-"} fee_bps:${moFee ?? "-"} tradeId:${tradeId} taker:${takerOrderId} (awaiting on-chain confirmation)`); + } + } + broadcastConditionOrders(); + continue; // only settle latency; position updates wait for the MINED event + } + if ((evt.type === "TRADE" || evt.event_type === "trade") && evt.status === "MINED") { + console.log("[RAW]", `[WS.User] MINED raw:`, JSON.stringify(evt)); + const tradeId = evt.id as string; + const traderSide = typeof evt.trader_side === "string" ? evt.trader_side.toUpperCase() : ""; + const isMaker = traderSide === "MAKER"; + const txHash = typeof evt.transaction_hash === "string" && evt.transaction_hash + ? evt.transaction_hash + : undefined; + // -- extract the "my fills" list -- + // TAKER: use top-level fields (taker_order_id / asset_id / size / side / price) -- 1 entry + // MAKER: filter from maker_orders[] where maker_address === PROXY_ADDRESS or order_id hits the local Map -- may be multiple entries + // note: for MAKER the top-level asset_id/side/size/price are from the counterparty's (taker's) perspective, must never be used + type MyFill = { + dedupKey: string; + orderId: string | undefined; + assetId: string; + side: "buy" | "sell"; + size: number; + price: number; + }; + const proxyLc = (PROXY_ADDRESS || "").toLowerCase(); + const myFills: MyFill[] = []; + if (isMaker) { + const makerOrders = Array.isArray(evt.maker_orders) ? evt.maker_orders : []; + for (const mo of makerOrders) { + if (!isRecord(mo)) continue; + const ma = typeof mo.maker_address === "string" ? mo.maker_address : ""; + const oid = typeof mo.order_id === "string" ? mo.order_id : ""; + const isMine = (!!ma && ma.toLowerCase() === proxyLc) + || (!!oid && (manualLimitOrders.has(oid) || strategyLimitOrders.has(oid))); + if (!isMine) continue; + const moAssetId = typeof mo.asset_id === "string" ? mo.asset_id : ""; + const moSide = normalizeTradeSide(mo.side); + const moAmount = parseFloat(String(mo.matched_amount ?? "0")); + const moPrice = parseFloat(String(mo.price ?? "0")); + if (!moAssetId || !moSide || !Number.isFinite(moAmount) || moAmount <= 0) continue; + myFills.push({ + dedupKey: `${tradeId}:${oid}`, + orderId: oid || undefined, + assetId: moAssetId, + side: moSide, + size: moAmount, + price: Number.isFinite(moPrice) ? moPrice : 0, + }); + } + } else { + const topAssetId = typeof evt.asset_id === "string" ? evt.asset_id : ""; + const topSize = typeof evt.size === "number" ? evt.size : parseFloat(String(evt.size ?? "")); + const topSide = normalizeTradeSide(evt.side); + const topPrice = typeof evt.price === "number" ? evt.price : parseFloat(String(evt.price ?? "")); + const topOrderId = typeof evt.taker_order_id === "string" && evt.taker_order_id + ? evt.taker_order_id + : undefined; + if (topAssetId && topSide && Number.isFinite(topSize) && topSize > 0) { + myFills.push({ + dedupKey: tradeId, + orderId: topOrderId, + assetId: topAssetId, + side: topSide, + size: topSize, + price: Number.isFinite(topPrice) ? topPrice : 0, + }); + } + } + if (myFills.length === 0) { + console.log(`[WS.User] MINED tradeId=${tradeId} trader_side=${traderSide} no own fill matched, skipping`); + continue; + } + + // loop over each fill + for (const fill of myFills) { + if (!rememberBounded(positions.confirmedIds, positions.confirmedIdOrder, fill.dedupKey, MAX_CONFIRMED_TRADE_IDS)) { + console.log(`[WS.User] MINED duplicate dedupKey=${fill.dedupKey}, skipping`); + continue; + } + // pendingMeta is looked up in the Map directly by the fill's own orderId + // fix A: MAKER path does not consume (peek only), multiple MINED fills of the same order share the same meta; + // meta is cleaned up in sync when stratOrder is deleted (cleanupPendingTradeMetaForOrder), with 15-minute fallback aging + // TAKER path still consumes once (one trade has one taker_order_id, no "multiple fills sharing" problem) + let pendingMeta: PendingTradeMeta | null = null; + if (fill.orderId) { + const direct = pendingTradeMeta.get(fill.orderId); + if (direct) { + if (!isMaker) { + pendingTradeMeta.delete(fill.orderId); // only TAKER consumes + } + pendingMeta = direct; + } + } + if (!pendingMeta) pendingMeta = consumePendingTradeMeta(evt); + const direction = pendingMeta?.direction ?? getDirectionByAssetId(fill.assetId); + if (!(fill.assetId in positions.localSize)) positions.localSize[fill.assetId] = 0; + positions.localSize[fill.assetId] = fill.side === "buy" + ? positions.localSize[fill.assetId] + fill.size + : Math.max(0, positions.localSize[fill.assetId] - fill.size); + positions.apiVerified[fill.assetId] = false; + positions.lastTradeAt = parseTradeEventTimestamp(evt); + if (direction && Number.isFinite(fill.price) && fill.price > 0) { + notifyTradeForPnl(txHash, pendingMeta?.source ?? "manual"); + } + if (fill.side === "buy" && direction && Number.isFinite(fill.price) && fill.price > 0) { + schedulePostTradePush({ + direction, + size: fill.size, + price: fill.price, + source: pendingMeta?.source ?? "manual", + }); + } + const tradeSymbol = getSymbolByAssetId(fill.assetId).toUpperCase(); + const dirArr = direction === "up" ? "⬆" : direction === "down" ? "⬇" : "·"; + const sideZh2 = fill.side === "buy" ? "buy" : "sell"; + const role = traderSide === "MAKER" ? "M" : "T"; + console.log( + `[WS.User] 🔗 on-chain ${dirArr} ${sideZh2} ${fill.size}@${Number.isFinite(fill.price) ? fill.price : "-"}` + + ` ${tradeSymbol} (${role})` + + ` orderID=${fmtOid(fill.orderId)}` + + ` source=${pendingMeta?.source ?? "-"}` + + ` tx=${fmtOid(txHash)}` + ); + // full pendingMeta + asset_id + tradeId go to raw log + console.log("[RAW]", `[WS.User] MINED detail tradeId:${tradeId} asset:${fill.assetId} pendingMeta:${pendingMeta ? JSON.stringify(pendingMeta) : "-"}`); + + // limit-strategy TP sell order on-chain confirmation (MINED side=sell + source=strategy*tp) + if (fill.side === "sell" && pendingMeta?.source && /^strategy[a-z0-9]+tp$/i.test(pendingMeta.source) && direction) { + const strategyKey = pendingMeta.source.replace(/^strategy/i, "").replace(/tp$/i, ""); + onStrategyTakeProfitMined(strategyKey, direction, fill.size); + } + + // conditional-order TP GTC on-chain confirmation (MINED side=sell + source=cond-tp) + // accumulate cond.chainFilledSize; only delete cond + clear same-group SL when it reaches zero (avoids MATCHED over-reporting causing erroneous deletion) + if (fill.side === "sell" && pendingMeta?.source === "cond-tp" && direction) { + for (const cond of activeConditionOrders.values()) { + if (cond.kind !== "tp") continue; + if (cond.polymarketOrderId !== fill.orderId) continue; + cond.chainFilledSize = (cond.chainFilledSize ?? 0) + fill.size; + if (cond.chainFilledSize >= cond.size - 0.01) { + console.log(`[Cond.TP] MINED fully filled ${cond.direction} ${cond.size}@${cond.triggerPrice} (order:${(cond.polymarketOrderId ?? "").slice(0, 10)}...)`); + activeConditionOrders.delete(cond.id); + void clearConditionOrdersByGroup(cond.groupId, cond.id, "TP filled"); + broadcast("condFilled", { kind: "tp", direction: cond.direction, price: cond.triggerPrice }); + } else { + console.log(`[Cond.TP] MINED partial +${fill.size.toFixed(4)} on-chain ${cond.chainFilledSize.toFixed(4)}/${cond.size} (remainder still resting on PM)`); + } + broadcastConditionOrders(); + } + } + + // post-buy on-chain calibration + conditional order creation: + // - TAKER: WS size has a ~1% deviation, calibrate with getRealFillFromTx + // - MAKER: mo.matched_amount is already the exact on-chain matched amount, no calibration needed (use directly) + if (fill.side === "buy" && txHash && PROXY_ADDRESS) { + const wsSize = fill.size; + const targetAssetId = fill.assetId; + const condDir = direction; + const condEntryPrice = fill.price; + const condMeta = pendingMeta; + const skipChainCalibration = isMaker; + void (async () => { + let finalSize = wsSize; + if (skipChainCalibration) { + console.log(`[Trade.OnChain] ✓ MAKER path exact fill asset:${targetAssetId} tx:${txHash} matched_amount:${wsSize} (calibration skipped)`); + positions.apiSize[targetAssetId] = positions.localSize[targetAssetId]; + positions.apiVerified[targetAssetId] = true; + broadcastState(); + } else { + const realFill = await getRealFillFromTx(txHash, PROXY_ADDRESS); + if (realFill == null) { + console.log(`[Trade.OnChain] ⚠ calibration failed tx:${txHash} asset:${targetAssetId} will fall back to REST`); + } else { + const delta = realFill - wsSize; + if (Math.abs(delta) < 0.000001) { + console.log(`[Trade.OnChain] ✓ buy calibration asset:${targetAssetId} tx:${txHash} WS:${wsSize} = on-chain:${realFill}`); + } else { + positions.localSize[targetAssetId] = (positions.localSize[targetAssetId] ?? 0) + delta; + console.log(`[Trade.OnChain] ✓ buy calibration asset:${targetAssetId} tx:${txHash} WS:${wsSize} -> on-chain:${realFill} (delta:${delta >= 0 ? "+" : ""}${delta.toFixed(6)})`); + } + positions.apiSize[targetAssetId] = positions.localSize[targetAssetId]; + positions.apiVerified[targetAssetId] = true; + broadcastState(); + finalSize = realFill; + } + } + if (condDir && condMeta && Number.isFinite(condEntryPrice) && condEntryPrice > 0 && (condMeta.stopProfit || condMeta.stopLoss)) { + await createConditionOrdersAfterFill({ + direction: condDir, + assetId: targetAssetId, + entryPrice: condEntryPrice, + filledSize: finalSize, + windowStart: condMeta.windowStart, + stopProfit: condMeta.stopProfit, + stopLoss: condMeta.stopLoss, + }); + } + if (condMeta?.source && /^strategy[a-z0-9]+$/i.test(condMeta.source) + && !/tp$/i.test(condMeta.source) && condDir) { + const strategyKey = condMeta.source.replace(/^strategy/i, ""); + // fix C: pass in fill.orderId so onStrategyLimitMined matches exactly, avoiding cross-talk between multiple stratOrders + await onStrategyLimitMined(strategyKey, condDir, finalSize, fill.orderId); + } + })(); + } + } + broadcastState(); + } + } + } catch { /* ignore */ } + }); + + userWs.on("close", () => { + if (userWsPingTimer) clearInterval(userWsPingTimer); + const delay = backoffDelay(userWsAttempt++); + console.log(`[WS.User] disconnected, reconnecting in ${delay}ms (attempt ${userWsAttempt})`); + wsStatus.user = false; broadcastWsStatus(); + invalidatePendingLatencyOnWsDisconnect(); + if (!stopped) setTimeout(startUserWs, delay); + }); + userWs.on("error", (err) => { console.error("[WS.User] error:", err.message); }); +} + +// ── Market WS ───────────────────────────────────────────────── +let marketWs: WebSocket | null = null; +let marketPingTimer: ReturnType | null = null; +let marketRenderTimer: ReturnType | null = null; +let marketValidationTimer: ReturnType | null = null; +let lastBestBidAskTimestamp = 0; +let bestBidAskPausedUntil = 0; +let marketValidationMismatchStreak = 0; +let marketReconnectPending = false; +let marketBestReady = false; + +function isProbabilityReady(now = Date.now()): boolean { + if (!wsStatus.market) return false; + if (!marketBestReady) return false; + if (now < bestBidAskPausedUntil) return false; + const bid = Number(state.bestBid); + const ask = Number(state.bestAsk); + return Number.isFinite(bid) && Number.isFinite(ask); +} + +function parseEventTimestamp(value: unknown): number { + const n = typeof value === "number" ? value : Number(value); + return Number.isFinite(n) ? n : 0; +} + +function applyBestBidAskUpdate( + bestBid: unknown, + bestAsk: unknown, + timestamp: unknown, +): boolean { + if (typeof bestBid !== "string" || typeof bestAsk !== "string") return false; + if (Date.now() < bestBidAskPausedUntil) return false; + const ts = parseEventTimestamp(timestamp); + if (ts > 0 && ts < lastBestBidAskTimestamp) return false; + if (ts > 0) lastBestBidAskTimestamp = ts; + state.bestBid = bestBid; + state.bestAsk = bestAsk; + marketBestReady = true; + scheduleStrategyTick(); // order book update (probability change) immediately triggers a strategy check + return true; +} + +function clearProbabilityForMs(ms: number, reason: string): void { + const until = Date.now() + ms; + if (until > bestBidAskPausedUntil) bestBidAskPausedUntil = until; + marketBestReady = false; + lastBestBidAskTimestamp = 0; + state.bestBid = "-"; + state.bestAsk = "-"; + state.updatedAt = Date.now(); + console.warn(`[Health.Prob] ${reason}, clearing probability ${ms}ms`); + broadcastState(); +} + +function requestMarketReconnect(reason: string, options?: { clearProbabilityMs?: number }): void { + clearProbabilityForMs(options?.clearProbabilityMs ?? 0, reason); + marketValidationMismatchStreak = 0; + if (marketReconnectPending) return; + marketReconnectPending = true; + console.warn(`[WS.Market] triggering reconnect: ${reason}`); + if (marketWs) { + marketWs.close(); + return; + } + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + void subscribeWindow(Math.max(subscribedWindow, getCurrentWindowStart())); + }, 1000); +} + +async function validateMarketProbability(expectedWindowStart: number, upTokenId: string): Promise { + if (marketReconnectPending) return; + if (subscribedWindow !== expectedWindowStart) return; + if (!marketWs || marketWs.readyState !== WebSocket.OPEN) return; + + try { + const { bestBid, bestAsk } = await fetchBookTopOfBook(upTokenId); + if (subscribedWindow !== expectedWindowStart || upTokenId !== state.upTokenId) return; + if (!(bestBid > 0) || !(bestAsk > 0)) return; + + const wsBid = Number(state.bestBid); + const wsAsk = Number(state.bestAsk); + if (!Number.isFinite(wsBid) || !Number.isFinite(wsAsk)) return; + + const restMid = (bestBid + bestAsk) / 2; + const wsMid = (wsBid + wsAsk) / 2; + const diffPct = Math.abs(restMid - wsMid) * 100; + + if (diffPct > 3) { + marketValidationMismatchStreak++; + // print only when deviation is large (>=5%) or a reconnect is about to trigger (>=2/3), to avoid noise + if (diffPct >= 5 || marketValidationMismatchStreak >= 2) { + console.warn(`[Health.Prob] ⚠ REST deviation ${diffPct.toFixed(2)}% streak ${marketValidationMismatchStreak}/3`); + } + if (marketValidationMismatchStreak >= 3) { + console.warn(`[Health.Prob] ❌ triggering reconnect: 3/3 consecutive deviation >3%`); + requestMarketReconnect(`probability deviation >${3}% 3 times in a row`); + } + return; + } + + marketValidationMismatchStreak = 0; + } catch (err) { + if (subscribedWindow !== expectedWindowStart || upTokenId !== state.upTokenId) return; + requestMarketReconnect( + `REST validation failed: ${err instanceof Error ? err.message : String(err)}`, + { clearProbabilityMs: 2000 }, + ); + } +} + +let _marketWsConnectedOnce = false; +function startMarketWs(expectedWindowStart: number, upTokenId: string, downTokenId: string, onClose: () => void): WebSocket { + const ws = new WebSocket(MARKET_WS_URL); + ws.on("open", () => { + if (ws !== marketWs || subscribedWindow !== expectedWindowStart) return; + console.log(_marketWsConnectedOnce ? "[WS.Market] reconnect succeeded" : "[WS.Market] connected"); + _marketWsConnectedOnce = true; + marketReconnectPending = false; + marketValidationMismatchStreak = 0; + marketBestReady = false; + wsStatus.market = true; broadcastWsStatus(); + ws.send(JSON.stringify({ + assets_ids: [upTokenId, downTokenId], + type: "market", + custom_feature_enabled: true, + })); + marketRenderTimer = setInterval(broadcastState, 1000); + marketValidationTimer = setInterval(() => { + void validateMarketProbability(expectedWindowStart, upTokenId); + }, 1000); + marketPingTimer = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) ws.send("PING"); + }, 10000); + }); + ws.on("message", (data) => { + if (ws !== marketWs || subscribedWindow !== expectedWindowStart || state.upTokenId !== upTokenId) return; + const msg = data.toString(); + if (msg === "PONG" || msg === "[]") return; + try { + const events = Array.isArray(JSON.parse(msg)) ? JSON.parse(msg) : [JSON.parse(msg)]; + for (const evt of events) { + if (evt.bids !== undefined && evt.asks !== undefined) { + if (evt.asset_id && evt.asset_id !== upTokenId) continue; + state.bids.clear(); state.asks.clear(); + for (const b of (evt.bids as { price: string; size: string }[])) { + if (Number(b.size) > 0) state.bids.set(b.price, b.size); + } + for (const a of (evt.asks as { price: string; size: string }[])) { + if (Number(a.size) > 0) state.asks.set(a.price, a.size); + } + state.updatedAt = Date.now(); + broadcastState(); + } else if (evt.event_type === "best_bid_ask") { + if (evt.asset_id && evt.asset_id !== upTokenId) continue; + if (!applyBestBidAskUpdate(evt.best_bid, evt.best_ask, evt.timestamp)) continue; + state.updatedAt = Date.now(); + broadcastState(); + } else if (evt.event_type === "price_change" && evt.price_changes) { + for (const change of evt.price_changes as Record[]) { + if (change.asset_id !== upTokenId) continue; + if (change.price && change.size !== undefined) { + state.lastPrice = Number(change.price).toFixed(2); + state.lastSide = change.side; + // sync update of order book depth + const size = Number(change.size); + const map = change.side === 'BUY' ? state.bids : state.asks; + if (size > 0) map.set(change.price, change.size); + else map.delete(change.price); + } + } + state.updatedAt = Date.now(); + broadcastState(); + } + } + } catch { /* ignore */ } + }); + ws.on("close", () => { + if (ws !== marketWs || subscribedWindow !== expectedWindowStart) return; + if (marketPingTimer) clearInterval(marketPingTimer); + if (marketRenderTimer) clearInterval(marketRenderTimer); + if (marketValidationTimer) { + clearInterval(marketValidationTimer); + marketValidationTimer = null; + } + marketBestReady = false; + state.bestBid = "-"; + state.bestAsk = "-"; + state.updatedAt = Date.now(); + console.log("[WS.Market] connection lost, reconnecting in 1 second"); + wsStatus.market = false; broadcastWsStatus(); + broadcastState(); + broadcast("marketDown", {}); + onClose(); + }); + ws.on("error", (err) => { console.error("[WS.Market] error:", err.message); }); + return ws; +} + +// ── Chainlink WS ────────────────────────────────────────────── +let chainlinkWs: WebSocket | null = null; + +function startChainlinkWs(expectedWindowStart: number, eventSlug: string, onClose: () => void, attempt = 0): WebSocket { + const ws = new WebSocket(CHAINLINK_WS_URL); + ws.on("open", () => { + if (ws !== chainlinkWs || subscribedWindow !== expectedWindowStart) return; + console.log(attempt === 0 ? "[WS.OnChainPrice] connected" : "[WS.OnChainPrice] reconnect succeeded"); + wsStatus.chainlink = true; broadcastWsStatus(); + ws.send(JSON.stringify({ + action: "subscribe", + subscriptions: [ + { topic: "crypto_prices_chainlink", type: "update", filters: JSON.stringify({ symbol: activeMarket.chainlinkSymbol }) }, + { topic: "activity", type: "orders_matched", filters: JSON.stringify({ event_slug: eventSlug }) }, + ], + })); + }); + ws.on("message", (data) => { + if (ws !== chainlinkWs || subscribedWindow !== expectedWindowStart) return; + try { + const msg = JSON.parse(data.toString()) as { topic?: string; type?: string; timestamp?: number; payload?: { value?: number; timestamp?: number } }; + if (msg.topic === "crypto_prices_chainlink" && msg.type === "update") { + const val = msg.payload?.value; + if (val != null) { + state.currentPrice = val; + const now = msg.payload?.timestamp ?? msg.timestamp ?? Date.now(); + state.priceHistory.push({ t: now, price: val }); + trimHistory(state.priceHistory, now - HISTORY_RETENTION_MS, MAX_CHAINLINK_HISTORY_POINTS); + maybeInitializeBinanceOffset(); + broadcast("chainlinkPrice", { t: now, price: val }); + broadcastState(); + scheduleStrategyTick(); // price update immediately triggers a strategy check + } + } + } catch { /* ignore */ } + }); + ws.on("close", () => { + if (ws !== chainlinkWs || subscribedWindow !== expectedWindowStart) return; + const delay = backoffDelay(attempt); + console.log(`[WS.OnChainPrice] connection lost, reconnecting in ${delay}ms (attempt ${attempt + 1})`); + wsStatus.chainlink = false; broadcastWsStatus(); + broadcast("chainlinkDown", {}); + onClose(); + }); + ws.on("error", (err) => { console.error("[WS.OnChainPrice] error:", err.message); }); + return ws; +} + +// -- Binance WS --------------------------------------------------- +let binanceWs: WebSocket | null = null; +let binanceWsAttempt = 0; + +function updateKlineArray(arr: Kline[], k: Kline, maxSize: number): void { + const last = arr[arr.length - 1]; + if (last && last.openTime === k.openTime) { + // within the same kline update + arr[arr.length - 1] = k; + } else { + arr.push(k); + if (arr.length > maxSize) arr.splice(0, arr.length - maxSize); + } +} + +/** pull historical klines from Binance REST (for startup pre-fill and gap-filling after reconnect) */ +async function fetchHistoricalKlines(interval: "1m" | "5m", limit: number): Promise { + try { + const url = `https://api.binance.com/api/v3/klines?symbol=${activeMarket.binanceSymbol.toUpperCase()}&interval=${interval}&limit=${limit}`; + const res = await fetch(url); + if (!res.ok) return null; + const raw = await res.json() as Array>; + return raw.map((r) => ({ + openTime: Number(r[0]), + open: parseFloat(String(r[1])), + high: parseFloat(String(r[2])), + low: parseFloat(String(r[3])), + close: parseFloat(String(r[4])), + volume: parseFloat(String(r[5])), + closed: true, // everything REST returns is already closed + })); + } catch (err) { + console.warn(`[Price.Binance] failed to pull historical ${interval} klines: ${(err as Error).message}`); + return null; + } +} + +/** merge historical klines into the existing array, dedupe and keep the latest maxSize bars */ +function mergeKlines(existing: Kline[], fetched: Kline[], maxSize: number): void { + const map = new Map(); + for (const k of existing) map.set(k.openTime, k); + for (const k of fetched) { + // historical data only overwrites when the current bar is absent or not yet closed + const curr = map.get(k.openTime); + if (!curr || !curr.closed) map.set(k.openTime, k); + } + const sorted = [...map.values()].sort((a, b) => a.openTime - b.openTime); + existing.length = 0; + const start = Math.max(0, sorted.length - maxSize); + for (let i = start; i < sorted.length; i++) existing.push(sorted[i]); +} + +async function loadHistoricalKlines(attempt = 1): Promise { + const [k1m, k5m] = await Promise.all([ + fetchHistoricalKlines("1m", MAX_KLINE_1M), + fetchHistoricalKlines("5m", MAX_KLINE_5M), + ]); + if (k1m) { + mergeKlines(state.kline1m, k1m, MAX_KLINE_1M); + console.log(`[Price.Binance] 1m kline pre-fill ${state.kline1m.length} bars`); + } + if (k5m) { + mergeKlines(state.kline5m, k5m, MAX_KLINE_5M); + console.log(`[Price.Binance] 5m kline pre-fill ${state.kline5m.length} bars`); + } + // if either fails, retry with exponential backoff, up to 5 times (3s / 6s / 12s / 24s / 48s) + const need1m = !k1m && state.kline1m.length < MAX_KLINE_1M; + const need5m = !k5m && state.kline5m.length < MAX_KLINE_5M; + if ((need1m || need5m) && attempt <= 5) { + const delayMs = Math.min(48000, 3000 * Math.pow(2, attempt - 1)); + console.warn(`[Price.Binance] kline load failed (1m=${!!k1m} 5m=${!!k5m}), retrying in ${delayMs/1000}s ${attempt}/5`); + setTimeout(() => { void loadHistoricalKlines(attempt + 1); }, delayMs); + } else if (need1m || need5m) { + console.error(`[Price.Binance] kline load failed 5 times in a row, giving up. Strategies may not work properly.`); + } +} + +function startBinanceWs(): void { + const url = getBinanceWsUrl(activeMarket.key); + console.log(`[WS.Binance] connecting ${activeMarket.symbol} -> ${url}`); + const ws = new WebSocket(url); + binanceWs = ws; + ws.on("open", () => { + console.log(binanceWsAttempt === 0 ? `[WS.Binance] connected (${activeMarket.symbol})` : `[WS.Binance] reconnect succeeded (${activeMarket.symbol})`); + binanceWsAttempt = 0; + wsStatus.binance = true; + broadcastWsStatus(); + // after connecting, asynchronously pull historical klines to fill / patch gaps + void loadHistoricalKlines(); + }); + ws.on("message", (data) => { + // when switching markets, the old ws may still push messages while closing; discard to avoid polluting the new market's binanceHistory + if (ws !== binanceWs) return; + try { + const raw = JSON.parse(data.toString()) as { stream?: string; data?: Record }; + const stream = raw.stream; + const payload = raw.data; + if (!stream || !payload) return; + + if (stream.endsWith("@aggTrade")) { + const p = payload as { p?: string; T?: number }; + const price = parseFloat(p.p ?? ""); + const t = p.T ?? Date.now(); + if (!price) return; + state.binanceHistory.push({ t, price }); + trimHistory(state.binanceHistory, t - HISTORY_RETENTION_MS, MAX_BINANCE_HISTORY_POINTS); + recomputeVolPct(t); + maybeInitializeBinanceOffset(); + broadcast("binancePrice", { t, price }); + scheduleStrategyTick(); // price change immediately triggers a strategy check + return; + } + + if (stream.endsWith("@kline_1m") || stream.endsWith("@kline_5m")) { + const kData = (payload as { k?: Record }).k; + if (!kData) return; + const kline: Kline = { + openTime: Number(kData.t), + open: parseFloat(String(kData.o)), + high: parseFloat(String(kData.h)), + low: parseFloat(String(kData.l)), + close: parseFloat(String(kData.c)), + volume: parseFloat(String(kData.v)), + closed: Boolean(kData.x), + }; + if (stream.endsWith("@kline_1m")) { + updateKlineArray(state.kline1m, kline, MAX_KLINE_1M); + } else { + updateKlineArray(state.kline5m, kline, MAX_KLINE_5M); + } + scheduleStrategyTick(); // kline update immediately triggers a strategy check + return; + } + } catch { /* ignore */ } + }); + ws.on("close", () => { + if (ws !== binanceWs) return; // an old ws close event should not trigger a reconnect + const delay = backoffDelay(binanceWsAttempt++); + console.log(`[WS.Binance] disconnected, reconnecting in ${delay}ms (attempt ${binanceWsAttempt})`); + wsStatus.binance = false; broadcastWsStatus(); + if (!stopped) setTimeout(startBinanceWs, delay); + }); + ws.on("error", (err) => { console.error("[WS.Binance] error:", err.message); }); +} + +// ── Coinbase WS ─────────────────────────────────────────────── +let coinbaseWs: WebSocket | null = null; +let coinbaseWsAttempt = 0; + +function startCoinbaseWs(): void { + console.log(`[WS.Coinbase] connecting ${activeMarket.coinbaseProduct}`); + const ws = new WebSocket(COINBASE_WS_URL); + coinbaseWs = ws; + ws.on("open", () => { + console.log(coinbaseWsAttempt === 0 ? `[WS.Coinbase] connected (${activeMarket.coinbaseProduct})` : `[WS.Coinbase] reconnect succeeded (${activeMarket.coinbaseProduct})`); + coinbaseWsAttempt = 0; + wsStatus.coinbase = true; + broadcastWsStatus(); + // subscribe to the ticker for the current market (pushes the price on every trade) + ws.send(JSON.stringify({ + type: "subscribe", + product_ids: [activeMarket.coinbaseProduct], + channels: ["ticker"], + })); + }); + ws.on("message", (data) => { + // when switching markets, the old ws may still push messages while closing; discard to avoid polluting the new market's coinbaseHistory + if (ws !== coinbaseWs) return; + try { + const raw = JSON.parse(data.toString()) as Record; + // ticker push: { type: "ticker", price: "...", time: "ISO", product_id: "BTC-USD", ... } + if (raw.type !== "ticker") return; + const price = typeof raw.price === "string" ? parseFloat(raw.price) : (typeof raw.price === "number" ? raw.price : NaN); + if (!Number.isFinite(price) || price <= 0) return; + const tStr = typeof raw.time === "string" ? raw.time : null; + const t = tStr ? Date.parse(tStr) : Date.now(); + state.coinbaseHistory.push({ t, price }); + trimHistory(state.coinbaseHistory, t - HISTORY_RETENTION_MS, MAX_COINBASE_HISTORY_POINTS); + maybeInitOffset(COINBASE_SPEC); + broadcast("coinbasePrice", { t, price }); + } catch { /* ignore */ } + }); + ws.on("close", () => { + if (ws !== coinbaseWs) return; + const delay = backoffDelay(coinbaseWsAttempt++); + console.log(`[WS.Coinbase] disconnected, reconnecting in ${delay}ms (attempt ${coinbaseWsAttempt})`); + wsStatus.coinbase = false; broadcastWsStatus(); + if (!stopped) setTimeout(startCoinbaseWs, delay); + }); + ws.on("error", (err) => { console.error("[WS.Coinbase] error:", err.message); }); +} + +// -- Last 4 rounds result query --------------------------------------- +function parseResolvedOutcome(event: Record | undefined): "up" | "down" | null { + const market = ((event?.markets as Record[] | undefined) || [])[0]; + if (!market) return null; + + let outcomes: string[] = []; + let outcomePrices: string[] = []; + + try { outcomes = JSON.parse(String(market.outcomes || "[]")) as string[]; } catch { /* ignore */ } + try { outcomePrices = JSON.parse(String(market.outcomePrices || "[]")) as string[]; } catch { /* ignore */ } + + if (!outcomes.length || outcomes.length !== outcomePrices.length) return null; + + const upIdx = outcomes.findIndex((o) => o.toLowerCase() === "up"); + const downIdx = outcomes.findIndex((o) => o.toLowerCase() === "down"); + if (upIdx < 0 || downIdx < 0) return null; + + const upPrice = Number(outcomePrices[upIdx]); + const downPrice = Number(outcomePrices[downIdx]); + if (!Number.isFinite(upPrice) || !Number.isFinite(downPrice)) return null; + + if (upPrice >= 0.999 && downPrice <= 0.001) return "up"; + if (downPrice >= 0.999 && upPrice <= 0.001) return "down"; + return null; +} + +async function fetchRecentResults(currentWindow: number, immediate = false): Promise { + if (!immediate) await new Promise(r => setTimeout(r, 5000)); + if (stopped) return; + try { + const slugs = [1,2,3,4].map(i => `${activeMarket.slugPrefix}-${currentWindow - i * activeMarket.periodSeconds}`); + const query = slugs.map(s => `slug=${s}`).join("&"); + const events = await fetch(`${GAMMA_URL}/events?${query}`).then(r => r.json()) as Record[]; + const results = slugs.map(slug => { + const event = events.find((e: Record) => e.slug === slug) as Record | undefined; + const ws = parseInt(slug.split("-").pop()!); + const timeRange = `${new Date(ws*1000).toLocaleTimeString("zh-CN",{timeZone:"Asia/Shanghai",hour:'2-digit',minute:'2-digit'})}→${new Date((ws+300)*1000).toLocaleTimeString("zh-CN",{timeZone:"Asia/Shanghai",hour:'2-digit',minute:'2-digit'})}`; + const result = parseResolvedOutcome(event); + return { timeRange, result }; + }); + const summary = results + .map((item) => `${item.timeRange}${item.result === "up" ? "Up won" : item.result === "down" ? "Down won" : "pending"}`) + .join(" | "); + console.log(`[Settlement] ${summary}`); + broadcast("recentResults", { results }); + } catch (e) { console.error(`[Settlement] request failed:`, (e as Error).message); } +} + +// -- Window switch ----------------------------------------------- +let subscribedWindow = 0; +let stopped = false; +let switchTimer: ReturnType | null = null; +let reconnectTimer: ReturnType | null = null; + +function disconnectWindowStreams(): void { + const hadMarketFeed = !!marketWs || wsStatus.market; + const hadChainlinkFeed = !!chainlinkWs || wsStatus.chainlink; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + if (marketPingTimer) { + clearInterval(marketPingTimer); + marketPingTimer = null; + } + if (marketRenderTimer) { + clearInterval(marketRenderTimer); + marketRenderTimer = null; + } + if (marketValidationTimer) { + clearInterval(marketValidationTimer); + marketValidationTimer = null; + } + if (marketWs) { + marketWs.removeAllListeners("close"); + marketWs.close(); + marketWs = null; + } + if (chainlinkWs) { + chainlinkWs.removeAllListeners("close"); + chainlinkWs.close(); + chainlinkWs = null; + } + if (wsStatus.market || wsStatus.chainlink) { + wsStatus.market = false; + wsStatus.chainlink = false; + broadcastWsStatus(); + } + if (hadMarketFeed) broadcast("marketDown", {}); + if (hadChainlinkFeed) broadcast("chainlinkDown", {}); +} + +function clearWindowRuntimeState(): void { + state.bids.clear(); + state.asks.clear(); + state.bestBid = "-"; + state.bestAsk = "-"; + state.lastPrice = "-"; + state.lastSide = ""; + state.priceToBeat = null; + state.currentPrice = null; + state.binanceOffset = null; + state.coinbaseOffset = null; + state.updatedAt = Date.now(); + marketBestReady = false; + marketValidationMismatchStreak = 0; + bestBidAskPausedUntil = 0; + strategyRuntime.positionsReady = !PROXY_ADDRESS; + resetStrategyRuntime(); + broadcastState(); +} + +function getCurrentWindowStart(now = Date.now()): number { + const sec = activeMarket.periodSeconds; + return Math.floor(now / 1000 / sec) * sec; +} + +// -- Multi-symbol / multi-period market switch ------------------------- +// pre-switch checks (against the **target market's** current window): +// 1. current position = 0 +// 2. current open orders = 0 +// 3. strategy runtime = IDLE +// note: positions/open orders in the old market are allowed to remain - 5m/15m markets auto-settle at expiry +async function switchMarket(targetKey: MarketKey): Promise<{ ok: boolean; reason?: string }> { + if (targetKey === activeMarket.key) return { ok: true }; + if (!MARKETS[targetKey]) return { ok: false, reason: "unknown market" }; + + // check 1: current market position is 0 + const upHas = (positions.localSize[state.upTokenId] || 0) > 0 || (positions.apiSize[state.upTokenId] || 0) > 0; + const downHas = (positions.localSize[state.downTokenId] || 0) > 0 || (positions.apiSize[state.downTokenId] || 0) > 0; + if (upHas || downHas) { + return { ok: false, reason: "holding a position, please close it before switching" }; + } + + // check 2: current market has no open orders + const hasOpenOrder = [...activeConditionOrders.values()].some( + (c) => c.assetId === state.upTokenId || c.assetId === state.downTokenId + ); + if (hasOpenOrder) { + return { ok: false, reason: "there are open orders, please cancel them first" }; + } + + // check 3: the strategy state machine must be idle + if (strategyRuntime.state !== "IDLE") { + return { ok: false, reason: `strategy is running (${strategyRuntime.state}), please wait for it to finish` }; + } + + console.log(`[System.Market] switching ${activeMarket.key} -> ${targetKey}`); + + // close the market-bound WS (market/chainlink/binance/coinbase) + // keep the user WS, CLOB client, and USDC monitoring + disconnectWindowStreams(); + if (binanceWs) { + binanceWs.removeAllListeners("close"); + binanceWs.close(); + binanceWs = null; + wsStatus.binance = false; + } + if (coinbaseWs) { + coinbaseWs.removeAllListeners("close"); + coinbaseWs.close(); + coinbaseWs = null; + wsStatus.coinbase = false; + } + broadcastWsStatus(); + + // clear window-level runtime state + historical price / klines + clearWindowRuntimeState(); + state.priceHistory.length = 0; + state.binanceHistory.length = 0; + state.coinbaseHistory.length = 0; + state.kline1m.length = 0; + state.kline5m.length = 0; + pendingTradeMeta.clear(); + positions.lastTradeAt = null; // the old market's fill time is no longer relevant + subscribedWindow = 0; + + // switch activeMarket and persist + activeMarket = MARKETS[targetKey]; + setFairProbMarket(activeMarket.symbol, activeMarket.period); // sync to the fair-prob table + setDiffExtremesMarket(activeMarket.symbol, activeMarket.period); // sync to the diff-extremes table + // when the current market has no table, the relevant strategies (p series, etc.) auto no-op even if enabled + saveActiveMarketKey(targetKey); + + // restart the 4 upstream WS (user is left untouched) + startBinanceWs(); + startCoinbaseWs(); + await advanceToLiveWindow(getCurrentWindowStart()); + + broadcast("marketSwitched", { key: activeMarket.key, displayName: activeMarket.displayName }); + broadcastState(); + console.log(`[System.Market] switch complete -> ${activeMarket.displayName}`); + return { ok: true }; +} + +async function advanceToLiveWindow(targetWindowStart: number): Promise { + const switchStartedAt = Date.now(); + let attempt = 0; + let clearedExpiredWindow = false; + while (!stopped) { + const desiredWindow = Math.max(targetWindowStart, getCurrentWindowStart()); + if (!clearedExpiredWindow && desiredWindow > subscribedWindow) { + disconnectWindowStreams(); + clearWindowRuntimeState(); + clearedExpiredWindow = true; + } + const subscribeStartedAt = Date.now(); + await subscribeWindow(desiredWindow); + if (subscribedWindow === desiredWindow) { + // summarize window-switch info: elapsed + offset + open price + const fmtTs = (ts: number) => new Date(ts * 1000).toLocaleTimeString("zh-CN", { timeZone: "Asia/Shanghai", hour12: false }); + const cost = Date.now() - switchStartedAt; + const binOff = state.binanceOffset; + const cbOff = state.coinbaseOffset; + const open = state.priceToBeat; + const parts: string[] = []; + if (binOff != null) parts.push(`Binance:${binOff >= 0 ? "+" : ""}${binOff.toFixed(2)}`); + if (cbOff != null) parts.push(`Coinbase:${cbOff >= 0 ? "+" : ""}${cbOff.toFixed(2)}`); + if (open != null) parts.push(`open:${open.toFixed(0)}`); + const tail = parts.length ? `(${parts.join(" ")})` : ""; + console.log(`[System.Window] ✓ switch ${fmtTs(subscribedWindow)} -> ${fmtTs(desiredWindow)} elapsed ${cost}ms${tail}`); + return; + } + + const delay = Math.min(1000 * Math.max(++attempt, 1), 5000); + console.warn(`[System.Window] switch retry windowStart=${desiredWindow} continuing in ${delay}ms`); + await new Promise(r => setTimeout(r, delay)); + } +} + +function scheduleNextWindow(windowEnd: number): void { + if (switchTimer) clearTimeout(switchTimer); + const msUntilEnd = windowEnd * 1000 - Date.now(); + switchTimer = setTimeout(async () => { + if (stopped) return; + await advanceToLiveWindow(windowEnd); + }, Math.max(0, msUntilEnd)); +} + +async function subscribeWindow(windowStart: number): Promise { + const startedAt = Date.now(); + const info = await fetchMarket(windowStart); + if (!info) { + broadcast("error", { message: `market not found windowStart=${windowStart}` }); + console.warn(`[System.Window] subscribe failed windowStart=${windowStart} elapsed:${Date.now() - startedAt}ms`); + return; + } + + const isNewWindow = subscribedWindow !== windowStart; + const prevWindowStart = subscribedWindow; + subscribedWindow = windowStart; + + const slug = `${activeMarket.slugPrefix}-${info.windowStart}`; + state.windowStart = info.windowStart; state.windowEnd = info.windowEnd; + state.upTokenId = info.upTokenId; state.downTokenId = info.downTokenId; + state.conditionId = info.conditionId; state.slug = slug; + // pre-fetch tickSize (BTC 5m fixed at 0.01, also async-validate via API) + getCachedTickSize(info.upTokenId); + getCachedTickSize(info.downTokenId); + state.bids.clear(); state.asks.clear(); + state.bestBid = "-"; state.bestAsk = "-"; + state.lastPrice = "-"; state.lastSide = ""; + state.binanceOffset = null; + state.coinbaseOffset = null; + state.updatedAt = Date.now(); + lastBestBidAskTimestamp = 0; + marketBestReady = false; + bestBidAskPausedUntil = 0; + marketValidationMismatchStreak = 0; + marketReconnectPending = false; + + if (isNewWindow) { + if (prevWindowStart > 0) fetchRecentResults(windowStart); + state.priceToBeat = null; state.currentPrice = null; + strategyRuntime.positionsReady = !PROXY_ADDRESS; + clearAllPresigned(); + resetStrategyRuntime(`switching to window ${windowStart}`); + rememberTokenSymbol(info.upTokenId, activeMarket.symbol); + rememberTokenSymbol(info.downTokenId, activeMarket.symbol); + prunePositionCaches([info.upTokenId, info.downTokenId]); + positions.localSize[info.upTokenId] = 0; + positions.localSize[info.downTokenId] = 0; + positions.apiSize[info.upTokenId] = 0; + positions.apiSize[info.downTokenId] = 0; + positions.apiVerified[info.upTokenId] = false; + positions.apiVerified[info.downTokenId] = false; + // window switch: cancel all GTC take-profit resting orders and clear the local conditional order list + for (const cond of activeConditionOrders.values()) { + if (cond.kind === "tp" && cond.polymarketOrderId) { + void cancelTakeProfitOrder(cond.polymarketOrderId); + } + } + activeConditionOrders.clear(); + broadcastConditionOrders(); + // window switch: cancel all strategy limit orders (the same window already settled, orders are void but cancel anyway to be safe) + for (const order of strategyLimitOrders.values()) { + if (order.windowStart !== windowStart) { + void cancelStrategyLimitOrder(order.orderID, "window switch"); + } + } + // clear the "already placed" markers, the new window can trigger again + strategyLimitWindowMark.clear(); + const thisWindow = info.windowStart; + const tryFetch = () => { + if (stopped || subscribedWindow !== thisWindow) return; + fetchCryptoPrice(info.eventStartTime, info.endDate).then(() => { + if (state.priceToBeat == null && !stopped && subscribedWindow === thisWindow) setTimeout(tryFetch, 1000); + else broadcastState(); + }); + }; + tryFetch(); + syncPositionsFromApi().then(() => broadcastState()); + } + + broadcast("window", { + windowStart: info.windowStart, windowEnd: info.windowEnd, + conditionId: info.conditionId, upTokenId: info.upTokenId, downTokenId: info.downTokenId, + }); + + if (marketWs || chainlinkWs || reconnectTimer) { + disconnectWindowStreams(); + } + + marketWs = startMarketWs(info.windowStart, info.upTokenId, info.downTokenId, () => { + if (stopped) return; + if (reconnectTimer) clearTimeout(reconnectTimer); + reconnectTimer = setTimeout(() => { + void subscribeWindow(Math.max(subscribedWindow, getCurrentWindowStart())); + }, 1000); + }); + + const eventSlug = `${activeMarket.slugPrefix}-${info.windowStart}`; + let clAttempt = 0; + const reconnectChainlink = () => { + if (stopped) return; + const delay = backoffDelay(clAttempt); + clAttempt++; + setTimeout(() => { + if (stopped) return; + chainlinkWs = startChainlinkWs(subscribedWindow, `${activeMarket.slugPrefix}-${subscribedWindow}`, reconnectChainlink, clAttempt); + }, delay); + }; + chainlinkWs = startChainlinkWs(info.windowStart, eventSlug, reconnectChainlink, 0); + + scheduleNextWindow(info.windowEnd); +} + +// -- Claim query ------------------------------------------------ +interface ClaimPosition { + conditionId: string; title: string; currentValue: number; size: number; +} +let claimablePositions: ClaimPosition[] = []; +let claimableTotal = 0; +let claimCycleTimer: ReturnType | null = null; +let claimCycleRunning = false; +let claimNextCheckAt = 0; +let claimCooldownUntil = 0; // Claim cooldown deadline timestamp (5 minutes) + +function broadcastClaimCooldown(running = false): void { + broadcast("claimCooldown", { + running, + nextCheckAt: claimNextCheckAt, + cooldownUntil: claimCooldownUntil, + }); +} + +function resetClaimableState(): void { + claimablePositions = []; + claimableTotal = 0; + broadcast("claimable", { total: claimableTotal, positions: claimablePositions }); +} + +async function syncClaimable(options: { clearOnError?: boolean } = {}): Promise { + if (!PROXY_ADDRESS) { + resetClaimableState(); + return false; + } + try { + const pos = await fetch( + `https://data-api.polymarket.com/positions?user=${PROXY_ADDRESS}&sizeThreshold=.01&redeemable=true&limit=100&offset=0` + ).then(r => r.json()) as Array<{ conditionId: string; title: string; currentValue: number; size: number; curPrice: number; asset?: string; oppositeAsset?: string; outcome?: string; oppositeOutcome?: string }>; + + // keep only winning positions (currentValue > 0 means that outcome is the winning direction) + // then verify with on-chain asset balance to filter out already-redeemed ones (data-api cache lag) + const winners = pos.filter(p => p.currentValue > 0 && p.asset); + let verified: typeof pos = winners; + if (winners.length) { + try { + const provider = getClaimProvider(); + const ctfRO = new ethers.Contract(getContractConfig(137).conditionalTokens, [ + 'function balanceOfBatch(address[] accounts, uint256[] ids) view returns (uint256[])', + ], provider); + const ids = winners.map(p => BigInt(p.asset!)); + const accounts = ids.map(() => PROXY_ADDRESS); + const balances: bigint[] = await ctfRO.balanceOfBatch(accounts, ids); + verified = winners.filter((_, i) => balances[i] > 0n); + } catch (e) { + console.warn(`[Claim] on-chain verification failed (falling back to currentValue>0): ${e instanceof Error ? e.message : e}`); + } + } + + claimablePositions = verified.map(p => ({ + conditionId: p.conditionId, title: p.title, currentValue: p.currentValue, size: p.size, + })); + claimableTotal = claimablePositions.reduce((s, p) => s + p.currentValue, 0); + broadcast("claimable", { total: claimableTotal, positions: claimablePositions }); + return true; + } catch (err) { + if (options.clearOnError) resetClaimableState(); + const msg = err instanceof Error ? err.message : String(err); + console.error(`[Claim] failed to query claimable positions: ${msg}`); + return false; + } +} + +function scheduleClaimCycle(delayMs = CLAIM_CYCLE_DELAY_MS): void { + if (stopped || !PROXY_ADDRESS) { + if (claimCycleTimer) clearTimeout(claimCycleTimer); + claimCycleTimer = null; + claimNextCheckAt = 0; + broadcastClaimCooldown(false); + return; + } + if (claimCycleTimer) clearTimeout(claimCycleTimer); + claimNextCheckAt = Date.now() + Math.max(0, delayMs); + broadcastClaimCooldown(false); + claimCycleTimer = setTimeout(() => { + void autoClaimCycle().catch((err) => { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[Claim.Auto] background claim exception: ${msg}`); + scheduleClaimCycle(); + }); + }, Math.max(0, delayMs)); +} + +async function autoClaimCycle(): Promise { + if (stopped || claimCycleRunning || claimInProgress) { + // the previous claim round hasn't finished (on-chain tx incomplete), skip this round, do not reset cooldown, the next timer will trigger it + if (claimInProgress && !claimCycleRunning) { + console.log(`[Claim.Auto] previous claim still in progress, skipping this loop`); + scheduleClaimCycle(); + } + return; + } + // when auto Claim is unchecked, do not query, do not broadcast, just clear the state + if (!strategyConfig.autoClaimEnabled || !PRIVATE_KEY) { + resetClaimableState(); + return; + } + claimCycleRunning = true; + claimNextCheckAt = 0; + broadcastClaimCooldown(true); + try { + // Step 1: query the latest claimable amount every loop (high-frequency refresh, frontend shows it in real time) + const synced = await syncClaimable({ clearOnError: true }); + if (!synced) return; + if (!claimablePositions.length || claimInProgress) return; + + // cooldown period: do not execute again within 5 minutes of the last claim + if (Date.now() < claimCooldownUntil) return; + + // strategy busy: entering/holding/exiting -> do not trigger a Safe transaction (avoids nonce conflicts) + const strategyBusy = strategyRuntime.state !== "IDLE" + && strategyRuntime.state !== "DONE" + && strategyRuntime.state !== "SCANNING"; + if (strategyBusy || hasOpenPosition()) { + console.log(`[Claim.Auto] strategy busy (${strategyRuntime.state}), skipping this time`); + return; + } + + // Step 3: refresh the amount once more before claiming (ensures conditionId and quantity are latest) + await syncClaimable({ clearOnError: false }); + if (!claimablePositions.length) return; + + console.log(`[Claim.Auto] detected ${claimablePositions.length} claimable positions, starting background claim...`); + claimCooldownUntil = Date.now() + CLAIM_COOLDOWN_MS; // enter cooldown (regardless of success or failure below) + await runClaim({ refreshAfter: false }); + } finally { + claimCycleRunning = false; + scheduleClaimCycle(); + } +} + +// -- Claim core logic ------------------------------------------- +let claimInProgress = false; + +// reuse the provider: avoids new-ing one on every claim, which triggers a network-probe loop +// use the full Network object + staticNetwork object version, skipping the eth_chainId probe at startup +const CLAIM_NETWORK = new ethers.Network("polygon", 137); +let cachedClaimProvider: ethers.JsonRpcProvider | null = null; + +// tickSize cache: fetched once per market at startup/window switch, reused fixed within the same window +// extreme price levels (upPct > 99%) cause Polymarket to switch to finer precision; if an order fails here, call invalidateTickSize to force a refresh +const DEFAULT_TICK_SIZE = "0.01" as const; +type TickSize = Awaited>; +const tickSizeCache = new Map(); + +/** synchronously returns the cached value; if not cached returns the default 0.01 and fetches asynchronously (fills the cache immediately after the first call) */ +function getCachedTickSize(tokenId: string): TickSize { + const cached = tickSizeCache.get(tokenId); + if (cached !== undefined) return cached; + // not cached: use the default value as a stopgap, fill the real value asynchronously (subscribeWindow pre-fetches at startup) + tickSizeCache.set(tokenId, DEFAULT_TICK_SIZE as TickSize); + void refreshTickSize(tokenId); + return DEFAULT_TICK_SIZE as TickSize; +} + +async function refreshTickSize(tokenId: string): Promise { + if (!clobClient) return DEFAULT_TICK_SIZE as TickSize; + try { + const real = await clobClient.getTickSize(tokenId); + const prev = tickSizeCache.get(tokenId); + tickSizeCache.set(tokenId, real); + if (prev !== undefined && String(prev) !== String(real)) { + console.warn(`[Price.Precision] ${tokenId.slice(-8)} changed: ${prev} -> ${real}`); + } + return real; + } catch { + return tickSizeCache.get(tokenId) ?? (DEFAULT_TICK_SIZE as TickSize); + } +} + +/** force refresh on order failure (one-sided market precision switch scenario) */ +async function invalidateTickSize(tokenId: string): Promise { + tickSizeCache.delete(tokenId); + return refreshTickSize(tokenId); +} +function getClaimProvider(): ethers.JsonRpcProvider { + if (!cachedClaimProvider) { + cachedClaimProvider = new ethers.JsonRpcProvider( + "https://polygon-bor-rpc.publicnode.com", + CLAIM_NETWORK, + { staticNetwork: CLAIM_NETWORK }, + ); + // silence RPC error (the original console.error would repeatedly print the same error) + cachedClaimProvider.on("error", () => { /* handled by caller */ }); + } + return cachedClaimProvider; +} + +async function runClaim(options: { refreshAfter?: boolean } = {}): Promise<{ title: string; txHash?: string; error?: string }[]> { + const { refreshAfter = true } = options; + if (!PROXY_ADDRESS || !PRIVATE_KEY) return []; + if (claimInProgress) return []; + if (!claimablePositions.length) return []; + claimInProgress = true; + + const contracts = getContractConfig(137); + const USDC_ADDR = contracts.collateral; // pUSD after the V2 upgrade + // V2: redeem goes through CtfCollateralAdapter (internally burns CTF token then auto-wraps into pUSD) + const ADAPTER = "0xADa100874d00e3331D00F2007a9c336a65009718"; + const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000"; + + const provider = getClaimProvider(); + const wallet = new ethers.Wallet(PRIVATE_KEY, provider); + + const ctfIface = new ethers.Interface([ + "function redeemPositions(address collateralToken, bytes32 parentCollectionId, bytes32 conditionId, uint256[] indexSets)", + "function setApprovalForAll(address operator, bool approved)", + "function isApprovedForAll(address account, address operator) view returns (bool)", + ]); + const safeIface = new ethers.Interface([ + "function nonce() view returns (uint256)", + "function getTransactionHash(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address refundReceiver, uint256 nonce) view returns (bytes32)", + "function execTransaction(address to, uint256 value, bytes calldata data, uint8 operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures) public payable returns (bool)", + ]); + const safe = new ethers.Contract(PROXY_ADDRESS, safeIface, wallet); + + const snapshot = [...claimablePositions]; + const total = snapshot.length; + const results: { title: string; txHash?: string; error?: string }[] = []; + console.log(`[Claim] starting claim, ${total} total: ${snapshot.map(p => p.title).join(' | ')}`); + // maintain nonce locally, to avoid safe.nonce() reading a stale value when the RPC node hasn't synced in time + let localNonce: bigint = await safe.nonce(); + console.log(`[Claim] starting nonce:${localNonce}`); + + // check and initiate one-time approval: the Safe must approve the Adapter to transferFrom CTF tokens to complete redeem + // not approving will throw GS013 (inner call revert: "ERC1155: need operator approval for 3rd party transfers.") + try { + const ctfRO = new ethers.Contract(contracts.conditionalTokens, [ + "function isApprovedForAll(address account, address operator) view returns (bool)", + ], provider); + const approved: boolean = await ctfRO.isApprovedForAll(PROXY_ADDRESS, ADAPTER); + if (!approved) { + console.log(`[Claim] Safe has not approved Adapter, initiating one-time approval first...`); + const approveCalldata = ctfIface.encodeFunctionData("setApprovalForAll", [ADAPTER, true]); + const approveTxHash: string = await safe.getTransactionHash(contracts.conditionalTokens, 0, approveCalldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, localNonce); + const approveSig = await wallet.signMessage(ethers.getBytes(approveTxHash)); + const approveV = parseInt(approveSig.slice(-2), 16) + 4; + const approveAdjusted = approveSig.slice(0, -2) + approveV.toString(16).padStart(2, '0'); + const approveTx = await safe.execTransaction(contracts.conditionalTokens, 0, approveCalldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, approveAdjusted); + console.log(`[Claim] approval transaction txHash:${approveTx.hash}, waiting for on-chain confirmation...`); + const approveReceipt = await Promise.race([ + approveTx.wait(), + new Promise((_, reject) => setTimeout(() => reject(new Error('approval on-chain wait timeout (30s)')), 30000)), + ]); + if (!approveReceipt) throw new Error('approval on-chain wait timeout'); + console.log(`[Claim] ✓ approval succeeded block:${approveReceipt.blockNumber}`); + localNonce++; + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`[Claim] approval failed, aborting this claim round: ${msg}`); + claimInProgress = false; + return []; + } + try { + const CLAIM_TX_INTERVAL_MS = 5000; // 5-second interval between two transactions + const CLAIM_MAX_RETRIES = 1; // retry a single failed transaction once + + for (let i = 0; i < snapshot.length; i++) { + const p = snapshot[i]; + console.log(`[Claim] (${i+1}/${total}) ${p.title} amount:${p.currentValue.toFixed(2)} conditionId:${p.conditionId}`); + broadcast("claimProgress", { current: i, total, title: p.title, status: "running" }); + + let success = false; + let lastErr = ""; + for (let attempt = 0; attempt <= CLAIM_MAX_RETRIES; attempt++) { + try { + if (attempt > 0) { + console.log(`[Claim] retry attempt ${attempt}...`); + await new Promise(r => setTimeout(r, 2000)); + // refresh nonce before retrying (the previous tx may still be unconfirmed in the mempool or already succeeded) + const onChainNonce: bigint = await safe.nonce(); + if (onChainNonce > localNonce) localNonce = onChainNonce; + } + const calldata = ctfIface.encodeFunctionData("redeemPositions", [ + USDC_ADDR, ZERO_BYTES32, p.conditionId, [1, 2] + ]); + const nonce = localNonce; + console.log(`[Claim] nonce:${nonce} building transaction...`); + const txHash = await safe.getTransactionHash(ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, nonce); + const sig = await wallet.signMessage(ethers.getBytes(txHash)); + const v = parseInt(sig.slice(-2), 16) + 4; + const adjustedSig = sig.slice(0, -2) + v.toString(16).padStart(2, '0'); + console.log(`[Claim] sending transaction...`); + const tx = await safe.execTransaction(ADAPTER, 0, calldata, 0, 0, 0, 0, ethers.ZeroAddress, ethers.ZeroAddress, adjustedSig); + console.log(`[Claim] waiting for on-chain txHash:${tx.hash}`); + const receipt = await Promise.race([ + tx.wait(), + new Promise((_, reject) => setTimeout(() => reject(new Error('on-chain wait timeout (30s)')), 30000)), + ]); + if (!receipt) throw new Error('on-chain wait timeout (30s)'); + console.log(`[Claim] ✓ success ${p.title} -> ${tx.hash}`); + results.push({ title: p.title, txHash: tx.hash }); + localNonce++; // local nonce auto-increment + broadcast("claimProgress", { current: i + 1, total, title: p.title, status: "success" }); + success = true; + break; + } catch (err) { + lastErr = err instanceof Error ? err.message : String(err); + console.error(`[Claim] ✗ attempt ${attempt + 1} failed ${p.title}: ${lastErr}`); + } + } + if (!success) { + results.push({ title: p.title, error: lastErr }); + broadcast("claimProgress", { current: i + 1, total, title: p.title, status: "error", error: lastErr }); + } + + // interval between two transactions (no wait after the last one) + if (i < snapshot.length - 1) { + await new Promise(r => setTimeout(r, CLAIM_TX_INTERVAL_MS)); + } + } + } finally { + claimInProgress = false; + } + console.log(`[Claim] complete success:${results.filter(r=>r.txHash).length} failed:${results.filter(r=>r.error).length}`); + if (refreshAfter) { + await syncClaimable({ clearOnError: true }); + await syncUsdcBalance(); + broadcastState(); + } + return results; +} + +function extractOrderError(result: unknown): string { + const obj = result && typeof result === "object" ? result as Record : {}; + const candidates = [obj.error, obj.message, obj.errorMsg, obj.errorMessage]; + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.trim()) return candidate.trim(); + } + return ""; +} + +function fmtOrderField(value: unknown): string { + if (value == null || value === "") return "-"; + return String(value); +} + +function getDecimalPlaces(value: string | number): number { + const text = String(value); + const [, decimals = ""] = text.split("."); + return decimals.replace(/0+$/, "").length; +} + +function floorToDecimals(value: number, decimals: number): number { + const factor = 10 ** decimals; + return Math.floor((value + Number.EPSILON) * factor) / factor; +} + +function isOrderWindowStale(now = Date.now()): boolean { + if (!state.windowStart || !state.windowEnd) return true; + if (state.windowEnd * 1000 <= now) return true; + return state.windowStart < getCurrentWindowStart(now); +} + +function getStrategyRemainingSeconds(now = Date.now()): number { + return state.windowEnd ? state.windowEnd - Math.floor(now / 1000) : 0; +} + +function buildTickContext(rem: number, upPct: number | null, dnPct: number | null, diff: number | null, now: number): import("./strategies/types.js").StrategyTickContext { + // diffBps: a cross-symbol generic percentage (per ten-thousand, i.e. bps). + // BTC ~$70000, ETH ~$3000, SOL ~$200, the dollar diff magnitude varies hugely; using bps lets thresholds be reused across markets. + let diffBps: number | null = null; + if (diff != null && state.priceToBeat != null && state.priceToBeat > 0) { + diffBps = Math.round((diff / state.priceToBeat) * 10000 * 100) / 100; + } + return { + rem, upPct, dnPct, diff, diffBps, volPct: state.volPct, now, + prevUpPct: strategyRuntime.prevUpPct, + kline1m: state.kline1m, + kline5m: state.kline5m, + marketHoursOnly: strategyConfig.marketHoursOnly, + }; +} + +// whether the current activeMarket is in the strategy's declared supportedMarkets list +// not declared / empty array = supports all markets (backward compatible with old strategies) +function isStrategySupportedHere(s: import("./strategies/types.js").IStrategy): boolean { + const desc = s.getDescription(); + const list = desc.supportedMarkets; + if (!list || list.length === 0) return true; + return list.includes(activeMarket.key); +} + +function checkEntry(ctx: import("./strategies/types.js").StrategyTickContext): { strategy: StrategyKey; dir: StrategyDirection } | null { + for (const s of getAllStrategies()) { + if (!strategyConfig.enabled[s.key]) continue; + if (!isStrategySupportedHere(s)) continue; // does not support the current market, skip + const signal = s.checkEntry(ctx); + if (signal) return { strategy: s.key, dir: signal.direction }; + } + return null; +} + +function checkExit(ctx: import("./strategies/types.js").StrategyTickContext): import("./strategies/types.js").ExitSignal { + const key = strategyRuntime.activeStrategy; + const direction = strategyRuntime.direction; + if (!key || !direction) return null; + const s = getStrategy(key); + if (!s) return null; + return s.checkExit(ctx, direction); +} + +interface PlaceOrderInput { + direction: StrategyDirection; + side: "buy" | "sell"; + amount: number; + slippage?: number; + source?: string; + exitReason?: string; + roundEntry?: string; + // conditional orders (only carried on buy, created after the fill triggers) + stopProfit?: { pctDelta?: number; targetPrice?: number }; + stopLoss?: { pctDelta?: number; diffValue?: number; slippage?: number }; +} + +interface OrderExecutionResult { + success: boolean; + statusCode: number; + body: Record; + errorMessage?: string; +} + +async function placeOrder(input: PlaceOrderInput): Promise { + const { direction, side, amount, source = "manual" } = input; + const slippageVal = typeof input.slippage === "number" && input.slippage >= 0 + ? input.slippage + : strategyConfig.slippage; + const orderTag = `[Order:${source}]`; + + if (!direction || !side || !amount || amount <= 0) { + return { success: false, statusCode: 400, body: { error: "invalid params" }, errorMessage: "invalid params" }; + } + if (!(await ensureClobClient())) { + return { + success: false, + statusCode: 500, + body: { error: "CLOB client not initialized, please check POLYMARKET_PRIVATE_KEY" }, + errorMessage: "CLOB client not initialized, please check POLYMARKET_PRIVATE_KEY", + }; + } + if (isOrderWindowStale()) { + return { + success: false, + statusCode: 409, + body: { error: "the current market window has expired, waiting to switch to a new window" }, + errorMessage: "the current market window has expired, waiting to switch to a new window", + }; + } + if (!isProbabilityReady()) { + return { + success: false, + statusCode: 409, + body: { error: "order book probability temporarily unavailable, waiting for WS to recover" }, + errorMessage: "order book probability temporarily unavailable, waiting for WS to recover", + }; + } + + const tokenId = direction === "up" ? state.upTokenId : state.downTokenId; + if (!tokenId) { + return { + success: false, + statusCode: 400, + body: { error: "the current window market is not ready" }, + errorMessage: "the current window market is not ready", + }; + } + + // prefer the in-memory order book pushed by Market WS (saves ~200ms HTTP); fall back to GET when stale or not ready + // note: state.bestBid / state.bestAsk only track the up token (see the Market WS book event filtering upTokenId). + // binary option identity: up + down = 1 -> down bid = 1 - up ask, down ask = 1 - up bid. + // + // fast-path guard: only require the side's price needed by this order to be valid (buy -> ask / sell -> bid), + // allow the fast path even in a one-sided market (the other side has 0 depth), to avoid unnecessary fallback. + let bestBid = 0; + let bestAsk = 0; + const BOOK_MAX_AGE_MS = 3000; + const bookAgeMs = lastBestBidAskTimestamp > 0 ? Date.now() - lastBestBidAskTimestamp : Infinity; + const stateBid = Number(state.bestBid); + const stateAsk = Number(state.bestAsk); + // convert to the target token's order book based on the order direction first + let candidateBid = 0; + let candidateAsk = 0; + if (Number.isFinite(stateBid) && Number.isFinite(stateAsk)) { + if (direction === "up") { + candidateBid = stateBid; + candidateAsk = stateAsk; + } else { + // down conversion: if the up side is 0, the conversion result 1-0=1 is an unrealistic "full price", treated as invalid + candidateBid = stateAsk > 0 ? 1 - stateAsk : 0; + candidateAsk = stateBid > 0 ? 1 - stateBid : 0; + } + } + const neededForSide = side === "buy" ? candidateAsk : candidateBid; + const wsUsable = marketBestReady && bookAgeMs < BOOK_MAX_AGE_MS && neededForSide > 0; + if (wsUsable) { + bestBid = candidateBid; + bestAsk = candidateAsk; + } else { + // fallback: GET on the spot when the WS order book is not ready / stale / the target side's price is missing + try { + ({ bestBid, bestAsk } = await fetchBookTopOfBook(tokenId)); + console.log(`${orderTag} WS order book fallback GET (age=${bookAgeMs}ms ready=${marketBestReady} need=${neededForSide}) bid=${bestBid} ask=${bestAsk}`); + } catch { + return { + success: false, + statusCode: 500, + body: { error: "unable to fetch order book price" }, + errorMessage: "unable to fetch order book price", + }; + } + } + // buy orders need bestAsk (counterparty's sell price); sell orders need bestBid (counterparty's buy price). In extreme markets the other side may be 0. + const needed = side === "buy" ? bestAsk : bestBid; + if (!(needed > 0)) { + return { + success: false, + statusCode: 500, + body: { error: side === "buy" ? "no sell orders in the book (insufficient depth)" : "no buy orders in the book (insufficient depth)" }, + errorMessage: side === "buy" ? "no sell orders in the book (insufficient depth)" : "no buy orders in the book (insufficient depth)", + }; + } + + const worstPrice = side === "buy" + ? Math.min(bestAsk + slippageVal, 0.99) + : Math.max(bestBid - slippageVal, 0.01); + + try { + const tickSize = getCachedTickSize(tokenId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedAmount = floorToDecimals(amount, 2); + const normalizedWorstPrice = floorToDecimals(worstPrice, priceDecimals); + const orderDebug = `tickSize:${tickSize} amount:${amount}->${normalizedAmount} worstPrice:${worstPrice}->${normalizedWorstPrice}`; + if (normalizedAmount <= 0 || normalizedWorstPrice <= 0) { + console.warn(`${orderTag} params invalid after precision handling ${orderDebug}`); + return { + success: false, + statusCode: 400, + body: { error: "order params invalid after precision handling", bestBid, bestAsk, worstPrice: normalizedWorstPrice }, + errorMessage: "order params invalid after precision handling", + }; + } + + const marketOrderArgs = { tokenID: tokenId, side: side === "buy" ? Side.BUY : Side.SELL, amount: normalizedAmount, price: normalizedWorstPrice }; + const marketOrderOpts = { tickSize, negRisk: false }; + console.log("[RAW]", `${orderTag} createMarketOrder args:`, JSON.stringify(marketOrderArgs), "opts:", JSON.stringify(marketOrderOpts), "ctx:", JSON.stringify({ + source, direction, side, amount, normalizedAmount, slippageVal, bestBid, bestAsk, worstPrice, normalizedWorstPrice, tokenId, tickSize, priceDecimals, bookAgeMs, marketBestReady, + exitReason: input.exitReason, roundEntry: input.roundEntry, stopProfit: input.stopProfit, stopLoss: input.stopLoss, + windowStart: state.windowStart, marketKey: activeMarket.key, + })); + const signedOrder = await clobClient!.createMarketOrder(marketOrderArgs, marketOrderOpts); + console.log("[RAW]", `${orderTag} signedOrder:`, JSON.stringify(signedOrder)); + // -- latency monitoring (1): start timing before this machine initiates postOrder -- + const latencyT0 = Date.now(); + const result = await clobClient!.postOrder(signedOrder, OrderType.FOK); + const httpMs = Date.now() - latencyT0; // for log debugging only + console.log("[RAW]", `${orderTag} postOrder raw result:`, JSON.stringify(result), `HTTP:${httpMs}ms t0:${latencyT0}`); + const sideZh = side === "buy" ? "buy" : "sell"; + const dirZh = direction === "up" ? "up" : "down"; + const rawStatus = result?.status ?? "unknown"; + const orderError = extractOrderError(result); + + if (result?.status === 400 || orderError) { + console.warn(`${orderTag} ${activeMarket.displayName} ${sideZh} ${dirZh} ${normalizedAmount} status:${rawStatus} reason:${orderError || "-"} ${orderDebug}`); + return { + success: false, + statusCode: 400, + body: { error: orderError || `order rejected status=${rawStatus}`, result, bestBid, bestAsk, worstPrice: normalizedWorstPrice }, + errorMessage: orderError || `order rejected status=${rawStatus}`, + }; + } + + // -- latency monitoring (2): order succeeded, register orderID (if WS already arrived it settles immediately) -- + const orderIdFromResult = typeof result?.orderID === "string" ? result.orderID : ""; + if (rawStatus === "matched" && orderIdFromResult) { + registerOrderLatencyStart(orderIdFromResult, latencyT0); + } + + const dirArrow = direction === "up" ? "⬆" : "⬇"; + const okMark = rawStatus === "matched" ? "✓" : "•"; + console.log(`${orderTag} ${okMark} ${dirArrow} ${sideZh} ${dirZh} ${normalizedAmount}@${normalizedWorstPrice} filled=${fmtOrderField(result?.takingAmount)} spent=${fmtOrderField(result?.makingAmount)} orderID=${fmtOid(orderIdFromResult)} HTTP=${httpMs}ms`); + rememberPendingTradeMeta({ + orderId: typeof result?.orderID === "string" && result.orderID ? result.orderID : undefined, + ts: Date.now(), + windowStart: state.windowStart, + side, + direction, + amount: normalizedAmount, + worstPrice: normalizedWorstPrice, + source, + exitReason: input.exitReason, + roundEntry: input.roundEntry, + stopProfit: side === "buy" ? input.stopProfit : undefined, + stopLoss: side === "buy" ? input.stopLoss : undefined, + }); + if (!(typeof result?.orderID === "string" && result.orderID)) { + console.warn(`${orderTag} order response missing orderID, the MINED event will degrade to matching by direction/quantity`); + } + broadcastState(); + return { + success: true, + statusCode: 200, + body: { success: true, result, bestBid, bestAsk, worstPrice: normalizedWorstPrice }, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`${orderTag} failed:`, msg); + return { + success: false, + statusCode: 500, + body: { error: msg }, + errorMessage: msg, + }; + } +} + +async function strategyBuy(direction: StrategyDirection, amount: number): Promise { + strategyRuntime.posBeforeBuy = getDirectionLocalSize(direction); + strategyRuntime.actionTs = Date.now(); + strategyRuntime.buyLockUntil = Date.now() + STRAT_BUY_LOCK_MS; + strategyRuntime.state = "WAIT_FILL"; + broadcastState(); + + // read the strategy config's GTC take-profit target price (e.g. d1 = 0.99) + const stratKey = strategyRuntime.activeStrategy ?? ""; + const stratInstance = stratKey ? getStrategy(stratKey) : undefined; + const tpPrice = stratInstance?.getMarketTakeProfitPrice?.() ?? null; + + const orderResult = await placeOrder({ + direction, + side: "buy", + amount, + slippage: strategyConfig.slippage, + source: `strategy${stratKey}`, + roundEntry: `${strategyRuntime.roundEntryCount}/${strategyConfig.maxRoundEntries}`, + stopProfit: tpPrice != null && tpPrice > 0 && tpPrice < 1 ? { targetPrice: tpPrice } : undefined, + }); + + if (!orderResult.success) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] buy failed: ${orderResult.errorMessage || "order failed"}`); + strategyRuntime.buyLockUntil = 0; + strategyRuntime.state = "SCANNING"; + strategyRuntime.activeStrategy = null; + broadcastState(); + } +} + +async function strategySell(direction: StrategyDirection, exitReason?: string): Promise { + const totalPos = getDirectionLocalSize(direction); + const shares = getSellableShares(direction); + if (shares <= 0) { + transitionToDone(); + return; + } + + strategyRuntime.posBeforeSell = totalPos; + strategyRuntime.waitVerifyAfterSell = !isDirectionVerified(direction); + strategyRuntime.actionTs = Date.now(); + strategyRuntime.state = "WAIT_SELL_FILL"; + broadcastState(); + + const orderResult = await placeOrder({ + direction, + side: "sell", + amount: shares, + slippage: strategyConfig.slippage, + source: `strategy${strategyRuntime.activeStrategy ?? ""}`, + exitReason, + roundEntry: `${strategyRuntime.roundEntryCount}/${strategyConfig.maxRoundEntries}`, + }); + + if (!orderResult.success) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] sell failed: ${orderResult.errorMessage || "order failed"}`); + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.state = "HOLDING"; + broadcastState(); + } +} + +// -- Backtest data collection ------------------------------------- +const BACKTEST_STATE_FILE = resolve(__dirname, ".backtest-state.json"); + +function loadBacktestState(): boolean { + try { + if (!existsSync(BACKTEST_STATE_FILE)) return false; // off by default on first run, enable in the frontend as needed + const data = JSON.parse(readFileSync(BACKTEST_STATE_FILE, "utf-8")); + return typeof data.collecting === "boolean" ? data.collecting : false; + } catch { + return false; + } +} + +function persistBacktestState(): void { + try { + writeFileSync(BACKTEST_STATE_FILE, JSON.stringify({ collecting: backtestCollecting }, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn(`[Backtest] state save failed: ${err instanceof Error ? err.message : String(err)}`); + } +} + +let backtestCollecting = loadBacktestState(); +let backtestLastTickTs = 0; +let backtestLastCleanupDate = ""; +const BACKTEST_RETENTION_DAYS = 30; + +// ensure the data directory exists at startup (prevents the first write from failing) +if (backtestCollecting) { + try { mkdirSync(BACKTEST_DATA_DIR, { recursive: true }); } catch { /* ignore */ } +} + +function setBacktestCollecting(enabled: boolean): void { + backtestCollecting = enabled; + console.log(`[Backtest] data collection ${enabled ? "enabled" : "disabled"}`); + persistBacktestState(); + if (enabled) { + mkdirSync(BACKTEST_DATA_DIR, { recursive: true }); + cleanupOldBacktestFiles(); + } + broadcastBacktestStatus(); +} + +function cleanupOldBacktestFiles(): void { + try { + if (!existsSync(BACKTEST_DATA_DIR)) return; + // compatible with three naming schemes: YYYY-MM-DD.jsonl (old BTC 5m) / YYYY-MM-DD-{sym}.jsonl (old multi-symbol 5m) / YYYY-MM-DD-{sym}-{period}.jsonl (new) + const files = readdirSync(BACKTEST_DATA_DIR).filter((f) => /^\d{4}-\d{2}-\d{2}(-\w+)?(-\w+)?\.jsonl$/.test(f)); + // group by "base date", keep the latest BACKTEST_RETENTION_DAYS days per group + const byDate = new Map(); + for (const f of files) { + const date = f.slice(0, 10); + if (!byDate.has(date)) byDate.set(date, []); + byDate.get(date)!.push(f); + } + const dates = [...byDate.keys()].sort(); + if (dates.length <= BACKTEST_RETENTION_DAYS) return; + const datesToDelete = dates.slice(0, dates.length - BACKTEST_RETENTION_DAYS); + for (const d of datesToDelete) { + for (const f of byDate.get(d) || []) { + try { + unlinkSync(resolve(BACKTEST_DATA_DIR, f)); + console.log(`[Backtest] cleaned up old data file: ${f}`); + } catch {} + } + } + } catch (err) { + console.warn(`[Backtest] failed to clean up old files: ${(err as Error).message}`); + } +} + +function broadcastBacktestStatus(): void { + broadcast("backtestStatus", { collecting: backtestCollecting }); +} + +function getBacktestFilePath(): string { + // BTC 5m keeps the original filename (backward compatible with existing data), others use the -{sym}-{period} suffix + // naming rule: YYYY-MM-DD.jsonl (old BTC 5m) / YYYY-MM-DD-{sym}-{period}.jsonl (new) + const sym = activeMarket.symbol; + const period = activeMarket.period; + const suffix = (sym === "btc" && period === "5m") ? "" : `-${sym}-${period}`; + return resolve(BACKTEST_DATA_DIR, `${getCstDateStr()}${suffix}.jsonl`); +} + +function backtestAppend(record: Record): void { + try { + appendFileSync(getBacktestFilePath(), JSON.stringify(record) + "\n"); + } catch (err) { + console.warn(`[Backtest] write failed: ${(err as Error).message}`); + } +} + +function backtestTick(): void { + if (!backtestCollecting) return; + const now = Date.now(); + if (now - backtestLastTickTs < 1000) return; + + const snapshot = getProbabilitySnapshot(); + const diff = getStrategyDiff(); + const rem = getStrategyRemainingSeconds(now); + if (snapshot == null || diff == null || !state.windowStart) return; + + // clean up old files (older than the retention days) on the first write each day + const todayStr = getCstDateStr(); + if (todayStr !== backtestLastCleanupDate) { + cleanupOldBacktestFiles(); + backtestLastCleanupDate = todayStr; + } + + // diff precision based on the current price magnitude (shares the same threshold definition as the frontend priceDecimals) + // avoids small-price symbols like SOL/XRP/DOGE losing precision due to 2-decimal rounding (e.g. SOL diff -0.0712 -> -0.07) + const dec = priceDecimals(state.priceToBeat); + const factor = Math.pow(10, dec); + + backtestAppend({ + type: "tick", + ts: now, + symbol: activeMarket.symbol, + period: activeMarket.period, + windowStart: state.windowStart, + diff: Math.round(diff * factor) / factor, + upPct: snapshot.upPct, + rem, + }); + backtestLastTickTs = now; +} + + +// event-driven strategy scheduler: multiple events in a short time trigger only one tick +let strategyTickScheduled = false; +let strategyTickLastRunTs = 0; +const STRATEGY_TICK_MIN_GAP_MS = 10; // minimum interval between consecutive ticks (prevents storms) + +function scheduleStrategyTick(): void { + if (strategyTickScheduled) return; + strategyTickScheduled = true; + const now = Date.now(); + const elapsed = now - strategyTickLastRunTs; + if (elapsed >= STRATEGY_TICK_MIN_GAP_MS) { + setImmediate(() => { + strategyTickScheduled = false; + strategyTickLastRunTs = Date.now(); + runStrategyTick(); + }); + } else { + setTimeout(() => { + strategyTickScheduled = false; + strategyTickLastRunTs = Date.now(); + runStrategyTick(); + }, STRATEGY_TICK_MIN_GAP_MS - elapsed); + } +} + +function runStrategyTick(): void { + const snapshot = getProbabilitySnapshot(); + const upPct = snapshot?.upPct ?? null; + const dnPct = snapshot?.dnPct ?? null; + const diff = getStrategyDiff(); + const now = Date.now(); + const rem = getStrategyRemainingSeconds(now); + const currentPosition = getDirectionLocalSize(strategyRuntime.direction); + const ctx = buildTickContext(rem, upPct, dnPct, diff, now); + const finalize = () => { + strategyRuntime.prevUpPct = upPct; + // notify all strategies that have finalizeTick (record historical data like lastDiff, regardless of whether enabled) + for (const s of getAllStrategies()) { + if ("finalizeTick" in s && typeof (s as any).finalizeTick === "function") { + (s as any).finalizeTick(diff); + } + } + }; + + if (isOrderWindowStale(now)) { + finalize(); + return; + } + + if (!strategyRuntime.positionsReady) { + finalize(); + return; + } + + // update the guard state of enabled strategies (cooldown locks, etc.); + // strategies that are not enabled but declare alwaysComputeData also call computeData (e.g. the s6 factor panel) + // note: strategies that do not support the current market are skipped entirely, to avoid running computations on the wrong market + for (const s of getAllStrategies()) { + if (!isStrategySupportedHere(s)) continue; + if (strategyConfig.enabled[s.key]) { + s.updateGuards(ctx); + } else if (s.alwaysComputeData && typeof s.computeData === "function") { + s.computeData(ctx); + } + } + + // conditional order monitoring: TP/SL tick check (independent of the strategy state machine) + if (activeConditionOrders.size > 0 && upPct != null && dnPct != null) { + void checkConditionOrdersTick(upPct, dnPct, diff); + } + + // limit strategies: first scan pre-sign requirements (async-sign and cache), then run the limit scheduler + runPresignTick(); + runLimitStrategyTick(); + + if (strategyRuntime.cleanupAfterVerify && strategyRuntime.direction) { + if (!isDirectionVerified(strategyRuntime.direction)) { + finalize(); + return; + } + if (currentPosition < 0.01) { + strategyRuntime.cleanupAfterVerify = false; + transitionToDone(); + finalize(); + return; + } + strategyRuntime.cleanupAfterVerify = false; + strategyRuntime.state = "SELLING"; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] position calibrated, ${currentPosition.toFixed(2)} remaining, executing liquidation sell`); + broadcastState(); + void strategySell(strategyRuntime.direction, `calibrated liquidation, ${currentPosition.toFixed(2)} remaining`); + finalize(); + return; + } + + if (strategyRuntime.state === "IDLE") { + if (upPct == null || diff == null) { + finalize(); + return; + } + if (anyStrategyEnabled()) { + strategyRuntime.state = "SCANNING"; + broadcastState(); + } + finalize(); + return; + } + + if (strategyRuntime.state === "SCANNING") { + if (!anyStrategyEnabled()) { + strategyRuntime.state = "IDLE"; + broadcastState(); + finalize(); + return; + } + if (hasOpenPosition() || hasPendingStrategyBuyLock(now) || upPct == null || dnPct == null || diff == null) { + finalize(); + return; + } + if (strategyRuntime.roundEntryCount >= strategyConfig.maxRoundEntries) { + finalize(); + return; + } + // US Eastern weekend pause: do not enter new entries (positions/TP/SL/Claim are unaffected) + if (strategyConfig.weekendPause && isUsWeekend()) { + logWeekendPauseOnce(); + finalize(); + return; + } + const entry = checkEntry(ctx); + if (!entry) { + finalize(); + return; + } + const buyAmount = strategyConfig.amount[entry.strategy]; + if (!hasEnoughUsdcForBuy(buyAmount)) { + finalize(); + return; + } + strategyRuntime.roundEntryCount++; + strategyRuntime.activeStrategy = entry.strategy; + strategyRuntime.direction = entry.dir; + strategyRuntime.buyAmount = buyAmount; + strategyRuntime.state = "BUYING"; + console.log(`[Strategy.${entry.strategy}] entry triggered (${strategyRuntime.roundEntryCount}/${strategyConfig.maxRoundEntries}) ${entry.dir === "up" ? "buy up" : "buy down"} amount:${buyAmount}`); + broadcastState(); + void strategyBuy(entry.dir, buyAmount); + finalize(); + return; + } + + if (strategyRuntime.state === "WAIT_FILL") { + if (hasConfirmedBuyPosition()) { + strategyRuntime.buyLockUntil = 0; + strategyRuntime.state = "HOLDING"; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] buy fill confirmed`); + // notify the strategy of the buy fill (used for initializing the tracking peak, etc.) + if (strategyRuntime.activeStrategy && strategyRuntime.direction) { + const activeStrat = getStrategy(strategyRuntime.activeStrategy); + if (activeStrat?.onEntryFilled) activeStrat.onEntryFilled(ctx, strategyRuntime.direction); + } + broadcastState(); + } else if (now - strategyRuntime.actionTs > WAIT_FILL_TIMEOUT_MS) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] buy not confirmed after 10s, entering delayed-confirmation wait`); + strategyRuntime.state = "RECONCILING_FILL"; + broadcastState(); + finalize(); + return; + } else { + finalize(); + return; + } + } + + if (strategyRuntime.state === "RECONCILING_FILL") { + if (hasConfirmedBuyPosition()) { + strategyRuntime.buyLockUntil = 0; + strategyRuntime.state = "HOLDING"; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] delayed confirmation succeeded, resuming position management`); + if (strategyRuntime.activeStrategy && strategyRuntime.direction) { + const activeStrat = getStrategy(strategyRuntime.activeStrategy); + if (activeStrat?.onEntryFilled) activeStrat.onEntryFilled(ctx, strategyRuntime.direction); + } + broadcastState(); + } else if (canReleaseUnconfirmedBuy(now)) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] over 15s and API confirms no position, resuming scanning`); + strategyRuntime.state = "SCANNING"; + strategyRuntime.activeStrategy = null; + strategyRuntime.direction = null; + strategyRuntime.buyAmount = 0; + strategyRuntime.posBeforeBuy = 0; + strategyRuntime.actionTs = 0; + strategyRuntime.buyLockUntil = 0; + broadcastState(); + finalize(); + return; + } else { + finalize(); + return; + } + } + + if (strategyRuntime.state === "HOLDING") { + if (currentPosition <= 0) { + transitionToDone(); + finalize(); + return; + } + if (upPct == null || dnPct == null || diff == null) { + finalize(); + return; + } + const exit = checkExit(ctx); + if (exit && strategyRuntime.direction) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] ${exit.signal === "tp" ? "TP" : "SL"} triggered: ${exit.reason}`); + strategyRuntime.state = "SELLING"; + broadcastState(); + void strategySell(strategyRuntime.direction, exit.reason); + } + finalize(); + return; + } + + if (strategyRuntime.state === "WAIT_SELL_FILL") { + if (currentPosition < strategyRuntime.posBeforeSell - 0.01) { + if (currentPosition < 0.01) { + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.cleanupAfterVerify = false; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] sell confirmed, done`); + transitionToDone(); + finalize(); + return; + } + + if (strategyRuntime.waitVerifyAfterSell) { + strategyRuntime.waitVerifyAfterSell = false; + if (isDirectionVerified(strategyRuntime.direction)) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] calibrated after sell, ${currentPosition.toFixed(2)} remaining, executing liquidation immediately`); + strategyRuntime.cleanupAfterVerify = false; + strategyRuntime.state = "SELLING"; + broadcastState(); + if (strategyRuntime.direction) void strategySell(strategyRuntime.direction, `calibrated liquidation, ${currentPosition.toFixed(2)} remaining`); + finalize(); + return; + } + strategyRuntime.cleanupAfterVerify = true; + strategyRuntime.state = "DONE"; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] sell confirmed, waiting for calibration before checking remaining position`); + broadcastState(); + finalize(); + return; + } + + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.state = "HOLDING"; + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] sell confirmed, ${currentPosition.toFixed(2)} remaining, continuing to process`); + broadcastState(); + finalize(); + return; + } + + if (now - strategyRuntime.actionTs > WAIT_FILL_TIMEOUT_MS) { + console.log(`[Strategy.${strategyRuntime.activeStrategy ?? ""}] sell timeout, back to holding`); + strategyRuntime.waitVerifyAfterSell = false; + strategyRuntime.state = "HOLDING"; + broadcastState(); + } + finalize(); + return; + } + + finalize(); +} + +function buildApiStatePayload(includeHistory: boolean = true): Record { + return { + ...buildStatePayload(includeHistory), + wsStatus, + claimable: { + total: claimableTotal, + positions: claimablePositions, + }, + claimCooldown: { + running: claimCycleRunning || claimInProgress, + nextCheckAt: claimNextCheckAt, + cooldownUntil: claimCooldownUntil, + }, + }; +} + +// derived metrics for the monitor page (today's PnL/win rate/trade count), same algorithm as the TG push +// also returns todayClosedCount + todayWins so the monitor page can correctly weight-aggregate (averaging win rates directly would distort) +function computeTodayPnlMetrics(): { + todayPnl: number | null; + todayCount: number | null; + todayWinRate: number | null; + todayClosedCount: number | null; + todayWins: number | null; +} { + if (!pmPnlManager.isInitialized()) { + return { todayPnl: null, todayCount: null, todayWinRate: null, todayClosedCount: null, todayWins: null }; + } + const todaySec = Math.floor(getCstDayStartMs() / 1000); + const snap = pmPnlManager.computeSnapshot(todaySec); + return { + todayPnl: snap.netPnl, + todayCount: snap.positions, + todayWinRate: snap.closedPositions > 0 ? snap.wins / snap.closedPositions : null, + todayClosedCount: snap.closedPositions, + todayWins: snap.wins, + }; +} + +app.get("/api/state", (req, res) => { + // ?lite=1 skips priceHistory/binanceHistory/coinbaseHistory (for the monitor page, saves ~98% traffic) + const lite = req.query.lite === "1" || req.query.lite === "true"; + const payload = buildApiStatePayload(!lite) as Record; + // inject the derived metrics the monitor page needs (same source data as the TG push) + payload.pmPnl = computeTodayPnlMetrics(); + res.json(payload); +}); + +app.get("/api/version", (_req, res) => { + res.json({ version: APP_VERSION }); +}); + +app.get("/api/market/list", (_req, res) => { + res.json({ + active: activeMarket.key, + activeSymbol: activeMarket.symbol, + activePeriod: activeMarket.period, + symbols: ALL_SYMBOLS, + periods: ALL_PERIODS, + markets: Object.values(MARKETS).map(m => ({ + key: m.key, symbol: m.symbol, period: m.period, displayName: m.displayName, + })), + }); +}); + +app.post("/api/market/switch", async (req, res) => { + // supports two input forms: { key: "btc-15m" } or { symbol: "btc", period: "15m" } + const body = (req.body as Record | undefined) || {}; + let key = String(body.key || ""); + if (!key && body.symbol && body.period) key = `${body.symbol}-${body.period}`; + if (!isValidKey(key)) { + return res.status(400).json({ ok: false, reason: `unknown market: ${key}` }); + } + const result = await switchMarket(key); + if (!result.ok) return res.status(409).json(result); + res.json(result); +}); + +app.get("/api/strategy/descriptions", (_req, res) => { + res.json(getAllDescriptions()); +}); + +app.get("/api/backtest/status", (_req, res) => { + res.json({ collecting: backtestCollecting }); +}); + +app.post("/api/backtest/toggle", (_req, res) => { + setBacktestCollecting(!backtestCollecting); + res.json({ collecting: backtestCollecting }); +}); + +// manually trigger a full PmPnl refresh (frontend refresh button) +app.post("/api/pmpnl/refresh", async (_req, res) => { + const ok = await pmPnlManager.fetchAll(); + pmPnlNextRefreshAt = Date.now() + PMPNL_REFRESH_INTERVAL_MS; + broadcastPmPnl(); + res.json({ ok, nextRefreshAt: pmPnlNextRefreshAt, lastRefreshAt: pmPnlManager.getLastRefreshAt() }); +}); + +app.post("/api/strategy/config", (req, res) => { + const prevAutoClaim = strategyConfig.autoClaimEnabled; + const { config, error } = applyStrategyConfigUpdate(strategyConfig, req.body); + if (!config) { + res.status(400).json({ error: error || "config error" }); + return; + } + + strategyConfig = config; + savePersistedStrategyConfig(config); + // immediately inject the tunable params into all strategy instances (avoids the next fill reading stale values) + for (const s of getAllStrategies()) applyTunableParamsToStrategy(s); + const configSummary = ALL_STRATEGY_KEYS.map((k) => `${k}:${config.enabled[k] ? "on" : "off"}(${config.amount[k]})`).join(" "); + console.log(`[Strategy.Config] updated ${configSummary} maxRound:${config.maxRoundEntries} active in the current process`); + broadcastState(); + + // autoClaimEnabled from off -> on: immediately trigger a query and claim check + if (!prevAutoClaim && config.autoClaimEnabled) { + scheduleClaimCycle(0); + } + + res.json({ success: true, strategyConfig }); +}); + +// read/write the manual order config (amount / slippage / TP-SL) +app.get("/api/manual/config", (_req, res) => { + res.json({ manualConfig }); +}); +app.post("/api/manual/config", (req, res) => { + const err = applyManualConfigUpdate(req.body); + if (err) { res.status(400).json({ error: err }); return; } + broadcast("manualConfig", { manualConfig }); + res.json({ success: true, manualConfig }); +}); + +// -- REST: order endpoint ---------------------------------------- +// manually cancel a conditional order +app.post("/api/cond/cancel", async (req, res) => { + const { id } = req.body as { id: string }; + const cond = activeConditionOrders.get(id); + if (!cond) { + res.status(404).json({ error: "conditional order does not exist or has ended" }); + return; + } + if (cond.kind === "tp" && cond.polymarketOrderId) { + const ok = await cancelTakeProfitOrder(cond.polymarketOrderId); + if (!ok) { + // cancel failed: keep it in the list, mark failed, the user can retry manually or wait for pollGtcOrderStatus fallback + cond.status = "failed"; + cond.failReason = "cancel resting order failed, please retry"; + broadcastConditionOrders(); + res.status(500).json({ error: "Polymarket cancel resting order failed", polymarketCancelFailed: true }); + return; + } + } + cond.status = "canceled"; + activeConditionOrders.delete(cond.id); + broadcastConditionOrders(); + res.json({ success: true }); +}); + +// one-click clear all active conditional orders (includes TP GTC and local SL) +app.post("/api/cond/cancel-all", async (_req, res) => { + const all = [...activeConditionOrders.values()]; + if (all.length === 0) { + res.json({ success: true, count: 0, failed: 0 }); + return; + } + let canceled = 0; + let failed = 0; + for (const cond of all) { + if (cond.kind === "tp" && cond.polymarketOrderId) { + const ok = await cancelTakeProfitOrder(cond.polymarketOrderId); + if (!ok) { + // keep the failed ones for the user / polling fallback to handle + cond.status = "failed"; + cond.failReason = "cancel resting order failed during full clear"; + failed++; + continue; + } + } + cond.status = "canceled"; + activeConditionOrders.delete(cond.id); + canceled++; + } + broadcastConditionOrders(); + console.log(`[Cond] manual full clear: ${canceled} succeeded, ${failed} failed (failed ones kept for retry)`); + res.json({ success: failed === 0, count: canceled, failed }); +}); + +// in-memory limit order list (shared by manual + strategy) +interface ManualLimitOrder { + orderID: string; + direction: StrategyDirection; + side: "buy" | "sell"; + size: number; + price: number; + createdAt: number; + /** source: manual=manual, strategy:t8=strategy resting order */ + source?: string; + /** the window it belongs to (used for window management of strategy resting orders) */ + windowStart?: number; + /** filled shares (partial-fill tracking) */ + filledSize?: number; +} +const manualLimitOrders = new Map(); + +// -- Strategy limit order scheduling (limit/both strategies like t8) -------------- +// +// design: +// - each strategy has at most 1 active resting order (per window) +// - place only once per window (after cancel, do not re-place, wait for the next window) +// - on cancel failure, enter the fallback retry queue (every 5s, up to 12 times or 90s or window end) + +interface StrategyLimitOrder { + orderID: string; + strategyKey: string; + /** "buy" = entry buy order (default); "sell" = take-profit sell order placed after the fill */ + side: "buy" | "sell"; + direction: StrategyDirection; + /** the token id at resting time, ensures placing TP after MINED uses the same window's token (state has changed after window switch) */ + tokenId: string; + price: number; + shares: number; + /** cumulative filled shares confirmed at the match layer (MATCHED push, not on-chain) */ + matchedSize: number; + /** cumulative filled shares confirmed on-chain (MINED push + on-chain calibration) */ + filledSize: number; + /** total TP sell shares already placed for on-chain-confirmed shares (avoids placing TP twice for the same shares) */ + takeProfitPlacedSize: number; + windowStart: number; + createdAt: number; + /** on cancel, found already matched/filled, keep the order awaiting MINED handling (avoids stratOrder being deleted prematurely causing TP/SL loss) */ + pendingMined?: boolean; +} + +// the currently active strategy limit order (at most 1 per strategy) +const strategyLimitOrders = new Map(); +// the set of strategies that have "already attempted to place" within this window (implements "place only once per window") +const strategyLimitWindowMark = new Set(); // value: `${strategyKey}:${windowStart}` +// orderIDs with cancel in progress (prevents the same tick / poll from concurrently invoking the same cancel API) +const cancelInFlight = new Set(); + +// -- GTC order status WS+REST dual confirmation mechanism -------------------- +// purpose: resolve the blind spot of "called the order/cancel HTTP but the actual status did not change" +// flow: after postOrder/cancelOrder register pending -> wait for WS event_type:order push or getOrder fallback after 8 seconds +// market FOK does not use this path (the HTTP response already contains the full result, it will not stay on the book) +const ORDER_CONFIRM_TIMEOUT_MS = 3000; +// the middle of the order ID is elided for easier terminal reading (raw log still keeps the full ID) +function fmtOid(id: string | undefined | null): string { + if (!id || typeof id !== "string") return "-"; + if (id.length <= 16) return id; + return `${id.slice(0, 10)}…${id.slice(-4)}`; +} + +// order context (for logging only, prints price / direction / side / size on cancel/confirm) +type OrderCtx = { price?: number; direction?: string; side?: string; size?: number }; +function fmtOrderCtx(ctx: OrderCtx | undefined): string { + if (!ctx) return ""; + const parts: string[] = []; + if (ctx.side) parts.push(`side=${ctx.side}`); + if (ctx.direction) parts.push(`dir=${ctx.direction}`); + if (typeof ctx.size === "number") parts.push(`size=${ctx.size}`); + if (typeof ctx.price === "number") parts.push(`price=${ctx.price}`); + return parts.length ? ` [${parts.join(" ")}]` : ""; +} +function getOrderCtxFromMaps(orderID: string): OrderCtx | undefined { + const so = strategyLimitOrders.get(orderID); + if (so) return { price: so.price, direction: so.direction, side: so.side, size: so.shares }; + const mo = manualLimitOrders.get(orderID); + if (mo) return { price: mo.price, direction: mo.direction, side: mo.side, size: mo.size }; + return undefined; +} +type PendingOrderConfirm = { + orderID: string; + source: string; // caller identifier, for logging only + ctx?: OrderCtx; + t0: number; + resolved: boolean; // set to true after WS arrives, the timeout branch will skip the query + timer: ReturnType; +}; +type PendingCancelConfirm = { + orderID: string; + reason: string; + ctx?: OrderCtx; + t0: number; + resolved: boolean; + timer: ReturnType; + rounds: number; // how many rounds of 8s fallback have elapsed (avoids an infinite loop when both WS+REST are down) +}; +const CANCEL_MAX_ROUNDS = 3; // at most 3 rounds = ~24 seconds cumulative + 3 cancelOrderImmediate (each with 3 internal HTTP retries) +// when verifyCancelViaRest re-cancels, pass through the current round to cancelOrderImmediate -> registerCancelConfirm +const cancelRoundOverride = new Map(); +const pendingOrderConfirm = new Map(); +const pendingCancelConfirm = new Map(); +// scenario where WS arrives before the HTTP response: cache the WS signal first, on register check for a hit and do not start the timer +type EarlyWsCache = { ts: number; reason: string }; +const earlyOrderConfirmCache = new Map(); +const earlyCancelConfirmCache = new Map(); +const EARLY_WS_CACHE_TTL_MS = 30_000; +function rememberEarlyWs(map: Map, orderID: string, reason: string): void { + if (!orderID) return; + map.set(orderID, { ts: Date.now(), reason }); + setTimeout(() => map.delete(orderID), EARLY_WS_CACHE_TTL_MS).unref?.(); +} + +function registerOrderConfirm(orderID: string, source: string, ctx?: OrderCtx): void { + if (!orderID) return; + // an existing one (same orderID registered twice, should not happen) -> clear the old one + const old = pendingOrderConfirm.get(orderID); + if (old) clearTimeout(old.timer); + // WS arrived before the HTTP response? accept immediately + const early = earlyOrderConfirmCache.get(orderID); + if (early) { + earlyOrderConfirmCache.delete(orderID); + console.log(`[Trade.Confirm] WS arrived first orderID=${fmtOid(orderID)}${fmtOrderCtx(ctx)} reason:${early.reason} (source=${source})`); + return; + } + const entry: PendingOrderConfirm = { + orderID, source, ctx, t0: Date.now(), resolved: false, + timer: setTimeout(() => verifyOrderViaRest(orderID), ORDER_CONFIRM_TIMEOUT_MS), + }; + pendingOrderConfirm.set(orderID, entry); +} +function resolveOrderConfirm(orderID: string, reason: string): void { + const entry = pendingOrderConfirm.get(orderID); + if (!entry) return; + entry.resolved = true; + clearTimeout(entry.timer); + pendingOrderConfirm.delete(orderID); + console.log(`[Trade.Confirm] WS confirmed orderID=${fmtOid(orderID)}${fmtOrderCtx(entry.ctx)} elapsed:${Date.now() - entry.t0}ms reason:${reason} (source=${entry.source})`); +} +async function verifyOrderViaRest(orderID: string): Promise { + const entry = pendingOrderConfirm.get(orderID); + if (!entry || entry.resolved) return; + pendingOrderConfirm.delete(orderID); + console.warn(`[Trade.Confirm] ${ORDER_CONFIRM_TIMEOUT_MS}ms no WS PLACEMENT/MATCHED received, getOrder fallback orderID=${fmtOid(orderID)} source=${entry.source}`); + if (!clobClient) { + console.warn(`[Trade.Confirm] CLOB not initialized, cannot fall back`); + return; + } + try { + const order = await clobClient.getOrder(orderID); + console.log("[RAW]", `[Trade.Confirm] getOrder fallback orderID=${orderID} raw:`, JSON.stringify(order)); + const status = typeof order?.status === "string" ? order.status.toUpperCase() : ""; + if (status === "LIVE" || status === "MATCHED" || status === "FILLED") { + console.log(`[Trade.Confirm] ✓ getOrder confirmed exists orderID=${fmtOid(orderID)} status=${status} (just slow WS)`); + } else if (status === "CANCELED") { + console.warn(`[Trade.Confirm] ⚠ orderID=${fmtOid(orderID)} was cancelled (abnormal state, clearing local)`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + clearPendingTradeMetaByOrderId(orderID); + broadcastManualLimitOrders(); + } else { + console.warn(`[Trade.Confirm] ⚠ orderID=${fmtOid(orderID)} unknown status status=${status || "(empty)"}`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (/not found|404|does not exist/i.test(msg)) { + console.warn(`[Trade.Confirm] ❌ getOrder 404 -> order not placed orderID=${fmtOid(orderID)} source=${entry.source} (clearing local, not auto re-placing)`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + clearPendingTradeMetaByOrderId(orderID); + broadcastManualLimitOrders(); + } else { + console.warn(`[Trade.Confirm] getOrder failed orderID=${fmtOid(orderID)}:`, msg); + } + } +} + +function registerCancelConfirm(orderID: string, reason: string, ctx?: OrderCtx, rounds: number = 0): void { + if (!orderID) return; + const old = pendingCancelConfirm.get(orderID); + if (old) clearTimeout(old.timer); + // ctx prefers the input arg, otherwise reverse-look it up from the in-memory Map (the Map is usually still there on cancel) + const finalCtx = ctx ?? getOrderCtxFromMaps(orderID); + // WS CANCELED arrived before the HTTP response? accept immediately + const early = earlyCancelConfirmCache.get(orderID); + if (early) { + earlyCancelConfirmCache.delete(orderID); + console.log(`[Trade.Cancel] WS arrived first orderID=${fmtOid(orderID)}${fmtOrderCtx(finalCtx)} reason:${early.reason} (call reason=${reason})`); + return; + } + const entry: PendingCancelConfirm = { + orderID, reason, ctx: finalCtx, t0: Date.now(), resolved: false, rounds, + timer: setTimeout(() => verifyCancelViaRest(orderID), ORDER_CONFIRM_TIMEOUT_MS), + }; + pendingCancelConfirm.set(orderID, entry); +} +function resolveCancelConfirm(orderID: string, reason: string): void { + const entry = pendingCancelConfirm.get(orderID); + if (!entry) return; + entry.resolved = true; + clearTimeout(entry.timer); + pendingCancelConfirm.delete(orderID); + console.log(`[Trade.Cancel] WS confirmed cancel orderID=${fmtOid(orderID)}${fmtOrderCtx(entry.ctx)} elapsed:${Date.now() - entry.t0}ms reason:${reason}`); +} +async function verifyCancelViaRest(orderID: string): Promise { + const entry = pendingCancelConfirm.get(orderID); + if (!entry || entry.resolved) return; + pendingCancelConfirm.delete(orderID); + const round = entry.rounds + 1; + console.warn(`[Trade.Cancel] ${ORDER_CONFIRM_TIMEOUT_MS}ms no WS CANCELED received, getOrder fallback orderID=${fmtOid(orderID)} round=${round}/${CANCEL_MAX_ROUNDS}`); + if (!clobClient) return; + try { + const order = await clobClient.getOrder(orderID); + console.log("[RAW]", `[Trade.Cancel] getOrder fallback orderID=${orderID} raw:`, JSON.stringify(order)); + const status = typeof order?.status === "string" ? order.status.toUpperCase() : ""; + if (status === "CANCELED" || status === "CANCELLED") { + console.log(`[Trade.Cancel] ✓ orderID=${fmtOid(orderID)} already ${status} (clearing local)`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + clearPendingTradeMetaByOrderId(orderID); + broadcastManualLimitOrders(); + } else if (status === "FILLED") { + // fully filled: keep the order awaiting MINED handling (avoids stratOrder being deleted prematurely causing TP/SL loss) + const so = strategyLimitOrders.get(orderID); + if (so) so.pendingMined = true; + console.log(`[Trade.Cancel] ⚠ orderID=${fmtOid(orderID)} already FILLED, keeping order awaiting MINED handling`); + } else if (status === "LIVE" || status === "MATCHED") { + if (round >= CANCEL_MAX_ROUNDS) { + console.error(`[Trade.Cancel] ❌ orderID=${fmtOid(orderID)} still ${status}, reached max re-cancel rounds ${CANCEL_MAX_ROUNDS}, giving up and force-clearing local (PM actual status may still be LIVE, needs manual check or wait for window-switch cleanup)`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + broadcastManualLimitOrders(); + } else { + console.warn(`[Trade.Cancel] ⚠ orderID=${fmtOid(orderID)} still ${status}, triggering re-cancel round=${round}`); + // pass through round to the next register (cancelOrderImmediate will call registerCancelConfirm) + // use a temp variable so cancelOrderImmediate knows the current round + cancelRoundOverride.set(orderID, round); + void cancelOrderImmediate(orderID); + } + } else { + console.warn(`[Trade.Cancel] ⚠ orderID=${fmtOid(orderID)} unknown status status=${status || "(empty)"}`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (/not found|404|does not exist/i.test(msg)) { + console.log(`[Trade.Cancel] ✓ orderID=${fmtOid(orderID)} REST 404 (no longer exists, treated as cancelled)`); + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + clearPendingTradeMetaByOrderId(orderID); + broadcastManualLimitOrders(); + } else { + console.warn(`[Trade.Cancel] getOrder failed orderID=${fmtOid(orderID)}:`, msg); + } + } +} + +type CancelResult = "canceled" | "soft_matched" | "soft_other" | false; +/** immediately attempt to cancel (retry 3 times), returns a tri-state + * parses the cancelOrder response structure: { canceled: [orderID...], not_canceled: { orderID: reason } } + * - canceled contains the target -> "canceled" (really cancelled) + * - not_canceled reason contains filled/matched -> "soft_matched" (actually filled, caller must keep the order awaiting MINED) + * - not_canceled reason contains not.?found/already, etc. -> "soft_other" (order does not exist, can be cleaned up) + * - otherwise -> returns false when retries are exhausted + */ +async function cancelOrderImmediate(orderID: string, reason: string = "immediate"): Promise { + if (!clobClient) return false; + // register WS fallback confirmation; if this is a re-cancel from verifyCancelViaRest, pass through the current round + const overrideRound = cancelRoundOverride.get(orderID); + if (overrideRound !== undefined) cancelRoundOverride.delete(orderID); + registerCancelConfirm(orderID, reason, undefined, overrideRound ?? 0); + for (let attempt = 1; attempt <= 3; attempt++) { + const t0 = Date.now(); + console.log("[RAW]", `[Trade.Cancel] cancelOrder args:`, JSON.stringify({ orderID }), `attempt:${attempt}/3 reason:${reason}`); + try { + const result = await clobClient.cancelOrder({ orderID }); + const dt = Date.now() - t0; + console.log("[RAW]", `[Trade.Cancel] cancelOrder raw result:`, JSON.stringify(result), `HTTP:${dt}ms attempt:${attempt}`); + const canceledList = Array.isArray((result as any)?.canceled) ? (result as any).canceled as string[] : []; + const notCanceled = (result as any)?.not_canceled; + const wasCanceled = canceledList.includes(orderID); + const ncReason = notCanceled && typeof notCanceled === "object" ? String((notCanceled as Record)[orderID] ?? "") : ""; + if (wasCanceled) { + // HTTP confirmed success, but the WS fallback is still kept (so verifyCancelViaRest does not double-handle when the WS actually arrives) + return "canceled"; + } + if (ncReason) { + // matched/filled takes priority: also classified as soft_matched when the reason also contains already + if (/filled|matched/i.test(ncReason)) { + console.log(`[Trade.Cancel] orderID=${fmtOid(orderID)} not_canceled already filled (keeping order awaiting MINED): ${ncReason}`); + resolveCancelConfirm(orderID, "HTTP not_canceled soft success (matched)"); + return "soft_matched"; + } + if (/not.?found|does not exist|already/i.test(ncReason)) { + console.log(`[Trade.Cancel] orderID=${fmtOid(orderID)} not_canceled order does not exist: ${ncReason}`); + resolveCancelConfirm(orderID, "HTTP not_canceled soft success (not_found)"); + return "soft_other"; + } + console.warn(`[Trade.Cancel] orderID=${fmtOid(orderID)} not_canceled reason needs retry: ${ncReason}`); + } else { + console.warn(`[Trade.Cancel] orderID=${fmtOid(orderID)} response has neither a canceled hit nor a not_canceled reason, retrying`); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const dt = Date.now() - t0; + console.log("[RAW]", `[Trade.Cancel] cancelOrder threw attempt:${attempt}/3 HTTP:${dt}ms err:`, msg); + // the throw path cannot distinguish matched/canceled, conservatively classify as soft_other (does not affect the fix goal -- matched goes through the HTTP response path, not the throw path) + if (/not found|does not exist|already/i.test(msg)) { + resolveCancelConfirm(orderID, "HTTP threw not found"); + return "soft_other"; + } + } + if (attempt < 3) await new Promise(r => setTimeout(r, 500)); + } + return false; +} + +/** + * cancel a strategy limit order (unified entry point) + * - first cancel: event-driven call (instant) + * - after failure: 1-second poll auto-retry (pollLimitOrderCancellations) + * - if the strategy changes its mind during retry (checkCancelOrder=false) -> auto-stop + * - after cancel succeeds: delete stratOrder + clear mark (if the strategy allows re-placing) + */ +async function cancelStrategyLimitOrder(orderID: string, reason: string): Promise { + // prevent concurrent cancels of the same orderID (event-driven + polling may collide) + if (cancelInFlight.has(orderID)) return; + cancelInFlight.add(orderID); + const stratOrder = strategyLimitOrders.get(orderID); + const tag = stratOrder?.strategyKey ?? "?"; + let result: CancelResult = false; + try { + result = await cancelOrderImmediate(orderID); + } finally { + cancelInFlight.delete(orderID); + } + if (result === "canceled" || result === "soft_other") { + // really cancelled / order no longer exists: clear local + strategyLimitOrders.delete(orderID); + manualLimitOrders.delete(orderID); + broadcastManualLimitOrders(); + if (stratOrder && stratOrder.side === "buy") { + const stratInstance = getStrategy(stratOrder.strategyKey); + if (stratInstance?.limitAllowReplaceAfterCancel) { + strategyLimitWindowMark.delete(`${stratOrder.strategyKey}:${stratOrder.windowStart}`); + console.log(`[Strategy.${tag}] re-placing allowed after cancel, window mark cleared`); + } + } + console.log(`[Strategy.${tag}] ✖ cancel succeeded orderID=${fmtOid(orderID)} reason: ${reason}`); + } else if (result === "soft_matched") { + // hit a fill during cancel: keep the order awaiting MINED to trigger TP/SL placement (fixes the bug where stratOrder was deleted prematurely leaving the position exposed) + if (stratOrder) stratOrder.pendingMined = true; + console.log(`[Strategy.${tag}] ⚠ already filled during cancel orderID=${fmtOid(orderID)} keeping order awaiting MINED handling reason: ${reason}`); + } else { + // do not enqueue, pollLimitOrderCancellations 1-second poll auto-retries + console.warn(`[Strategy.${tag}] ⚠ cancel failed, will auto-retry via polling orderID=${fmtOid(orderID)} reason: ${reason}`); + } +} + +/** + * limit order cancel polling (every 1 second) + * + * logic: scan all active stratOrders, the strategy says cancel + the order is still there -> call cancel + * - order not in strategyLimitOrders = already successfully cancelled, auto-skip + * - strategy checkCancelOrder=false (condition bounced back) = let the order keep resting, do not cancel + * - cancel succeeds -> delete + the next loop no longer iterates it + * - cancel fails -> retry after 1 second (no limit, naturally terminated by the strategy's intent) + */ +async function pollLimitOrderCancellations(): Promise { + if (strategyLimitOrders.size === 0) return; + const snapshot = getProbabilitySnapshot(); + const upPct = snapshot?.upPct ?? null; + const dnPct = snapshot?.dnPct ?? null; + const diff = getStrategyDiff(); + const now = Date.now(); + const rem = getStrategyRemainingSeconds(now); + const ctx = buildTickContext(rem, upPct, dnPct, diff, now); + + const currentWs = getCurrentWindowStart(); + for (const stratOrder of [...strategyLimitOrders.values()]) { + if (cancelInFlight.has(stratOrder.orderID)) continue; + // order already filled awaiting MINED handling: no longer cancel, no longer trigger any action (wait for MINED to place TP/SL + clean up) + if (stratOrder.pendingMined) continue; + // cross-window residual -> force cancel (applies to both buy/sell, the position has settled and is meaningless) + if (stratOrder.windowStart !== currentWs) { + void cancelStrategyLimitOrder(stratOrder.orderID, "window expired"); + continue; + } + if (stratOrder.side !== "buy") continue; // sell (TP) is managed by the cond system within the current window, does not participate in strategy checkCancelOrder + // strategy disabled -> force cancel + if (!strategyConfig.enabled[stratOrder.strategyKey]) { + void cancelStrategyLimitOrder(stratOrder.orderID, "strategy disabled"); + continue; + } + const strat = getStrategy(stratOrder.strategyKey); + if (!strat?.checkCancelOrder) continue; + const runtime = { + direction: stratOrder.direction, + price: stratOrder.price, + shares: stratOrder.shares, + filledSize: stratOrder.filledSize, + windowStart: stratOrder.windowStart, + }; + if (strat.checkCancelOrder(ctx, runtime)) { + void cancelStrategyLimitOrder(stratOrder.orderID, "polling cancel"); + } + } +} + +// -- Pre-sign infrastructure (generic) -------------------------------------- +// strategies declare requirements via getPresignRequest, the server async-creates and caches the signed package when rem in [remMin, remMax] +// async-createOrder caches the signature package in the background; on trigger placeStrategyLimitOrder prefers the cache +// skipping signing latency. Conservatively no retry on failure, falls back to live-signing when the cache is empty. + +interface PresignedPack { + strategyKey: string; + direction: StrategyDirection; + windowStart: number; + tokenId: string; + price: number; + shares: number; + signed: unknown; // the signature package returned by clobClient.createOrder, type determined by the SDK +} + +/** key = `${strategyKey}:${windowStart}:${direction}` */ +const strategyPresigned = new Map(); +/** the set of keys currently being signed, to avoid duplicate triggers within the same tick */ +const presignInflight = new Set(); + +function presignKey(strategyKey: string, ws: number, direction: StrategyDirection): string { + return `${strategyKey}:${ws}:${direction}`; +} + +/** clear all pre-sign caches on window switch (the old window's tokenId is no longer valid) */ +function clearAllPresigned(): void { + if (strategyPresigned.size === 0 && presignInflight.size === 0) return; + console.log(`[Strategy.Presign] cleared ${strategyPresigned.size} cached pre-sign packages (window switch)`); + strategyPresigned.clear(); + presignInflight.clear(); +} + +async function presignOne( + strategyKey: string, + direction: StrategyDirection, + price: number, + shares: number, + ws: number, +): Promise { + const key = presignKey(strategyKey, ws, direction); + if (presignInflight.has(key)) return; + if (strategyPresigned.has(key)) return; + if (!(await ensureClobClient())) return; + const tokenId = direction === "up" ? state.upTokenId : state.downTokenId; + if (!tokenId) return; + // the window may have already switched + if (getCurrentWindowStart() !== ws) return; + + presignInflight.add(key); + const startedAt = Date.now(); + try { + const tickSize = getCachedTickSize(tokenId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedSize = floorToDecimals(shares, 2); + const normalizedPrice = floorToDecimals(price, priceDecimals); + if (normalizedSize < 5 || normalizedPrice <= 0) { + console.warn(`[Strategy.Presign.${strategyKey}] params invalid size=${normalizedSize} price=${normalizedPrice}`); + return; + } + const signed = await clobClient!.createOrder( + { tokenID: tokenId, side: Side.BUY, price: normalizedPrice, size: normalizedSize }, + { tickSize, negRisk: false }, + ); + // check the window again after signing (it may have switched during the async period) + if (getCurrentWindowStart() !== ws) { + console.log(`[Strategy.Presign.${strategyKey}] ${direction} signed but window switched, discarding`); + return; + } + strategyPresigned.set(key, { + strategyKey, + direction, + windowStart: ws, + tokenId, + price: normalizedPrice, + shares: normalizedSize, + signed, + }); + console.log(`[Strategy.Presign.${strategyKey}] ${direction} signed size=${normalizedSize} price=${normalizedPrice} elapsed ${Date.now() - startedAt}ms`); + } catch (err) { + // conservative: no retry on failure, falls back when the cache is empty on trigger + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[Strategy.Presign.${strategyKey}] ${direction} signing failed (conservatively no retry): ${msg}`); + } finally { + presignInflight.delete(key); + } +} + +/** called every tick: scan all strategies' pre-sign requirements, async-sign when the timing is right (does not block the tick) */ +function runPresignTick(): void { + const ws = getCurrentWindowStart(); + if (!ws) return; + const rem = getStrategyRemainingSeconds(Date.now()); + + for (const s of getAllStrategies()) { + if (!isStrategySupportedHere(s)) continue; + if (!strategyConfig.enabled[s.key]) continue; + if (!s.getPresignRequest) continue; + const req = s.getPresignRequest(); + if (!req) continue; + if (rem < req.remMin || rem > req.remMax) continue; + + for (const direction of req.directions) { + const key = presignKey(s.key, ws, direction); + if (strategyPresigned.has(key) || presignInflight.has(key)) continue; + // async-sign, do not await + void presignOne(s.key, direction, req.price, req.shares, ws); + } + } +} + +/** strategy limit order (place a GTC order by the strategy signal) */ +async function placeStrategyLimitOrder( + strategyKey: string, + signal: { direction: StrategyDirection; price: number; shares: number }, +): Promise { + if (!(await ensureClobClient())) return; + const tokenId = signal.direction === "up" ? state.upTokenId : state.downTokenId; + if (!tokenId) { + console.warn(`[Strategy.${strategyKey}] current window not ready, skipping resting order`); + return; + } + try { + const tickSize = getCachedTickSize(tokenId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedSize = floorToDecimals(signal.shares, 2); + const normalizedPrice = floorToDecimals(signal.price, priceDecimals); + if (normalizedSize < 5 || normalizedPrice <= 0) { + console.warn(`[Strategy.${strategyKey}] params invalid size=${normalizedSize} price=${normalizedPrice}`); + return; + } + + // prefer the pre-sign cache: on hit skip createOrder (saves ~200ms signing latency) + // only use it when direction + tokenId + price + shares all match, otherwise live-sign + const ws = getCurrentWindowStart(); + const pkey = presignKey(strategyKey, ws, signal.direction); + const cached = strategyPresigned.get(pkey); + let signed; + let usedPresign = false; + const createOrderArgs = { tokenID: tokenId, side: Side.BUY, price: normalizedPrice, size: normalizedSize }; + const createOrderOpts = { tickSize, negRisk: false }; + console.log("[RAW]", `[Strategy.${strategyKey}] placeStrategyLimitOrder ctx:`, JSON.stringify({ + strategyKey, signal, normalizedSize, normalizedPrice, tickSize, priceDecimals, + tokenId, windowStart: ws, presignCacheKey: pkey, presignHit: !!cached, + }), "createOrder args:", JSON.stringify(createOrderArgs), "opts:", JSON.stringify(createOrderOpts)); + if ( + cached && + cached.tokenId === tokenId && + cached.price === normalizedPrice && + cached.shares === normalizedSize + ) { + signed = cached.signed as Awaited["createOrder"]>>; + usedPresign = true; + strategyPresigned.delete(pkey); + // the other direction's pre-sign package will not be used again within the same window after trigger, clear it immediately to save memory + const otherDir: StrategyDirection = signal.direction === "up" ? "down" : "up"; + strategyPresigned.delete(presignKey(strategyKey, ws, otherDir)); + } else { + signed = await clobClient!.createOrder(createOrderArgs, createOrderOpts); + } + console.log("[RAW]", `[Strategy.${strategyKey}] signedOrder:`, JSON.stringify(signed)); + const postT0 = Date.now(); + const result = await clobClient!.postOrder(signed, OrderType.GTC); + const postHttpMs = Date.now() - postT0; + console.log("[RAW]", `[Strategy.${strategyKey}] postOrder GTC raw result:`, JSON.stringify(result), `HTTP:${postHttpMs}ms t0:${postT0}`); + const orderID = typeof result?.orderID === "string" ? result.orderID : ""; + if (!orderID) { + const errMsg = typeof result?.error === "string" ? result.error : "response has no orderID"; + console.warn(`[Strategy.${strategyKey}] resting order failed: ${errMsg}`); + return; + } + const order: StrategyLimitOrder = { + orderID, + strategyKey, + side: "buy", + direction: signal.direction, + tokenId, + price: normalizedPrice, + shares: normalizedSize, + matchedSize: 0, + filledSize: 0, + takeProfitPlacedSize: 0, + windowStart: ws, + createdAt: Date.now(), + }; + strategyLimitOrders.set(orderID, order); + // also stuff into manualLimitOrders so the frontend can see it + manualLimitOrders.set(orderID, { + orderID, + direction: signal.direction, + side: "buy", + size: normalizedSize, + price: normalizedPrice, + createdAt: order.createdAt, + source: `strategy:${strategyKey}`, + windowStart: ws, + filledSize: 0, + }); + // write pendingTradeMeta: after the MINED event hits, source is used to call notifyTradeForPnl, + // so the real PnL panel attributes this fill to the corresponding strategy (otherwise it falls to "manual") + rememberPendingTradeMeta({ + orderId: orderID, + ts: order.createdAt, + windowStart: ws, + side: "buy", + direction: signal.direction, + amount: normalizedSize, + worstPrice: normalizedPrice, + source: `strategy${strategyKey}`, + }); + broadcastManualLimitOrders(); + strategyLimitWindowMark.add(`${strategyKey}:${ws}`); + console.log(`[Strategy.${strategyKey}] ➕ ${signal.direction === "up" ? "⬆" : "⬇"} resting order buy ${signal.direction === "up" ? "up" : "down"} ${normalizedSize}@${normalizedPrice} orderID=${fmtOid(orderID)}${usedPresign ? " ⚡presign" : ""}`); + registerOrderConfirm(orderID, `strategy:${strategyKey}`, { + side: "buy", direction: signal.direction, size: normalizedSize, price: normalizedPrice, + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[Strategy.${strategyKey}] resting order exception: ${msg}`); + } +} + +/** + * strategy take-profit resting order: called after the buy maker order is confirmed filled on-chain, places a same-direction GTC sell order + * + * price fixed at 0.10 (10x profit), shares = the newly confirmed fill shares this time (partial fills are accumulated and placed) + * will not place twice: order.takeProfitPlacedSize accumulates the already-placed amount, ensuring the same shares are placed only once + */ +async function placeStrategyTakeProfitOrder( + buyOrder: StrategyLimitOrder, + sellSize: number, + sellPrice: number, +): Promise { + if (!(await ensureClobClient())) return; + if (sellSize < 5) { + console.warn(`[Strategy.${buyOrder.strategyKey}] TP shares ${sellSize.toFixed(2)} < 5, skipping maker path`); + return; + } + // use the buy order's tokenId at the time (avoids state.upTokenId already being the new window's after a window switch) + const tokenId = buyOrder.tokenId; + if (!tokenId) { + console.warn(`[Strategy.${buyOrder.strategyKey}] TP: buy order missing tokenId, skipping`); + return; + } + // window-switch protection: do not place if the buy order's window has ended (position settled or zeroed) + const currentWs = getCurrentWindowStart(); + if (buyOrder.windowStart !== currentWs) { + console.warn(`[Strategy.${buyOrder.strategyKey}] TP: buy order belongs to the previous window (${buyOrder.windowStart} != ${currentWs}), position settled, skipping TP`); + return; + } + try { + const tickSize = getCachedTickSize(tokenId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedSize = floorToDecimals(sellSize, 2); + const normalizedPrice = floorToDecimals(sellPrice, priceDecimals); + if (normalizedSize < 5 || normalizedPrice <= 0) { + console.warn(`[Strategy.${buyOrder.strategyKey}] TP params invalid size=${normalizedSize} price=${normalizedPrice}`); + return; + } + const stratTpArgs = { tokenID: tokenId, side: Side.SELL, price: normalizedPrice, size: normalizedSize }; + const stratTpOpts = { tickSize, negRisk: false }; + console.log("[RAW]", `[Strategy.${buyOrder.strategyKey}] TP createOrder args:`, JSON.stringify(stratTpArgs), "opts:", JSON.stringify(stratTpOpts), "ctx:", JSON.stringify({ + buyOrderID: buyOrder.orderID, buyDirection: buyOrder.direction, buyWindow: buyOrder.windowStart, + sellSize, sellPrice, normalizedSize, normalizedPrice, tickSize, priceDecimals, currentWs, + })); + const signed = await clobClient!.createOrder(stratTpArgs, stratTpOpts); + console.log("[RAW]", `[Strategy.${buyOrder.strategyKey}] TP signedOrder:`, JSON.stringify(signed)); + const stratTpT0 = Date.now(); + const result = await clobClient!.postOrder(signed, OrderType.GTC); + const stratTpHttpMs = Date.now() - stratTpT0; + console.log("[RAW]", `[Strategy.${buyOrder.strategyKey}] TP postOrder GTC raw result:`, JSON.stringify(result), `HTTP:${stratTpHttpMs}ms`); + const orderID = typeof result?.orderID === "string" ? result.orderID : ""; + if (!orderID) { + const errMsg = typeof result?.error === "string" ? result.error : "response has no orderID"; + console.warn(`[Strategy.${buyOrder.strategyKey}] TP resting order failed: ${errMsg}`); + return; + } + const ws = getCurrentWindowStart(); + const sellOrder: StrategyLimitOrder = { + orderID, + strategyKey: buyOrder.strategyKey, + side: "sell", + direction: buyOrder.direction, + tokenId, + price: normalizedPrice, + shares: normalizedSize, + matchedSize: 0, + filledSize: 0, + takeProfitPlacedSize: 0, + windowStart: ws, + createdAt: Date.now(), + }; + strategyLimitOrders.set(orderID, sellOrder); + manualLimitOrders.set(orderID, { + orderID, + direction: buyOrder.direction, + side: "sell", + size: normalizedSize, + price: normalizedPrice, + createdAt: sellOrder.createdAt, + source: `strategy:${buyOrder.strategyKey}:tp`, + windowStart: ws, + filledSize: 0, + }); + registerOrderConfirm(orderID, `strategy:${buyOrder.strategyKey}:tp`, { + side: "sell", direction: buyOrder.direction, size: normalizedSize, price: normalizedPrice, + }); + // write pendingTradeMeta: after the MINED sell event hits, source can be found + rememberPendingTradeMeta({ + orderId: orderID, + ts: sellOrder.createdAt, + windowStart: ws, + side: "sell", + direction: buyOrder.direction, + amount: normalizedSize, + worstPrice: normalizedPrice, + source: `strategy${buyOrder.strategyKey}tp`, + }); + broadcastManualLimitOrders(); + buyOrder.takeProfitPlacedSize += normalizedSize; + console.log(`[Strategy.${buyOrder.strategyKey}] TP resting order sell ${buyOrder.direction} size=${normalizedSize} price=${normalizedPrice} orderID=${fmtOid(orderID)}`); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[Strategy.${buyOrder.strategyKey}] TP resting order exception: ${msg}`); + } +} + +/** + * limit-strategy TP sell order on-chain confirmation (MINED side=sell): + * accumulate the sell stratOrder's filledSize, clean up when it reaches shares + */ +function onStrategyTakeProfitMined( + strategyKey: string, + direction: StrategyDirection, + minedSize: number, +): void { + if (minedSize <= 0) return; + const candidates = [...strategyLimitOrders.values()].filter(o => + o.strategyKey === strategyKey && o.side === "sell" && o.direction === direction + && o.filledSize < o.shares - 0.01 + ); + if (candidates.length === 0) { + console.log(`[Strategy.${strategyKey}] TP MINED ${minedSize.toFixed(2)} shares but no matching sell order found`); + return; + } + candidates.sort((a, b) => a.createdAt - b.createdAt); + + let remaining = minedSize; + for (const order of candidates) { + if (remaining <= 0) break; + const need = order.shares - order.filledSize; + const take = Math.min(remaining, need); + order.filledSize += take; + remaining -= take; + const ml = manualLimitOrders.get(order.orderID); + if (ml) ml.filledSize = order.filledSize; + if (order.filledSize >= order.shares - 0.01) { + console.log(`[Strategy.${strategyKey}] TP sell on-chain confirmed ${order.shares} shares @${order.price} (order:${order.orderID.slice(0, 10)}...)`); + strategyLimitOrders.delete(order.orderID); + manualLimitOrders.delete(order.orderID); + clearPendingTradeMetaByOrderId(order.orderID); + } else { + console.log(`[Strategy.${strategyKey}] TP sell partial on-chain confirmation +${take.toFixed(2)} shares confirmed ${order.filledSize.toFixed(2)}/${order.shares}`); + } + broadcastManualLimitOrders(); + } +} + +/** + * called after a limit-strategy maker buy order is confirmed filled on-chain: + * - find the corresponding buy stratOrder + * - accumulate filledSize (on-chain confirmed shares) + * - place TP sell orders for the newly confirmed shares (place on every MINED increment, plan B) + * - clean up the buy stratOrder when filledSize reaches shares + */ +async function onStrategyLimitMined( + strategyKey: string, + direction: StrategyDirection, + minedSize: number, + orderID?: string, +): Promise { + if (minedSize <= 0) return; + // fix C: prefer exact matching by orderID (avoids cross-talk between multiple stratOrders) + // when orderID is not passed, fall back to fuzzy lookup by strategyKey + direction (backward compatible) + let candidates: StrategyLimitOrder[]; + if (orderID) { + const order = strategyLimitOrders.get(orderID); + if (!order || order.side !== "buy" || order.direction !== direction || order.filledSize >= order.shares - 0.01) { + console.log(`[Strategy.${strategyKey}] MINED on-chain confirmed ${minedSize.toFixed(2)} shares orderID=${fmtOid(orderID)} but the order is no longer present or already full (may have been cleaned up or duplicated)`); + return; + } + candidates = [order]; + } else { + candidates = [...strategyLimitOrders.values()].filter(o => + o.strategyKey === strategyKey && o.side === "buy" && o.direction === direction + && o.filledSize < o.shares - 0.01 + ); + if (candidates.length === 0) { + console.log(`[Strategy.${strategyKey}] MINED on-chain confirmed ${minedSize.toFixed(2)} shares but no matching buy order found (may have been cleaned up)`); + return; + } + // ascending by createdAt: consume the old ones first (those placed earlier in the same window confirm first) + candidates.sort((a, b) => a.createdAt - b.createdAt); + } + + let remaining = minedSize; + for (const order of candidates) { + if (remaining <= 0) break; + const need = order.shares - order.filledSize; + const take = Math.min(remaining, need); + order.filledSize += take; + remaining -= take; + const ml = manualLimitOrders.get(order.orderID); + if (ml) ml.filledSize = order.filledSize; + if (order.filledSize >= order.shares - 0.01) { + console.log(`[Strategy.${strategyKey}] limit order on-chain confirmed ${order.direction} ${order.shares} shares @${order.price} (order:${order.orderID.slice(0, 10)}...)`); + strategyLimitOrders.delete(order.orderID); + manualLimitOrders.delete(order.orderID); + clearPendingTradeMetaByOrderId(order.orderID); + } else { + console.log(`[Strategy.${strategyKey}] limit order partial on-chain confirmation +${take.toFixed(2)} shares confirmed ${order.filledSize.toFixed(2)}/${order.shares}`); + } + broadcastManualLimitOrders(); + // place TP/SL for the newly confirmed shares + const tpPending = order.filledSize - order.takeProfitPlacedSize; + if (tpPending > 0) { + const stratInstance = getStrategy(strategyKey); + // prefer the cond system (TP+SL co-group management) + const condCfg = stratInstance?.getLimitConditionOrder?.() ?? null; + if (condCfg && (condCfg.stopProfit || condCfg.stopLoss)) { + await createConditionOrdersAfterFill({ + direction: order.direction, + assetId: order.tokenId, + entryPrice: order.price, + filledSize: tpPending, + windowStart: order.windowStart, + stopProfit: condCfg.stopProfit, + stopLoss: condCfg.stopLoss, + }); + order.takeProfitPlacedSize = order.filledSize; + console.log(`[Strategy.${strategyKey}] created conditional orders (TP/SL) for ${tpPending.toFixed(2)} shares`); + } else { + // fall back to the old TP-only path + const tpPrice = stratInstance?.getLimitTakeProfitPrice?.() ?? null; + if (tpPrice == null) { + // no TP, skip (filled shares are held to settlement) + order.takeProfitPlacedSize = order.filledSize; + } else if (tpPending >= 5) { + await placeStrategyTakeProfitOrder(order, tpPending, tpPrice); + } else { + console.log(`[Strategy.${strategyKey}] TP pending ${tpPending.toFixed(2)} shares < 5 (maker minimum limit), not placing for now`); + } + } + } + } +} + +/** limit-strategy scheduling (called every tick) */ +function runLimitStrategyTick(): void { + const snapshot = getProbabilitySnapshot(); + const upPct = snapshot?.upPct ?? null; + const dnPct = snapshot?.dnPct ?? null; + const diff = getStrategyDiff(); + const now = Date.now(); + const rem = getStrategyRemainingSeconds(now); + const ctx = buildTickContext(rem, upPct, dnPct, diff, now); + const currentWs = getCurrentWindowStart(); + + for (const s of getAllStrategies()) { + if (!isStrategySupportedHere(s)) continue; + if (!strategyConfig.enabled[s.key]) continue; + if (!s.checkLimitOrder) continue; // not a limit strategy + + // find the current strategy's active buy resting order (sell is the TP order, auto-managed by the server, strategy does not participate) + // orders with pendingMined have hit a fill, awaiting MINED handling, do not participate in this path (avoids repeatedly triggering cancel) + const existingOrder = [...strategyLimitOrders.values()].find(o => o.strategyKey === s.key && o.side === "buy" && !o.pendingMined); + + if (existingOrder) { + // check whether to cancel + if (s.checkCancelOrder) { + const runtime = { + direction: existingOrder.direction, + price: existingOrder.price, + shares: existingOrder.shares, + filledSize: existingOrder.filledSize, + windowStart: existingOrder.windowStart, + }; + if (s.checkCancelOrder(ctx, runtime)) { + void cancelStrategyLimitOrder(existingOrder.orderID, "strategy cancel condition triggered"); + } + } + } else { + // no resting order, check whether to place a new one (note: place only once per window) + const mark = `${s.key}:${currentWs}`; + if (strategyLimitWindowMark.has(mark)) continue; + + // inject the latest shares config into the strategy instance (so checkLimitOrder uses the latest value) + if ("shares" in s) { + const cfgShares = strategyConfig.shares[s.key]; + if (cfgShares != null && cfgShares >= 5) { + (s as any).shares = cfgShares; + } + } + // inject the latest tunable params into the strategy instance (e.g. l1's tpDelta/slDiff) + applyTunableParamsToStrategy(s); + + // US Eastern weekend pause: do not place new limit orders (existing ones are not cancelled, filled ones' TP/SL run as usual) + if (strategyConfig.weekendPause && isUsWeekend()) { + logWeekendPauseOnce(); + continue; + } + + const signal = s.checkLimitOrder(ctx); + if (signal) { + // mark immediately, to avoid the next tick placing again + strategyLimitWindowMark.add(mark); + void placeStrategyLimitOrder(s.key, signal); + } + } + } +} + + +function broadcastManualLimitOrders(): void { + broadcast("manualLimitOrders", { list: [...manualLimitOrders.values()].sort((a, b) => b.createdAt - a.createdAt) }); +} + +app.get("/api/orders/open", (_req, res) => { + res.json({ list: [...manualLimitOrders.values()].sort((a, b) => b.createdAt - a.createdAt) }); +}); + +app.post("/api/orders/cancel", async (req, res) => { + const { orderID } = req.body as { orderID: string }; + if (!orderID) { res.status(400).json({ error: "invalid params" }); return; } + if (!(await ensureClobClient())) { res.status(500).json({ error: "CLOB not initialized" }); return; } + registerCancelConfirm(orderID, "manual"); + const t0 = Date.now(); + console.log("[RAW]", `[Trade.Limit] cancelOrder args:`, JSON.stringify({ orderID })); + try { + const result = await clobClient!.cancelOrder({ orderID }); + const dt = Date.now() - t0; + console.log("[RAW]", `[Trade.Limit] cancelOrder raw result:`, JSON.stringify(result), `HTTP:${dt}ms`); + const canceledList = Array.isArray((result as any)?.canceled) ? (result as any).canceled as string[] : []; + const notCanceled = (result as any)?.not_canceled; + const ncReason = notCanceled && typeof notCanceled === "object" ? String((notCanceled as Record)[orderID] ?? "") : ""; + if (canceledList.includes(orderID)) { + manualLimitOrders.delete(orderID); + broadcastManualLimitOrders(); + console.log(`[Trade.Limit] ✖ cancel orderID=${fmtOid(orderID)}`); + res.json({ success: true }); + return; + } + if (ncReason) { + if (/filled|matched|not.?found|does not exist|already/i.test(ncReason)) { + manualLimitOrders.delete(orderID); + broadcastManualLimitOrders(); + resolveCancelConfirm(orderID, "HTTP not_canceled soft success"); + console.log(`[Trade.Limit] orderID=${fmtOid(orderID)} handled: ${ncReason}`); + res.json({ success: true, note: ncReason }); + return; + } + console.warn(`[Trade.Limit] cancel not_canceled: ${ncReason}`); + res.status(500).json({ error: `cancel rejected: ${ncReason}` }); + return; + } + console.warn(`[Trade.Limit] cancel response ambiguous orderID=${fmtOid(orderID)}`); + res.status(500).json({ error: "cancel response not confirmed" }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + const dt = Date.now() - t0; + console.log("[RAW]", `[Trade.Limit] cancelOrder threw HTTP:${dt}ms err:`, msg); + if (/not found|does not exist|already/i.test(msg)) { + manualLimitOrders.delete(orderID); + broadcastManualLimitOrders(); + resolveCancelConfirm(orderID, "HTTP threw not found"); + res.json({ success: true, note: "order no longer exists" }); + return; + } + console.warn(`[Trade.Limit] cancel failed orderID=${fmtOid(orderID)} err:`, msg); + res.status(500).json({ error: msg }); + } +}); + +app.post("/api/order/limit", async (req, res) => { + const { direction, side, size, price } = req.body as { + direction: StrategyDirection; side: "buy" | "sell"; size: number; price: number; + }; + if (!direction || !side || !size || !price || size < 5 || price <= 0 || price >= 1) { + res.status(400).json({ error: "invalid params" }); + return; + } + if (!(await ensureClobClient())) { + res.status(500).json({ error: "CLOB client not initialized" }); + return; + } + const tokenId = direction === "up" ? state.upTokenId : state.downTokenId; + if (!tokenId) { res.status(400).json({ error: "the current window market is not ready" }); return; } + try { + const tickSize = getCachedTickSize(tokenId); + const priceDecimals = getDecimalPlaces(tickSize); + const normalizedSize = floorToDecimals(size, 2); + const normalizedPrice = floorToDecimals(price, priceDecimals); + if (normalizedSize <= 0 || normalizedPrice <= 0) { + res.status(400).json({ error: "params invalid after precision handling" }); return; + } + const manualLimitArgs = { tokenID: tokenId, side: side === "buy" ? Side.BUY : Side.SELL, price: normalizedPrice, size: normalizedSize }; + const manualLimitOpts = { tickSize, negRisk: false }; + console.log("[RAW]", `[Trade.Limit] createOrder args:`, JSON.stringify(manualLimitArgs), "opts:", JSON.stringify(manualLimitOpts), "ctx:", JSON.stringify({ direction, side, size, price, normalizedSize, normalizedPrice, tickSize, priceDecimals, tokenId, windowStart: state.windowStart })); + const signed = await clobClient!.createOrder(manualLimitArgs, manualLimitOpts); + console.log("[RAW]", `[Trade.Limit] signedOrder:`, JSON.stringify(signed)); + const limitT0 = Date.now(); + const result = await clobClient!.postOrder(signed, OrderType.GTC); + const limitHttpMs = Date.now() - limitT0; + console.log("[RAW]", `[Trade.Limit] postOrder GTC raw result:`, JSON.stringify(result), `HTTP:${limitHttpMs}ms`); + const orderID = typeof result?.orderID === "string" ? result.orderID : ""; + if (!orderID) { + const errMsg = typeof result?.error === "string" ? result.error : "response has no orderID"; + res.status(400).json({ error: errMsg }); return; + } + console.log(`[Trade.Limit] ➕ ${direction === "up" ? "⬆" : "⬇"} ${side === "buy" ? "buy" : "sell"} ${direction === "up" ? "up" : "down"} ${normalizedSize}@${normalizedPrice} orderID=${fmtOid(orderID)}`); + registerOrderConfirm(orderID, "manual-limit", { side, direction, size: normalizedSize, price: normalizedPrice }); + manualLimitOrders.set(orderID, { orderID, direction, side, size: normalizedSize, price: normalizedPrice, createdAt: Date.now() }); + broadcastManualLimitOrders(); + res.json({ orderID }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + res.status(500).json({ error: msg }); + } +}); + +app.post("/api/order", async (req, res) => { + const { direction, side, amount, slippage, stopProfit, stopLoss } = req.body as { + direction: "up" | "down"; + side: "buy" | "sell"; + amount: number; + slippage?: number; + // conditional order params (only effective when side=buy; percentage is the delta relative to the fill price, 0~1 float) + stopProfit?: { pctDelta: number }; + stopLoss?: { pctDelta: number; slippage?: number }; + }; + if (side === "buy" && (stopProfit || stopLoss)) { + const tp = stopProfit ? `TP+${(stopProfit.pctDelta * 100).toFixed(1)}%` : ""; + const sl = stopLoss ? `SL-${(stopLoss.pctDelta * 100).toFixed(1)}%(slippage ${((stopLoss.slippage ?? 0.15) * 100).toFixed(0)}%)` : ""; + console.log(`[Cond] received conditional order params ${[tp, sl].filter(Boolean).join(" · ")} (direction=${direction} amount=${amount})`); + } + const result = await placeOrder({ + direction, side, amount, slippage, + source: "manual", + stopProfit: side === "buy" ? stopProfit : undefined, + stopLoss: side === "buy" ? stopLoss : undefined, + }); + res.status(result.statusCode).json(result.body); +}); + +// -- REST: Telegram push config ---------------------------------- +app.get("/api/tg/config", (_req, res) => { + const masked = tgConfig.botToken ? tgConfig.botToken.slice(0, 10) + "***" + tgConfig.botToken.slice(-4) : ""; + res.json({ + enabled: tgConfig.enabled, + botToken: masked, + botTokenSet: !!tgConfig.botToken, + chatId: tgConfig.chatId, + intervalMinutes: tgConfig.intervalMinutes, + scheduledEnabled: tgConfig.scheduledEnabled, + postTradeEnabled: tgConfig.postTradeEnabled, + }); +}); + +app.post("/api/tg/config", (req, res) => { + const body = req.body as Partial; + if (typeof body.enabled === "boolean") tgConfig.enabled = body.enabled; + if (typeof body.botToken === "string" && body.botToken && !body.botToken.includes("***")) { + tgConfig.botToken = body.botToken.trim(); + } + if (typeof body.chatId === "string") tgConfig.chatId = body.chatId.trim(); + if (typeof body.intervalMinutes === "number" && body.intervalMinutes >= 5) { + tgConfig.intervalMinutes = Math.floor(body.intervalMinutes); + } + if (typeof body.scheduledEnabled === "boolean") tgConfig.scheduledEnabled = body.scheduledEnabled; + if (typeof body.postTradeEnabled === "boolean") tgConfig.postTradeEnabled = body.postTradeEnabled; + saveTgConfig(tgConfig); + startTgPushLoop(); // hot-restart the scheduled task + broadcastTgConfig(); + res.json({ ok: true }); +}); + +app.post("/api/tg/test", async (_req, res) => { + if (!tgConfig.botToken || !tgConfig.chatId) { + res.status(400).json({ ok: false, error: "please configure the Bot Token and Chat ID first" }); + return; + } + const text = `🧪 ${activeMarket.displayName} test message\n\n${buildTgMessage()}`; + const result = await sendTgMessage(tgConfig, text); + res.json(result); +}); + +// auto-detect Chat ID (take the chat.id of the most recent message from getUpdates) +app.post("/api/tg/detect-chat-id", async (req, res) => { + const body = req.body as { botToken?: string }; + // prefer the token in the request (just entered by the frontend user), otherwise use the saved one + let token = body.botToken && !body.botToken.includes("***") ? body.botToken.trim() : tgConfig.botToken; + if (!token) { + res.status(400).json({ ok: false, error: "please enter the Bot Token first" }); + return; + } + const result = await autoDetectChatId(token); + res.json(result); +}); + +// -- Browser WS connection ---------------------------------------- +if (wss) { + wss.on("connection", (ws, req) => { + const dataMode = resolveClientDataModeFromUrl(req.url); + clientSessions.set(ws, createClientSession(dataMode)); + console.log(`[System.Frontend] browser connected, current: ${wss!.clients.size} mode=${dataMode}`); + send(ws, "clientConfig", { dataMode }); + sendStateToClient(ws, { includeHistory: true }); + sendPmPnlToClient(ws); + sendHttpHeartbeatToClient(ws); + sendOrderLatencyToClient(ws); + sendConditionOrdersToClient(ws); + send(ws, "manualLimitOrders", { list: [...manualLimitOrders.values()].sort((a, b) => b.createdAt - a.createdAt) }); + send(ws, "manualConfig", { manualConfig }); + sendTgConfigToClient(ws); + send(ws, "wsStatus", wsStatus as unknown as Record); + send(ws, "claimable", { total: claimableTotal, positions: claimablePositions }); + send(ws, "claimCooldown", { running: claimCycleRunning || claimInProgress, nextCheckAt: claimNextCheckAt, cooldownUntil: claimCooldownUntil }); + send(ws, "backtestStatus", { collecting: backtestCollecting }); + ws.on("message", (raw) => { + try { + const msg = JSON.parse(raw.toString()); + // frontend -> backend RTT test: reply pong immediately (echo clientTs back, frontend computes RTT) + if (msg && msg.type === "ping") { + send(ws, "pong", { clientTs: msg.clientTs }); + return; + } + applyClientConfig(ws, msg); + } catch { + // ignore non-JSON or non-config messages + } + }); + ws.on("close", () => { + const session = clientSessions.get(ws); + if (session) { + clearStateTimer(session); + clientSessions.delete(ws); + } + console.log(`[System.Frontend] browser disconnected, current: ${wss!.clients.size}`); + }); + }); +} + +// -- Before startup: load all strategy plugins first (the core of the plugin architecture) -------------- +await initStrategies(); +initStrategyConfig(); +// limit strategies: read the shares config from env and inject into the strategy instance +for (const s of getAllStrategies()) { + const upper = s.key.toUpperCase(); + const sharesEnv = process.env[`STRATEGY_${upper}_SHARES`]; + if (sharesEnv != null && "shares" in s) { + const v = Number(sharesEnv); + if (Number.isFinite(v) && v >= 5) { + (s as any).shares = v; + console.log(`[Strategy] ${s.key} shares config set to ${v}`); + } + } + // tunable params: env STRATEGY__PARAM_ override -> write into strategyConfig.params + const defs = getStrategyTunableParams(s.key); + if (defs.length) { + if (strategyConfig.params[s.key] == null) strategyConfig.params[s.key] = {}; + for (const def of defs) { + const envName = `STRATEGY_${upper}_PARAM_${def.key.toUpperCase()}`; + const envRaw = process.env[envName]; + if (envRaw != null) { + const v = Number(envRaw); + if (Number.isFinite(v) && v >= def.min && (def.max == null || v <= def.max)) { + strategyConfig.params[s.key][def.key] = v; + console.log(`[Strategy] ${s.key}.${def.key} = ${v} (from ${envName})`); + } + } + } + } + // inject the persisted/env params into the strategy instance + applyTunableParamsToStrategy(s); +} + +// -- Startup ---------------------------------------------------- +// WebSocketServer is attached to the http server; when the server reports EADDRINUSE, wss also fires an error event. +// must register a silent handler on wss to prevent Node from treating it as an unhandled 'error' and crashing directly. +if (wss) wss.on("error", (err) => { + const e = err as NodeJS.ErrnoException; + if (e.code === "EADDRINUSE") return; // handled by listenWithFallback retry + console.warn(`[System.Frontend] error:`, e.message); +}); + +async function listenWithFallback(): Promise { + for (let i = 0; i < PORT_MAX_TRIES; i++) { + const tryPort = PORT_BASE + i; + try { + await new Promise((resolve, reject) => { + const onError = (err: NodeJS.ErrnoException) => { + server.off("listening", onListening); + reject(err); + }; + const onListening = () => { + server.off("error", onError); + resolve(); + }; + server.once("error", onError); + server.once("listening", onListening); + server.listen(tryPort); + }); + PORT = tryPort; + if (i > 0) console.log(`[System.Port] ${PORT_BASE} is occupied, auto-switched to ${PORT}`); + // write .port so start.sh knows the actual port (readable immediately after startup; a stale file from a dead PID is harmless) + try { writeFileSync(resolve(__dirname, ".port"), String(PORT)); } catch { /* ignore */ } + return; + } catch (err) { + const e = err as NodeJS.ErrnoException; + if (e.code !== "EADDRINUSE") throw err; + console.log(`[System.Port] ${tryPort} is occupied, trying the next one...`); + } + } + throw new Error(`[System.Port] ports ${PORT_BASE}-${PORT_BASE + PORT_MAX_TRIES - 1} are all occupied, startup failed`); +} + +await listenWithFallback(); + +(async () => { + console.log(`\n ${activeMarket.displayName} 5m order book monitor service started v${APP_VERSION}`); + console.log(` by Penguin Sensei · 岳 | X: @x_188888_x`); + console.log(` run mode: ${APP_MODE}`); + console.log(` state API: http://localhost:${PORT}/api/state`); + if (IS_FULL_MODE) { + console.log(` open browser: http://localhost:${PORT}`); + console.log(` WS address: ws://localhost:${PORT}`); + } + console.log(""); + + await ensureClobClient(); + void ensureAccountName(); + startUserWs(); + startBinanceWs(); + startCoinbaseWs(); + await syncPositionsFromApi(); + await syncUsdcBalance(); + // start the 15s polling loop (whether it actually queries/claims is decided by the autoClaimEnabled config) + scheduleClaimCycle(); + + setInterval(async () => { await syncPositionsFromApi(); broadcastState(); }, 2000); + setInterval(async () => { await syncUsdcBalance(); broadcastState(); }, 5000); + setInterval(() => { refreshBinanceOffset("scheduled", { allowLatestFallback: false }); }, BINANCE_ALIGN_REFRESH_MS); + setInterval(() => { refreshGenericOffset(COINBASE_SPEC, "scheduled", { allowLatestFallback: false }); }, BINANCE_ALIGN_REFRESH_MS); + // market + limit strategies share a tick (event-driven + 250ms fallback, so limit-strategy cancels also respond in a second) + setInterval(() => { runStrategyTick(); backtestTick(); }, STRATEGY_TICK_MS); + // limit cancel polling (every 1 second: strategy says cancel + order still there -> call cancel API) + setInterval(() => { void pollLimitOrderCancellations(); }, 1000); + // the old GTC take-profit fallback polling has been removed: now relies on UserWS event_type:order real-time push + 8s register fallback, + // on window switch all GTC are cancelled uniformly, residual UI state lags at most until the next window + // the Claim feature has moved to the Polymarket official site (Settings -> Auto Redeem), no longer auto-executed locally + + // HTTP Keep-Alive heartbeat: ping CLOB /time every 20 seconds, to prevent the server from closing the idle connection + // measured: the server closes in 30-60 seconds, a 20-second interval has a 10+ second safety margin + // each channel pings once: undici (fetch) + axios (Node https globalAgent) + setInterval(async () => { + // undici channel + { + const t0 = Date.now(); + try { + const r = await fetch(`${CLOB_URL}/time`); + await r.text(); + httpHeartbeat.latencyMs = Date.now() - t0; + httpHeartbeat.lastAt = Date.now(); + httpHeartbeat.ok = true; + // print only on anomaly (>1000ms considered suspicious) + if (httpHeartbeat.latencyMs > 1000) { + console.warn(`[Health.undici] ⚠ ${httpHeartbeat.latencyMs}ms (>1000ms)`); + } + } catch (err) { + httpHeartbeat.latencyMs = -1; + httpHeartbeat.lastAt = Date.now(); + httpHeartbeat.ok = false; + console.error(`[Health.undici] ❌ heartbeat failed: ${err instanceof Error ? err.message : String(err)}`); + } + broadcastHttpHeartbeat(); + } + // axios channel: use Node native https (defaults to globalAgent, shares the connection pool with axios) + { + const t0 = Date.now(); + try { + await new Promise((res, rej) => { + const req = httpsMod.request(`${CLOB_URL}/time`, { method: "GET" }, (r) => { + r.on("data", () => { /* drain */ }); + r.on("end", () => res()); + r.on("error", rej); + }); + req.on("error", rej); + req.setTimeout(5000, () => { req.destroy(new Error("timeout")); }); + req.end(); + }); + axiosHeartbeat.latencyMs = Date.now() - t0; + axiosHeartbeat.lastAt = Date.now(); + axiosHeartbeat.ok = true; + if (axiosHeartbeat.latencyMs > 1000) { + console.warn(`[Health.axios] ⚠ ${axiosHeartbeat.latencyMs}ms (>1000ms)`); + } + } catch (err) { + axiosHeartbeat.latencyMs = -1; + axiosHeartbeat.lastAt = Date.now(); + axiosHeartbeat.ok = false; + console.error(`[Health.axios] ❌ heartbeat failed: ${err instanceof Error ? err.message : String(err)}`); + } + broadcastAxiosHeartbeat(); + } + }, 20000); + + // Polymarket real PnL: full load at startup + full refresh every 5 minutes + const schedulePmPnlRefresh = () => { + pmPnlNextRefreshAt = Date.now() + PMPNL_REFRESH_INTERVAL_MS; + }; + pmPnlManager.init().then(() => { + schedulePmPnlRefresh(); + broadcastPmPnl(); + }).catch((err) => { + console.warn(`[PnL] startup load failed: ${err instanceof Error ? err.message : String(err)}`); + schedulePmPnlRefresh(); + }); + setInterval(async () => { + await pmPnlManager.fetchAll(); + schedulePmPnlRefresh(); + broadcastPmPnl(); + }, PMPNL_REFRESH_INTERVAL_MS); + + // Telegram push: read the config, start the scheduled task per the config + startTgPushLoop(); + + const currentWindow = getCurrentWindowStart(); + fetchRecentResults(currentWindow, true); + await subscribeWindow(currentWindow); +})(); + +function gracefulShutdown(signal: string): void { + console.log(`[System.Exit] received ${signal}, preparing to shut down (force exit after 3 seconds)`); + // fallback: SIGKILL if not exited within 3 seconds (unref so it does not block a normal exit) + setTimeout(() => { + console.warn("[System.Exit] not finished in 3 seconds, forcing SIGKILL"); + process.kill(process.pid, "SIGKILL"); + }, 3000).unref(); + + stopped = true; + if (switchTimer) clearTimeout(switchTimer); + if (reconnectTimer) clearTimeout(reconnectTimer); + if (claimCycleTimer) clearTimeout(claimCycleTimer); + if (marketWs) marketWs.close(); + if (chainlinkWs) chainlinkWs.close(); + if (userWs) (userWs as WebSocket).close(); + if (binanceWs) binanceWs.close(); + server.close(); + process.exit(0); +} + +process.on("SIGINT", () => gracefulShutdown("SIGINT")); +process.on("SIGTERM", () => gracefulShutdown("SIGTERM")); +process.on("SIGHUP", () => gracefulShutdown("SIGHUP")); diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..6efd603 --- /dev/null +++ b/start.bat @@ -0,0 +1,38 @@ +@echo off +chcp 65001 >nul +cd /d "%~dp0" + +if not exist ".env" ( + echo [Error] .env not found. Please copy .env.example and fill in your config: + echo copy .env.example .env + pause + exit /b 1 +) + +if not exist "node_modules\" ( + echo Installing dependencies... + npm install + if errorlevel 1 ( + echo [Error] npm install failed. + pause + exit /b 1 + ) +) + +set "APP_MODE=full" +for /f "tokens=1,* delims==" %%A in (.env) do ( + if /I "%%A"=="APP_MODE" set "APP_MODE=%%B" +) + +echo Starting BTC 5m monitor... +echo Mode: %APP_MODE% +echo Port 3456 in use will auto-fallback to 3457-3465 (see startup log) +echo Press Ctrl+C to stop. +echo. + +REM Probe actual listening port and open browser +if /I not "%APP_MODE%"=="headless" ( + start "" /b cmd /c "for /l %%i in (1,1,10) do (timeout /t 1 /nobreak >nul & for %%P in (3456 3457 3458 3459 3460 3461 3462 3463 3464 3465) do (curl -sf -m 1 http://localhost:%%P/api/state >nul 2>&1 && (start """" http://localhost:%%P & exit /b 0)))" +) + +npx --yes tsx server.ts diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..790b2e9 --- /dev/null +++ b/start.sh @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +cd "$(dirname "$0")" + +# Check whether .env exists +if [ ! -f .env ]; then + echo "No .env file found. Please copy .env.example first and fill in the config:" + echo " cp .env.example .env" + echo " Then edit .env and fill in POLYMARKET_PRIVATE_KEY and POLYMARKET_PROXY_ADDRESS" + exit 1 +fi + +# Auto-install Node.js (Linux only; macOS usually already has it) +if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then + if [ "$(uname -s)" = "Linux" ]; then + echo "Node.js not detected, auto-installing Node.js 20..." + if ! command -v curl >/dev/null 2>&1; then + sudo apt-get update && sudo apt-get install -y curl + fi + if command -v apt-get >/dev/null 2>&1; then + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + elif command -v yum >/dev/null 2>&1; then + curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash - + sudo yum install -y nodejs + else + echo "Unable to identify the package manager. Please install Node.js 20+ manually and retry" + exit 1 + fi + echo "Node.js installation complete: $(node -v)" + else + echo "Node.js or npm not found. Please install Node.js 20+ first (visit https://nodejs.org)" + exit 1 + fi +fi + +# Install dependencies (on first run, or when package.json/lock is newer than node_modules) +NEED_INSTALL=0 +if [ ! -d node_modules ]; then + NEED_INSTALL=1 +elif [ package.json -nt node_modules ] || [ package-lock.json -nt node_modules ]; then + echo "Dependency updates detected..." + NEED_INSTALL=1 +fi +if [ $NEED_INSTALL -eq 1 ]; then + echo "Installing dependencies..." + npm install + # Update the node_modules timestamp so it won't reinstall next time + touch node_modules +fi + +# When the port is taken, no longer kill the old process; let server.ts auto-try 3457-3465 +if lsof -ti:3456 > /dev/null 2>&1; then + echo "Port 3456 is already in use; server will automatically try the next available port (see startup log below)" +fi + +echo "Starting BTC 5m order book monitor..." +APP_MODE=$(grep -E '^APP_MODE=' .env | tail -n1 | cut -d= -f2 | tr -d '\r' | tr -d '"') +if [ -z "$APP_MODE" ]; then + APP_MODE="full" +fi +echo "Run mode: $APP_MODE" +echo "Press Ctrl+C to exit" +echo "" + +# Clean up background child processes on exit (e.g. the subshell below that opens the browser asynchronously) +# Note: do not trap INT/TERM, otherwise bash handles it first and the signal won't reach the tsx/node process +trap 'kill -- -$$ 2>/dev/null' EXIT + +# Open the browser automatically once the service is ready +# Key: use the .port file written by the server to confirm the port of this startup (avoids opening an old server instance by mistake) +# First record the .port mtime before startup, then read once .port is refreshed (≠ old mtime) +if [ "$APP_MODE" != "headless" ] && [ "$(uname -s)" = "Darwin" ]; then + OLD_PORT_MTIME="" + if [ -f .port ]; then + OLD_PORT_MTIME=$(stat -f %m .port 2>/dev/null || echo "") + fi + ( + for i in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15; do + sleep 1 + if [ -f .port ]; then + NEW_MTIME=$(stat -f %m .port 2>/dev/null || echo "") + # The file was rewritten by this startup (mtime changed) and the service responds + if [ "$NEW_MTIME" != "$OLD_PORT_MTIME" ]; then + PORT=$(cat .port 2>/dev/null | tr -d '[:space:]') + if [ -n "$PORT" ] && curl -sf -m 1 "http://localhost:$PORT/api/state" > /dev/null 2>&1; then + open "http://localhost:$PORT" >/dev/null 2>&1 || true + exit 0 + fi + fi + fi + done + ) & +fi + +# Use exec so node/tsx directly replaces the bash process (PID unchanged, one less hop, more reliable signaling) +exec npx tsx ./server.ts diff --git a/strategies/STRATEGY-GUIDE.md b/strategies/STRATEGY-GUIDE.md new file mode 100644 index 0000000..3c22ad9 --- /dev/null +++ b/strategies/STRATEGY-GUIDE.md @@ -0,0 +1,504 @@ +# Strategy Development Guide + +This document explains how to add / modify Polymarket up/down strategies in this project. + +--- + +## 1. File naming + +Strategies go under the `strategies/` directory, with filename format: + +``` +.ts +``` + +- **prefix** by strategy type: + - `d` — diff-based (market path) + - `p` — prob-chase (market path) + - `t` — trend arbitrage (market path) + - `l` — limit-order (maker resting orders; see §6A) + - `m` — momentum (reserved) +- **number** increments (e.g. d1.ts, d2.ts, d3.ts) + +The bundled strategies are **d1 / p1 / p2** (market path). Limit-order strategies use the `l` prefix. + +On startup `_runtime/loader.ts` automatically scans and registers them, **with no changes to the main project code**. + +--- + +## 2. Strategy interface + +Each strategy exports a **class implementing the `IStrategy` interface** as the default export. + +Minimal skeleton (see [d1.ts](d1.ts) for a complete example): + +```ts +import type { + IStrategy, StrategyKey, StrategyNumber, + StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription, + StrategyDirection, +} from "./types.js"; + +export default class MyStrategy implements IStrategy { + readonly key: StrategyKey = "x1"; + readonly number: StrategyNumber = 1; + readonly name = "My Strategy"; + + // private state (must be cleared in resetState) + private peakDiff = 0; + + getDescription(): StrategyDescription { + return { + key: this.key, + number: this.number, + name: this.name, + title: "Strategy description", + category: { id: "diff", label: "Diff", color: "#58a6ff" }, + supportedMarkets: ["btc-5m"], // see §3 + lines: [ + { text: "📈 Entry condition description..." }, + ], + }; + } + + updateGuards(_ctx: StrategyTickContext): void { + // maintain cooldown locks, cold-start checks, etc. each tick + } + + checkEntry(ctx: StrategyTickContext): EntrySignal | null { + if (ctx.diffBps != null && ctx.diffBps >= 8) { + return { direction: "up" }; + } + return null; + } + + checkExit(_ctx: StrategyTickContext, _direction: StrategyDirection): ExitSignal { + // usually only return sl; take profit goes via GTC (see §6) + return null; + } + + resetState(): void { + this.peakDiff = 0; + } + + getStatePayload(): Record { + return { peakDiff: this.peakDiff }; + } +} +``` + +--- + +## 3. Market allowlist `supportedMarkets` + +Declare **which markets this strategy supports**. Unsupported markets are not shown by the frontend and skipped during the backend tick. + +```ts +supportedMarkets: ["btc-5m"] // BTC 5m only +supportedMarkets: ["btc-5m", "btc-15m"] // both BTC periods +supportedMarkets: ["eth-5m", "sol-5m"] // ETH and SOL 5m +// omitted / empty array = supports all markets (not recommended; most strategy thresholds are strongly coin-dependent) +``` + +**market key format**: `{symbol}-{period}` +- symbol: `btc` / `eth` / `sol` (see [market-configs.ts](../market-configs.ts)) +- period: `5m` / `15m` + +**Convention**: write a separate strategy file per coin + period. +- Example: BTC 5m uses d1.ts, ETH 5m uses d10.ts, BTC 15m uses d20.ts +- Do not split logic with if-else in the same file; it is hard to maintain + +--- + +## 4. TickContext fields + +`checkEntry` / `checkExit` / `updateGuards` all receive a `StrategyTickContext`: + +| Field | Type | Meaning | +|---|---|---| +| `rem` | number | current window remaining seconds (5m 0~300, 15m 0~900) | +| `upPct` | number\|null | Polymarket up probability, 0-100 integer | +| `dnPct` | number\|null | down probability (= 100 - upPct) | +| `diff` | number\|null | Binance current price - window open price (**absolute USD value**) | +| `diffBps` | number\|null | diff as bps of PTB, **cross-market generic** | +| `prevUpPct` | number\|null | the previous tick's upPct, used to detect threshold crossings | +| `kline1m` | Kline[] | Binance 1m K-lines (latest at the end) | +| `kline5m` | Kline[] | Binance 5m K-lines | +| `marketHoursOnly` | boolean | user config: whether to enter only during US stock market hours | +| `now` | number | current timestamp (ms) | + +### diff vs diffBps — which to use? + +- **Writing a cross-market strategy → use diffBps** (recommended) + - Example: BTC `diff=50` → `diffBps≈7`, SOL `diff=0.05` → `diffBps≈25` + - Thresholds written in bps are cross-market readable: `if (ctx.diffBps >= 10)` means a 0.1% move +- **Writing a BTC-5m-only legacy strategy → use diff** (keep as is) + - Do not casually change the USD thresholds of the legacy d1/d2/m1 etc.; they are already hand-tuned + +### Unit conversion + +- 1% = 100 bps +- 0.1% = 10 bps +- 0.01% = 1 bps +- diffBps is rounded to 2 decimal places + +--- + +## 5. Entry signal `checkEntry` + +```ts +checkEntry(ctx): EntrySignal | null { + // return null for no entry + // return { direction: "up" | "down" } to enter +} +``` + +**When called**: once every 250ms, only when `strategyRuntime.state === "SCANNING"` (the idle scanning period after IDLE). + +**Notes**: +- Do not write state-machine transition logic here — after returning a signal, server.ts handles it itself +- With multiple concurrent strategies, **the first strategy to return non-null wins**, iterated in strategyKeys order +- Data guard: skip when `upPct == null` / `diff == null` (data not ready) + +--- + +## 6. Exit signal `checkExit` + +```ts +checkExit(ctx, direction): ExitSignal { + // return null for no exit + // return { signal: "sl", reason: "..." } for stop loss + // take profit is generally not returned here (see below) +} +``` + +### Take profit goes via GTC limit order (important convention) + +**Why**: Polymarket waives the taker fee (~1.56%) for makers and pays a rebate, so a complete buy+sell using maker orders saves ~3% in fees. + +**Implementation** (market-path strategies, i.e. those that enter via `checkEntry`): +1. The strategy implements the optional method `getMarketTakeProfitPrice(): number | null` returning the absolute target price (0~1): + ```ts + getMarketTakeProfitPrice(): number | null { + return 0.98; // null = no take profit, hold to settlement + } + ``` +2. After the buy is MINED, server.ts automatically places a GTC sell order at that price (via the cond system) using the real filled shares +3. **Do not write a tp branch in `checkExit`**; keep only sl +4. Fallback: GTC minimum is 5 shares; if insufficient or order placement fails, the server automatically falls back to a local market take profit + +> Limit-path strategies (`orderType: "limit"`) do **not** use this method — they manage take profit / stop loss through `getLimitConditionOrder` instead. See §6A. + +### No-take-profit strategies + +The bundled strategies (d1 / p1 / p2) deliberately do not take profit — data proves that once BTC commits to a direction it reaches 99-100% at settlement, so taking profit actually earns less. They hold to settlement and return only an `sl` signal from `checkExit`, so none of them implement `getMarketTakeProfitPrice`. + +--- + +## 6A. Limit-order strategies (`checkLimitOrder` / `checkCancelOrder` / `getLimitConditionOrder`) + +Everything above (§5 `checkEntry`, §6 `checkExit`) is the **market path**: the server takes the order book and buys at market on a signal. A **limit-order strategy** is a separate path — it rests a GTC maker buy order on the book, manages its own cancellation, and on fill places a co-managed take-profit + stop-loss pair. Using maker orders on both legs is what saves the ~3% fee. + +A strategy opts into this path purely by what it implements; the main project code never changes. + +### Opting in + +Set `orderType: "limit"` in `getDescription()` and implement `checkLimitOrder`. The market-path methods become no-ops: + +```ts +getDescription(): StrategyDescription { + return { + key: this.key, number: this.number, name: this.name, + title: "...", + category: { id: "limit-diff", label: "Limit Diff", color: "#3fb950" }, + supportedMarkets: ["btc-5m"], + orderType: "limit", // ← declares this as a limit strategy + lines: [ /* ... */ ], + }; +} + +// market path unused — return nothing +checkEntry(_ctx) { return null; } +checkExit(_ctx, _dir) { return null; } +``` + +(`orderType` may also be `"both"` if a strategy uses the market and limit paths together. Omitted defaults to `"market"`.) + +### The three hooks and when the server calls them + +The server runs a limit tick every cycle. For each enabled limit strategy it follows this state machine: + +| Situation | Hook called | Return | Server action | +|---|---|---|---| +| No active resting order this window | `checkLimitOrder(ctx)` | `LimitOrderSignal` | place a GTC maker buy at the returned price/shares | +| | | `null` | do nothing this tick | +| An order is resting (unfilled / partially filled) | `checkCancelOrder(ctx, order)` | `true` | cancel the resting order | +| | | `false` | leave it on the book | +| A resting order is confirmed filled on-chain (MINED) | `getLimitConditionOrder()` | `{ stopProfit?, stopLoss? }` | place the TP + SL conditional pair for the filled shares | + +Key timing rules the server enforces for you — you do **not** code these: + +- **One order per window** by default. After placing, the window is marked; `checkLimitOrder` won't be called again that window. (Set `limitAllowReplaceAfterCancel = true` to allow re-placing after a cancel — see below.) +- **`checkLimitOrder` and `checkCancelOrder` are mutually exclusive per tick**: the place hook only runs when there is no resting order, the cancel hook only when there is one. +- **Remaining-shares cancellation after a fill is automatic** — your `checkCancelOrder` does not need to handle "cancel the unfilled remainder once partially filled". +- **All resting orders and presign caches are cleared on window switch.** + +### `checkLimitOrder` — place a resting buy + +Called each tick while the strategy has no active order. Return a `LimitOrderSignal` to rest a GTC maker buy, or `null` to wait. + +```ts +checkLimitOrder(ctx: StrategyTickContext): LimitOrderSignal | null { + // decide direction / price / shares from ctx (rem, diff, upPct, dnPct, ...) + if (/* your entry condition */) { + return { + direction: "up", // or "down" + price: 0.50, // 0~1 absolute limit price + shares: this.shares, // minimum 5 (Polymarket maker minimum) + }; + } + return null; +} +``` + +### `checkCancelOrder` — pull the resting buy + +Called each tick while an order is resting. Return `true` to cancel (e.g. the edge that justified the order has decayed, or the window is near its end), `false` to keep resting. The current order's runtime state is passed in: + +```ts +checkCancelOrder(ctx: StrategyTickContext, order: LimitOrderRuntime): boolean { + // order: { direction, price, shares, filledSize, windowStart } + if (/* condition gone */) return true; + return false; +} +``` + +### `getLimitConditionOrder` — TP + SL after fill + +Called once the resting buy is confirmed filled on-chain. Return a take-profit and/or stop-loss spec; the server places them as a **shared group** (triggering either one auto-cancels the other), reusing the same conditional-order infrastructure as manual TP/SL. + +```ts +getLimitConditionOrder() { + return { + // take profit (omit for no TP → hold to settlement): + stopProfit: { pctDelta: 0.05 }, // tp price = entryPrice + 0.05 + // ...or an absolute price instead: { targetPrice: 0.99 } + // stop loss (diff-crossing, market sell): + stopLoss: { diffValue: 10, slippage: 0.15 }, + // buy up → triggers when diff ≤ -diffValue + // buy down→ triggers when diff ≥ +diffValue + }; +} +``` + +- Omit `stopProfit` → no take profit, the filled shares are held to settlement. +- Omit `stopLoss` → no automatic stop loss. +- This interface **takes precedence over** the simpler `getLimitTakeProfitPrice()` (TP-only) fallback; implement one or the other. + +### Optional refinements + +- **`limitAllowReplaceAfterCancel?: boolean`** (default `false`). When `true`, the per-window mark is cleared after a cancel, so `checkLimitOrder` may place a fresh order again later in the same window (useful for full-window strategies that re-arm after a false start). When `false`, one cancel ends the strategy's activity for that window. + +- **`getPresignRequest?(): PresignRequest | null`** — a pure latency optimization. Declare the rem interval / directions / price / shares you expect to use, and the server pre-signs the order package in the background so that when `checkLimitOrder` fires it can `postOrder` immediately and skip the signing round-trip. Your `checkLimitOrder` does not need to know whether the presign hit — it just returns the signal as usual; presigning is invisible to strategy logic and is invalidated on window switch. + +### Minimal limit-strategy skeleton + +```ts +import type { + IStrategy, StrategyKey, StrategyNumber, StrategyDirection, + StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription, + LimitOrderSignal, LimitOrderRuntime, +} from "./types.js"; + +export class MyLimitStrategy implements IStrategy { + readonly key: StrategyKey = "l9"; + readonly number: StrategyNumber = 9; + readonly name = "My Limit Strategy"; + shares = 5; + readonly limitAllowReplaceAfterCancel = true; + + getDescription(): StrategyDescription { + return { + key: this.key, number: this.number, name: this.name, + title: "Limit example", + category: { id: "limit-diff", label: "Limit Diff", color: "#3fb950" }, + supportedMarkets: ["btc-5m"], + orderType: "limit", + lines: [{ text: "..." }], + }; + } + + updateGuards(_ctx: StrategyTickContext): void {} + checkEntry(_ctx: StrategyTickContext): EntrySignal | null { return null; } + checkExit(_ctx: StrategyTickContext, _d: StrategyDirection): ExitSignal { return null; } + + checkLimitOrder(ctx: StrategyTickContext): LimitOrderSignal | null { + // return { direction, price, shares } when conditions are met, else null + return null; + } + + checkCancelOrder(ctx: StrategyTickContext, _order: LimitOrderRuntime): boolean { + // return true to cancel the resting order + return false; + } + + getLimitConditionOrder() { + return { stopProfit: { pctDelta: 0.05 }, stopLoss: { diffValue: 10, slippage: 0.15 } }; + } + + resetState(): void {} + getStatePayload(): Record { return {}; } +} +``` + +> The filename prefix for limit strategies is `l` (see §1). The frontend renders a **shares** input box for `orderType: "limit"` strategies (instead of the amount box used by market strategies). + +--- + +## 7. Strategy lifecycle + +``` +IDLE → SCANNING → BUYING → WAIT_FILL → HOLDING → SELLING → DONE +``` + +Methods called in each state: + +| State | Calls | Description | +|---|---|---| +| SCANNING | `checkEntry` | look for entry opportunities | +| BUYING / WAIT_FILL | — | place order / wait for fill | +| HOLDING | `checkExit` | look for stop loss | +| SELLING | — | closing the position | + +**`updateGuards`**: called every tick (regardless of state), used to maintain the strategy's private state (cooldown locks, consecutive observation counts, etc.). + +**`onEntryFilled`** (optional): called once after a buy fills, used to record entryPrice etc. + +**`resetState`**: called on window switch / strategy switch, clears private fields. + +**`getStatePayload`**: returns the strategy's private state to display on the frontend (e.g. peakDiff). + +--- + +## 8. Data tables (fair-prob / diff-extremes) + +When a strategy needs to look up a table (e.g. the p-series looks up the fair-prob deviation): + +```ts +import { getFairProb } from "./_core/fair-prob.js"; + +const fair = getFairProb(ctx.diff, ctx.rem); +if (fair == null) return null; // current market has no table, safely skip +``` + +### Adding a table for a new market + +Data tables use a `Record` structure; adding a new table changes only one place: + +```ts +// strategies/_core/fair-prob.ts +export const FAIR_PROB_TABLES: Record = { + "btc-5m": BTC_5M, + "eth-5m": ETH_5M, // ← new + "btc-15m": BTC_15M, // ← new +}; +``` + +Generate data: run `python3 backtest-data/analyze.py --symbol --period

` and paste the output in. + +### Guard mechanism + +- Current market has no table → `getFairProb` returns null +- Strategy receives null → does not enter +- Combined with `supportedMarkets`, double protection + +--- + +## 9. Observe panel (optional) + +A strategy can display an observe panel in the frontend's top status bar (e.g. m1's factor scoring): + +```ts +readonly alwaysComputeData = true; // run computeData even when the strategy is disabled + +computeData(ctx: StrategyTickContext): void { + // compute panel data, store to this.xxx +} + +getObservePanel(): ObservePanelData { + return { + title: "Entry factors", + color: "#3fb950", + rows: [ + { type: "score", label: "Momentum", value: this.score, threshold: 60 }, + { type: "direction", label: "Direction", value: "up" }, + ], + }; +} +``` + +The frontend's generic renderer displays it automatically, with no extra frontend code. + +--- + +## 10. New strategy checklist + +After writing a new strategy, self-check against this list: + +- [ ] filename `.ts` is unique +- [ ] `key` matches the filename +- [ ] `number` matches the filename's number and is globally unique +- [ ] implements `getDescription` / `updateGuards` / `checkEntry` / `checkExit` / `resetState` / `getStatePayload` +- [ ] **`supportedMarkets` is declared** (otherwise it shows in all markets but uses the wrong thresholds) +- [ ] **all private state is cleared in `resetState`** +- [ ] data guard: correctly handles when `upPct == null` / `diff == null` / `getFairProb()` returns null +- [ ] cross-market strategy: use `ctx.diffBps` instead of `ctx.diff` +- [ ] take profit goes via GTC: implement `getTargetPrice`, do not write a tp branch in `checkExit` +- [ ] after restarting the service, `/api/strategy/descriptions` shows the new strategy +- [ ] switching to the corresponding market on the frontend shows the strategy toggle +- [ ] after enabling, the logs `[Strategy]` / `[Order]` show entry behavior + +--- + +## 11. Debugging tips + +### Dry-run (no order) test + +Add a temporary `console.log` to the strategy file, start the service but **do not check enable on the frontend** — `updateGuards` / `computeData` are still called (if `alwaysComputeData` is declared), so you can observe scoring without actually placing orders. + +### View live data + +Open the frontend → look at the "Prob Chase" panel at the top for the fair / bias values, or the "Momentum" panel for the factor values. + +### Historical data backtest + +Run `npx tsx backtest/.ts` to replay historical jsonl — for how to write the script, refer to [backtest/diff-extremes.ts](../backtest/diff-extremes.ts). + +### Test a single market in isolation + +Before starting, change `.active-market.json` to `{"key":"eth-5m"}` and restart the service. Or switch via the top dropdown on the frontend. + +--- + +## 12. Common pitfalls + +1. **Forgetting to clear state** — `resetState` not cleaned thoroughly, leaving stale peakDiff and the like across windows. +2. **Using ctx.diff in a cross-market strategy** — a BTC threshold of $35 triggers at $0.35 on ETH, inevitably causing bad entries. **Use diffBps**. +3. **Writing a tp branch in checkExit** — conflicts with GTC and may sell twice. Take profit always via `getTargetPrice`. +4. **Wrong supportedMarkets key** — e.g. `["BTC-5m"]` (uppercase) → never matches, strategy not shown. Must be lowercase `["btc-5m"]`. +5. **Depending on fair-prob but forgetting to check null** — switching to a market without a table makes `getFairProb` return null; not handling it makes NaN comparisons always false (seemingly harmless but hard to spot). +6. **Static field sharing** — using `static` fields shares them across all strategy instances. Use `private` instance fields for private state. + +--- + +## Appendix: related file index + +- [types.ts](types.ts) — interface definitions +- [registry.ts](registry.ts) — registry (usually no need to change) +- [_runtime/loader.ts](_runtime/loader.ts) — auto loader +- [_core/fair-prob.ts](_core/fair-prob.ts) — fair probability table +- [_core/diff-extremes.ts](_core/diff-extremes.ts) — diff extremes table +- [_core/s6-core.ts](_core/s6-core.ts) — momentum scoring shared logic +- [../market-configs.ts](../market-configs.ts) — market config (symbol / period / slug etc.) +- [../server.ts](../server.ts) — main service (strategy scheduling, order placement) +- [../CLAUDE.md](../CLAUDE.md) — project overview diff --git a/strategies/_core/diff-extremes.ts b/strategies/_core/diff-extremes.ts new file mode 100644 index 0000000..85c9809 --- /dev/null +++ b/strategies/_core/diff-extremes.ts @@ -0,0 +1,84 @@ +/** + * diff extremes mapping table — used by adaptive-threshold strategies such as p2 + * + * Data structure: EXTREME_TABLES is Record + * - key shaped like "btc-5m", "eth-5m" + * - each market maintains its own (diff magnitude and distribution differ completely across markets) + * + * Adding a new market table: + * 1. Run the backtest: `npx tsx backtest/diff-extremes.ts --market ` + * 2. Add the generated array to EXTREME_TABLES["sym-period"] + * 3. Restart the service + * + * A market without a table makes getExtremeThreshold() return null, and related strategies automatically no-op + * (combined with the strategy's supportedMarkets allowlist, double protection) + */ + +// Percentiles for each bin (p50/p70/p80/p85/p90/p93/p95/p97/p99) +type PercentileRow = { + p50: number; p70: number; p80: number; p85: number; + p90: number; p93: number; p95: number; p97: number; p99: number; +}; + +type RawData = Array<[string, PercentileRow]>; // [rem bin, percentile row] + +// BTC 5m — statistics based on 6 days of tick data from 2026-04-04 ~ 2026-04-09 +const BTC_5M: RawData = [ + ["0-30", { p50: 32, p70: 56, p80: 77, p85: 91, p90: 115, p93: 136, p95: 167, p97: 200, p99: 303 }], + ["30-60", { p50: 30, p70: 53, p80: 73, p85: 86, p90: 109, p93: 135, p95: 158, p97: 191, p99: 295 }], + ["60-90", { p50: 28, p70: 50, p80: 68, p85: 82, p90: 103, p93: 128, p95: 151, p97: 194, p99: 290 }], + ["90-120", { p50: 26, p70: 48, p80: 66, p85: 79, p90: 99, p93: 125, p95: 150, p97: 192, p99: 288 }], + ["120-150", { p50: 25, p70: 46, p80: 64, p85: 76, p90: 98, p93: 118, p95: 139, p97: 182, p99: 278 }], + ["150-180", { p50: 24, p70: 43, p80: 59, p85: 70, p90: 91, p93: 110, p95: 127, p97: 159, p99: 244 }], + ["180-210", { p50: 21, p70: 38, p80: 53, p85: 64, p90: 80, p93: 97, p95: 115, p97: 147, p99: 210 }], + ["210-240", { p50: 18, p70: 33, p80: 46, p85: 54, p90: 68, p93: 81, p95: 95, p97: 121, p99: 182 }], + ["240-270", { p50: 14, p70: 24, p80: 33, p85: 39, p90: 49, p93: 57, p95: 65, p97: 81, p99: 127 }], + ["270-300", { p50: 8, p70: 17, p80: 23, p85: 28, p90: 35, p93: 41, p95: 48, p97: 59, p99: 89 }], +]; + +// Extreme tables per market — to add a new market, just add an entry here +export const EXTREME_TABLES: Record = { + "btc-5m": BTC_5M, + // "eth-5m": ETH_5M, + // "btc-15m": BTC_15M, +}; + +// Precompiled into a Map, by marketKey → (binKey → PercentileRow) +const COMPILED_MAPS = new Map>(); +for (const [marketKey, raw] of Object.entries(EXTREME_TABLES)) { + const m = new Map(); + for (const [bin, row] of raw) m.set(bin, row); + COMPILED_MAPS.set(marketKey, m); +} + +/** Look up the bin by rem value (one bin per 30 seconds, covering 0-300) */ +export function getExtremeBin(rem: number): string | null { + if (rem < 0 || rem >= 300) return rem >= 300 ? "270-300" : null; + const lo = Math.floor(rem / 30) * 30; + return `${lo}-${lo + 30}`; +} + +/** Query the percentile extreme at a given rem; returns null when the current market has no table */ +export function getExtremeThreshold( + rem: number, + percentile: 50 | 70 | 80 | 85 | 90 | 93 | 95 | 97 | 99, +): number | null { + const map = COMPILED_MAPS.get(activeMarketKey); + if (!map) return null; + const bin = getExtremeBin(rem); + if (!bin) return null; + const row = map.get(bin); + if (!row) return null; + return row[`p${percentile}` as keyof PercentileRow]; +} + +/** Whether the current market has an extreme table */ +export function hasExtremeTable(marketKey?: string): boolean { + return COMPILED_MAPS.has(marketKey ?? activeMarketKey); +} + +// Current active market key (synced by server.ts when switching markets) +let activeMarketKey = "btc-5m"; +export function setActiveMarket(sym: string, period: string): void { + activeMarketKey = `${sym}-${period}`; +} diff --git a/strategies/_core/fair-prob.ts b/strategies/_core/fair-prob.ts new file mode 100644 index 0000000..a6bddde --- /dev/null +++ b/strategies/_core/fair-prob.ts @@ -0,0 +1,441 @@ +/** + * Fair probability mapping table — shared by the p-series strategies + * + * Data structure: FAIR_PROB_TABLES is Record + * - key shaped like "btc-5m", "eth-5m", "btc-15m" + * - each market maintains its own table (the fair-probability distribution differs completely across markets) + * + * Adding a new market table: + * 1. Run the backtest: `python3 backtest-data/analyze.py --symbol --period

` + * 2. Add the generated RawData array to FAIR_PROB_TABLES["sym-period"] + * 3. Restart the service + * + * A market without a table makes getFairProb() return null, and p-series strategies automatically no-op + * (combined with the strategy's supportedMarkets allowlist, double protection) + */ + +type RawData = Array<[number, string, number]>; // [diff bucket, rem bin, upPct median] + +// BTC 5m table (based on 37 days of ~2.5M tick statistics; step=2, updated 2026-05-09) +// How to update: rerun the backtest tool to regenerate and replace the data below +const BTC_5M: RawData = [ + [-140,"0-30",1],[-140,"30-60",1],[-140,"60-120",2],[-140,"120-180",6],[-140,"180-240",10],[-140,"240-300",16], + [-138,"0-30",0],[-138,"30-60",0],[-138,"60-120",2],[-138,"120-180",5],[-138,"180-240",14],[-138,"240-300",15], + [-136,"0-30",0],[-136,"30-60",0],[-136,"60-120",6],[-136,"120-180",10],[-136,"180-240",11],[-136,"240-300",14], + [-134,"0-30",0],[-134,"30-60",1],[-134,"60-120",12],[-134,"120-180",8],[-134,"180-240",10],[-134,"240-300",16], + [-132,"0-30",0],[-132,"30-60",2],[-132,"60-120",5],[-132,"120-180",12],[-132,"180-240",22],[-132,"240-300",29], + [-130,"0-30",0],[-130,"30-60",1],[-130,"60-120",7],[-130,"120-180",9],[-130,"180-240",19],[-130,"240-300",10], + [-128,"0-30",1],[-128,"30-60",7],[-128,"60-120",8],[-128,"120-180",16],[-128,"180-240",14],[-128,"240-300",34], + [-126,"0-30",0],[-126,"30-60",1],[-126,"60-120",3],[-126,"120-180",12],[-126,"180-240",12],[-126,"240-300",41], + [-124,"0-30",0],[-124,"30-60",8],[-124,"60-120",4],[-124,"120-180",9],[-124,"180-240",16],[-124,"240-300",12], + [-122,"0-30",0],[-122,"30-60",0],[-122,"60-120",1],[-122,"120-180",10],[-122,"180-240",15],[-122,"240-300",14], + [-120,"0-30",1],[-120,"30-60",5],[-120,"60-120",2],[-120,"120-180",6],[-120,"180-240",19],[-120,"240-300",18], + [-118,"0-30",0],[-118,"30-60",1],[-118,"60-120",1],[-118,"120-180",10],[-118,"180-240",13],[-118,"240-300",25], + [-116,"0-30",1],[-116,"30-60",6],[-116,"60-120",4],[-116,"120-180",6],[-116,"180-240",17],[-116,"240-300",22], + [-114,"0-30",0],[-114,"30-60",1],[-114,"60-120",5],[-114,"120-180",7],[-114,"180-240",9],[-114,"240-300",25], + [-112,"0-30",1],[-112,"30-60",3],[-112,"60-120",3],[-112,"120-180",5],[-112,"180-240",10],[-112,"240-300",14], + [-110,"0-30",0],[-110,"30-60",0],[-110,"60-120",2],[-110,"120-180",5],[-110,"180-240",10],[-110,"240-300",25], + [-108,"0-30",0],[-108,"30-60",4],[-108,"60-120",3],[-108,"120-180",6],[-108,"180-240",10],[-108,"240-300",31], + [-106,"0-30",0],[-106,"30-60",0],[-106,"60-120",5],[-106,"120-180",7],[-106,"180-240",8],[-106,"240-300",16], + [-104,"0-30",0],[-104,"30-60",1],[-104,"60-120",3],[-104,"120-180",5],[-104,"180-240",14],[-104,"240-300",18], + [-102,"0-30",0],[-102,"30-60",1],[-102,"60-120",5],[-102,"120-180",6],[-102,"180-240",10],[-102,"240-300",3], + [-100,"0-30",0],[-100,"30-60",1],[-100,"60-120",4],[-100,"120-180",6],[-100,"180-240",15],[-100,"240-300",17], + [-98,"0-30",0],[-98,"30-60",1],[-98,"60-120",4],[-98,"120-180",7],[-98,"180-240",10],[-98,"240-300",24], + [-96,"0-30",0],[-96,"30-60",4],[-96,"60-120",6],[-96,"120-180",10],[-96,"180-240",7],[-96,"240-300",21], + [-94,"0-30",0],[-94,"30-60",0],[-94,"60-120",2],[-94,"120-180",5],[-94,"180-240",12],[-94,"240-300",29], + [-92,"0-30",0],[-92,"30-60",2],[-92,"60-120",2],[-92,"120-180",11],[-92,"180-240",15],[-92,"240-300",24], + [-90,"0-30",5],[-90,"30-60",9],[-90,"60-120",3],[-90,"120-180",7],[-90,"180-240",14],[-90,"240-300",20], + [-88,"0-30",2],[-88,"30-60",2],[-88,"60-120",9],[-88,"120-180",9],[-88,"180-240",12],[-88,"240-300",18], + [-86,"0-30",0],[-86,"30-60",1],[-86,"60-120",6],[-86,"120-180",5],[-86,"180-240",20],[-86,"240-300",16], + [-84,"0-30",1],[-84,"30-60",4],[-84,"60-120",8],[-84,"120-180",11],[-84,"180-240",25],[-84,"240-300",23], + [-82,"0-30",3],[-82,"30-60",5],[-82,"60-120",7],[-82,"120-180",11],[-82,"180-240",16],[-82,"240-300",18], + [-80,"0-30",3],[-80,"30-60",5],[-80,"60-120",6],[-80,"120-180",11],[-80,"180-240",22],[-80,"240-300",20], + [-78,"0-30",3],[-78,"30-60",3],[-78,"60-120",6],[-78,"120-180",10],[-78,"180-240",18],[-78,"240-300",28], + [-76,"0-30",1],[-76,"30-60",4],[-76,"60-120",6],[-76,"120-180",13],[-76,"180-240",24],[-76,"240-300",39], + [-74,"0-30",3],[-74,"30-60",4],[-74,"60-120",6],[-74,"120-180",12],[-74,"180-240",16],[-74,"240-300",37], + [-72,"0-30",0],[-72,"30-60",3],[-72,"60-120",10],[-72,"120-180",13],[-72,"180-240",20],[-72,"240-300",31], + [-70,"0-30",2],[-70,"30-60",5],[-70,"60-120",6],[-70,"120-180",11],[-70,"180-240",22],[-70,"240-300",27], + [-68,"0-30",3],[-68,"30-60",5],[-68,"60-120",12],[-68,"120-180",21],[-68,"180-240",19],[-68,"240-300",25], + [-66,"0-30",5],[-66,"30-60",10],[-66,"60-120",9],[-66,"120-180",13],[-66,"180-240",20],[-66,"240-300",30], + [-64,"0-30",4],[-64,"30-60",7],[-64,"60-120",13],[-64,"120-180",14],[-64,"180-240",19],[-64,"240-300",35], + [-62,"0-30",3],[-62,"30-60",8],[-62,"60-120",9],[-62,"120-180",13],[-62,"180-240",21],[-62,"240-300",30], + [-60,"0-30",3],[-60,"30-60",5],[-60,"60-120",12],[-60,"120-180",13],[-60,"180-240",22],[-60,"240-300",33], + [-58,"0-30",1],[-58,"30-60",7],[-58,"60-120",12],[-58,"120-180",19],[-58,"180-240",18],[-58,"240-300",25], + [-56,"0-30",3],[-56,"30-60",6],[-56,"60-120",10],[-56,"120-180",19],[-56,"180-240",22],[-56,"240-300",36], + [-54,"0-30",3],[-54,"30-60",8],[-54,"60-120",8],[-54,"120-180",21],[-54,"180-240",25],[-54,"240-300",32], + [-52,"0-30",8],[-52,"30-60",7],[-52,"60-120",12],[-52,"120-180",21],[-52,"180-240",24],[-52,"240-300",32], + [-50,"0-30",3],[-50,"30-60",5],[-50,"60-120",13],[-50,"120-180",19],[-50,"180-240",21],[-50,"240-300",31], + [-48,"0-30",11],[-48,"30-60",12],[-48,"60-120",13],[-48,"120-180",19],[-48,"180-240",25],[-48,"240-300",36], + [-46,"0-30",4],[-46,"30-60",7],[-46,"60-120",13],[-46,"120-180",17],[-46,"180-240",25],[-46,"240-300",31], + [-44,"0-30",5],[-44,"30-60",8],[-44,"60-120",16],[-44,"120-180",21],[-44,"180-240",23],[-44,"240-300",33], + [-42,"0-30",8],[-42,"30-60",12],[-42,"60-120",18],[-42,"120-180",19],[-42,"180-240",23],[-42,"240-300",31], + [-40,"0-30",7],[-40,"30-60",14],[-40,"60-120",17],[-40,"120-180",23],[-40,"180-240",25],[-40,"240-300",32], + [-38,"0-30",10],[-38,"30-60",15],[-38,"60-120",13],[-38,"120-180",22],[-38,"180-240",29],[-38,"240-300",34], + [-36,"0-30",9],[-36,"30-60",13],[-36,"60-120",18],[-36,"120-180",23],[-36,"180-240",28],[-36,"240-300",36], + [-34,"0-30",7],[-34,"30-60",10],[-34,"60-120",18],[-34,"120-180",21],[-34,"180-240",23],[-34,"240-300",36], + [-32,"0-30",9],[-32,"30-60",13],[-32,"60-120",18],[-32,"120-180",24],[-32,"180-240",30],[-32,"240-300",34], + [-30,"0-30",11],[-30,"30-60",15],[-30,"60-120",21],[-30,"120-180",25],[-30,"180-240",31],[-30,"240-300",40], + [-28,"0-30",13],[-28,"30-60",18],[-28,"60-120",26],[-28,"120-180",27],[-28,"180-240",33],[-28,"240-300",36], + [-26,"0-30",11],[-26,"30-60",17],[-26,"60-120",26],[-26,"120-180",28],[-26,"180-240",32],[-26,"240-300",36], + [-24,"0-30",13],[-24,"30-60",21],[-24,"60-120",27],[-24,"120-180",30],[-24,"180-240",36],[-24,"240-300",39], + [-22,"0-30",14],[-22,"30-60",27],[-22,"60-120",24],[-22,"120-180",31],[-22,"180-240",40],[-22,"240-300",38], + [-20,"0-30",15],[-20,"30-60",23],[-20,"60-120",28],[-20,"120-180",33],[-20,"180-240",35],[-20,"240-300",41], + [-18,"0-30",14],[-18,"30-60",23],[-18,"60-120",28],[-18,"120-180",33],[-18,"180-240",40],[-18,"240-300",40], + [-16,"0-30",16],[-16,"30-60",25],[-16,"60-120",29],[-16,"120-180",38],[-16,"180-240",40],[-16,"240-300",43], + [-14,"0-30",16],[-14,"30-60",20],[-14,"60-120",35],[-14,"120-180",35],[-14,"180-240",38],[-14,"240-300",40], + [-12,"0-30",21],[-12,"30-60",31],[-12,"60-120",35],[-12,"120-180",36],[-12,"180-240",37],[-12,"240-300",44], + [-10,"0-30",22],[-10,"30-60",29],[-10,"60-120",35],[-10,"120-180",42],[-10,"180-240",39],[-10,"240-300",43], + [-8,"0-30",23],[-8,"30-60",32],[-8,"60-120",38],[-8,"120-180",39],[-8,"180-240",42],[-8,"240-300",47], + [-6,"0-30",31],[-6,"30-60",41],[-6,"60-120",38],[-6,"120-180",41],[-6,"180-240",45],[-6,"240-300",46], + [-4,"0-30",32],[-4,"30-60",36],[-4,"60-120",39],[-4,"120-180",42],[-4,"180-240",47],[-4,"240-300",46], + [-2,"0-30",49],[-2,"30-60",50],[-2,"60-120",54],[-2,"120-180",57],[-2,"180-240",53],[-2,"240-300",52], + [0,"0-30",51],[0,"30-60",53],[0,"60-120",54],[0,"120-180",53],[0,"180-240",54],[0,"240-300",53], + [2,"0-30",58],[2,"30-60",60],[2,"60-120",59],[2,"120-180",55],[2,"180-240",55],[2,"240-300",55], + [4,"0-30",66],[4,"30-60",61],[4,"60-120",56],[4,"120-180",53],[4,"180-240",54],[4,"240-300",54], + [6,"0-30",73],[6,"30-60",65],[6,"60-120",62],[6,"120-180",58],[6,"180-240",57],[6,"240-300",55], + [8,"0-30",76],[8,"30-60",69],[8,"60-120",63],[8,"120-180",62],[8,"180-240",58],[8,"240-300",56], + [10,"0-30",80],[10,"30-60",67],[10,"60-120",71],[10,"120-180",63],[10,"180-240",60],[10,"240-300",61], + [12,"0-30",81],[12,"30-60",69],[12,"60-120",65],[12,"120-180",59],[12,"180-240",60],[12,"240-300",60], + [14,"0-30",81],[14,"30-60",74],[14,"60-120",69],[14,"120-180",62],[14,"180-240",63],[14,"240-300",62], + [16,"0-30",86],[16,"30-60",71],[16,"60-120",65],[16,"120-180",65],[16,"180-240",68],[16,"240-300",62], + [18,"0-30",89],[18,"30-60",79],[18,"60-120",71],[18,"120-180",66],[18,"180-240",62],[18,"240-300",65], + [20,"0-30",91],[20,"30-60",84],[20,"60-120",71],[20,"120-180",71],[20,"180-240",65],[20,"240-300",60], + [22,"0-30",86],[22,"30-60",86],[22,"60-120",77],[22,"120-180",71],[22,"180-240",73],[22,"240-300",67], + [24,"0-30",91],[24,"30-60",81],[24,"60-120",77],[24,"120-180",74],[24,"180-240",68],[24,"240-300",64], + [26,"0-30",91],[26,"30-60",89],[26,"60-120",80],[26,"120-180",74],[26,"180-240",69],[26,"240-300",68], + [28,"0-30",89],[28,"30-60",85],[28,"60-120",82],[28,"120-180",77],[28,"180-240",68],[28,"240-300",66], + [30,"0-30",91],[30,"30-60",87],[30,"60-120",80],[30,"120-180",77],[30,"180-240",74],[30,"240-300",71], + [32,"0-30",92],[32,"30-60",82],[32,"60-120",79],[32,"120-180",79],[32,"180-240",74],[32,"240-300",70], + [34,"0-30",88],[34,"30-60",86],[34,"60-120",86],[34,"120-180",83],[34,"180-240",69],[34,"240-300",75], + [36,"0-30",94],[36,"30-60",88],[36,"60-120",86],[36,"120-180",80],[36,"180-240",72],[36,"240-300",69], + [38,"0-30",95],[38,"30-60",86],[38,"60-120",85],[38,"120-180",83],[38,"180-240",73],[38,"240-300",75], + [40,"0-30",97],[40,"30-60",94],[40,"60-120",88],[40,"120-180",81],[40,"180-240",80],[40,"240-300",74], + [42,"0-30",97],[42,"30-60",89],[42,"60-120",85],[42,"120-180",78],[42,"180-240",74],[42,"240-300",73], + [44,"0-30",98],[44,"30-60",92],[44,"60-120",87],[44,"120-180",78],[44,"180-240",75],[44,"240-300",72], + [46,"0-30",97],[46,"30-60",92],[46,"60-120",89],[46,"120-180",77],[46,"180-240",77],[46,"240-300",73], + [48,"0-30",98],[48,"30-60",94],[48,"60-120",90],[48,"120-180",80],[48,"180-240",76],[48,"240-300",70], + [50,"0-30",99],[50,"30-60",95],[50,"60-120",85],[50,"120-180",84],[50,"180-240",76],[50,"240-300",73], + [52,"0-30",99],[52,"30-60",95],[52,"60-120",91],[52,"120-180",85],[52,"180-240",83],[52,"240-300",77], + [54,"0-30",99],[54,"30-60",96],[54,"60-120",89],[54,"120-180",81],[54,"180-240",79],[54,"240-300",69], + [56,"0-30",97],[56,"30-60",95],[56,"60-120",92],[56,"120-180",85],[56,"180-240",82],[56,"240-300",76], + [58,"0-30",98],[58,"30-60",95],[58,"60-120",89],[58,"120-180",85],[58,"180-240",78],[58,"240-300",74], + [60,"0-30",98],[60,"30-60",96],[60,"60-120",90],[60,"120-180",90],[60,"180-240",82],[60,"240-300",77], + [62,"0-30",99],[62,"30-60",95],[62,"60-120",90],[62,"120-180",90],[62,"180-240",79],[62,"240-300",80], + [64,"0-30",99],[64,"30-60",98],[64,"60-120",95],[64,"120-180",86],[64,"180-240",83],[64,"240-300",73], + [66,"0-30",100],[66,"30-60",96],[66,"60-120",94],[66,"120-180",86],[66,"180-240",85],[66,"240-300",70], + [68,"0-30",100],[68,"30-60",98],[68,"60-120",92],[68,"120-180",89],[68,"180-240",84],[68,"240-300",75], + [70,"0-30",99],[70,"30-60",98],[70,"60-120",91],[70,"120-180",87],[70,"180-240",84],[70,"240-300",79], + [72,"0-30",99],[72,"30-60",98],[72,"60-120",97],[72,"120-180",91],[72,"180-240",84],[72,"240-300",71], + [74,"0-30",99],[74,"30-60",99],[74,"60-120",91],[74,"120-180",87],[74,"180-240",81],[74,"240-300",79], + [76,"0-30",99],[76,"30-60",98],[76,"60-120",96],[76,"120-180",90],[76,"180-240",81],[76,"240-300",82], + [78,"0-30",100],[78,"30-60",100],[78,"60-120",96],[78,"120-180",91],[78,"180-240",82],[78,"240-300",79], + [80,"0-30",100],[80,"30-60",99],[80,"60-120",95],[80,"120-180",90],[80,"180-240",85],[80,"240-300",87], + [82,"0-30",100],[82,"30-60",99],[82,"60-120",94],[82,"120-180",92],[82,"180-240",90],[82,"240-300",81], + [84,"0-30",98],[84,"30-60",100],[84,"60-120",96],[84,"120-180",91],[84,"180-240",91],[84,"240-300",80], + [86,"0-30",100],[86,"30-60",100],[86,"60-120",98],[86,"120-180",92],[86,"180-240",88],[86,"240-300",86], + [88,"0-30",100],[88,"30-60",100],[88,"60-120",98],[88,"120-180",89],[88,"180-240",86],[88,"240-300",76], + [90,"0-30",100],[90,"30-60",100],[90,"60-120",96],[90,"120-180",92],[90,"180-240",88],[90,"240-300",77], + [92,"0-30",100],[92,"30-60",100],[92,"60-120",98],[92,"120-180",94],[92,"180-240",87],[92,"240-300",89], + [94,"0-30",100],[94,"30-60",99],[94,"60-120",98],[94,"120-180",90],[94,"180-240",87],[94,"240-300",86], + [96,"0-30",100],[96,"30-60",99],[96,"60-120",98],[96,"120-180",95],[96,"180-240",91],[96,"240-300",84], +]; + +// ETH 5m table (based on 13 days of ~940K tick statistics; step=0.1) +// Bucket design: step 0.1, range ±7 (covers 99% of the diff distribution) +const ETH_5M: RawData = [ + [-7.0,"0-30",18],[-7.0,"30-60",31],[-7.0,"60-120",2],[-7.0,"120-180",9],[-7.0,"180-240",25],[-7.0,"240-300",59], + [-6.9,"0-30",100],[-6.9,"30-60",33],[-6.9,"60-120",5],[-6.9,"120-180",35],[-6.9,"180-240",27],[-6.9,"240-300",0], + [-6.8,"0-30",100],[-6.8,"30-60",100],[-6.8,"60-120",31],[-6.8,"120-180",4],[-6.8,"180-240",11],[-6.8,"240-300",20], + [-6.7,"0-30",26],[-6.7,"30-60",0],[-6.7,"60-120",26],[-6.7,"120-180",43],[-6.7,"180-240",40],[-6.7,"240-300",25], + [-6.6,"0-30",100],[-6.6,"30-60",100],[-6.6,"60-120",9],[-6.6,"120-180",17],[-6.6,"180-240",20],[-6.6,"240-300",0], + [-6.5,"0-30",15],[-6.5,"30-60",0],[-6.5,"60-120",0],[-6.5,"120-180",12],[-6.5,"180-240",43],[-6.5,"240-300",20], + [-6.4,"0-30",28],[-6.4,"30-60",100],[-6.4,"60-120",4],[-6.4,"120-180",23],[-6.4,"180-240",4],[-6.4,"240-300",10], + [-6.3,"0-30",100],[-6.3,"30-60",40],[-6.3,"60-120",24],[-6.3,"120-180",0],[-6.3,"180-240",17],[-6.3,"240-300",60], + [-6.2,"0-30",100],[-6.2,"30-60",60],[-6.2,"60-120",18],[-6.2,"120-180",14],[-6.2,"180-240",0],[-6.2,"240-300",11], + [-6.1,"0-30",100],[-6.1,"30-60",33],[-6.1,"60-120",8],[-6.1,"120-180",26],[-6.1,"180-240",10],[-6.1,"240-300",22], + [-6.0,"0-30",11],[-6.0,"30-60",0],[-6.0,"60-120",30],[-6.0,"120-180",22],[-6.0,"180-240",7],[-6.0,"240-300",0], + [-5.9,"0-30",21],[-5.9,"30-60",50],[-5.9,"60-120",14],[-5.9,"120-180",20],[-5.9,"180-240",10],[-5.9,"240-300",12], + [-5.8,"0-30",19],[-5.8,"30-60",33],[-5.8,"60-120",31],[-5.8,"120-180",19],[-5.8,"180-240",5],[-5.8,"240-300",8], + [-5.7,"0-30",7],[-5.7,"30-60",0],[-5.7,"60-120",0],[-5.7,"120-180",15],[-5.7,"180-240",12],[-5.7,"240-300",8], + [-5.6,"0-30",9],[-5.6,"30-60",0],[-5.6,"60-120",6],[-5.6,"120-180",9],[-5.6,"180-240",24],[-5.6,"240-300",8], + [-5.5,"0-30",0],[-5.5,"30-60",0],[-5.5,"60-120",1],[-5.5,"120-180",5],[-5.5,"180-240",19],[-5.5,"240-300",12], + [-5.4,"0-30",27],[-5.4,"30-60",0],[-5.4,"60-120",5],[-5.4,"120-180",13],[-5.4,"180-240",7],[-5.4,"240-300",18], + [-5.3,"0-30",100],[-5.3,"30-60",0],[-5.3,"60-120",1],[-5.3,"120-180",17],[-5.3,"180-240",20],[-5.3,"240-300",19], + [-5.2,"0-30",64],[-5.2,"30-60",0],[-5.2,"60-120",9],[-5.2,"120-180",8],[-5.2,"180-240",19],[-5.2,"240-300",25], + [-5.1,"0-30",100],[-5.1,"30-60",0],[-5.1,"60-120",4],[-5.1,"120-180",7],[-5.1,"180-240",10],[-5.1,"240-300",33], + [-5.0,"0-30",0],[-5.0,"30-60",6],[-5.0,"60-120",2],[-5.0,"120-180",15],[-5.0,"180-240",7],[-5.0,"240-300",41], + [-4.9,"0-30",25],[-4.9,"30-60",22],[-4.9,"60-120",6],[-4.9,"120-180",9],[-4.9,"180-240",9],[-4.9,"240-300",26], + [-4.8,"0-30",0],[-4.8,"30-60",0],[-4.8,"60-120",3],[-4.8,"120-180",7],[-4.8,"180-240",12],[-4.8,"240-300",27], + [-4.7,"0-30",25],[-4.7,"30-60",17],[-4.7,"60-120",4],[-4.7,"120-180",14],[-4.7,"180-240",27],[-4.7,"240-300",27], + [-4.6,"0-30",11],[-4.6,"30-60",20],[-4.6,"60-120",10],[-4.6,"120-180",4],[-4.6,"180-240",23],[-4.6,"240-300",47], + [-4.5,"0-30",75],[-4.5,"30-60",5],[-4.5,"60-120",17],[-4.5,"120-180",7],[-4.5,"180-240",11],[-4.5,"240-300",19], + [-4.4,"0-30",0],[-4.4,"30-60",0],[-4.4,"60-120",12],[-4.4,"120-180",12],[-4.4,"180-240",22],[-4.4,"240-300",56], + [-4.3,"0-30",86],[-4.3,"30-60",22],[-4.3,"60-120",18],[-4.3,"120-180",10],[-4.3,"180-240",15],[-4.3,"240-300",37], + [-4.2,"0-30",0],[-4.2,"30-60",21],[-4.2,"60-120",8],[-4.2,"120-180",16],[-4.2,"180-240",15],[-4.2,"240-300",32], + [-4.1,"0-30",100],[-4.1,"30-60",8],[-4.1,"60-120",8],[-4.1,"120-180",15],[-4.1,"180-240",19],[-4.1,"240-300",38], + [-4.0,"0-30",56],[-4.0,"30-60",20],[-4.0,"60-120",7],[-4.0,"120-180",12],[-4.0,"180-240",14],[-4.0,"240-300",48], + [-3.9,"0-30",5],[-3.9,"30-60",13],[-3.9,"60-120",9],[-3.9,"120-180",7],[-3.9,"180-240",14],[-3.9,"240-300",37], + [-3.8,"0-30",41],[-3.8,"30-60",10],[-3.8,"60-120",7],[-3.8,"120-180",14],[-3.8,"180-240",12],[-3.8,"240-300",45], + [-3.7,"0-30",67],[-3.7,"30-60",17],[-3.7,"60-120",6],[-3.7,"120-180",8],[-3.7,"180-240",15],[-3.7,"240-300",40], + [-3.6,"0-30",18],[-3.6,"30-60",1],[-3.6,"60-120",7],[-3.6,"120-180",12],[-3.6,"180-240",8],[-3.6,"240-300",30], + [-3.5,"0-30",14],[-3.5,"30-60",20],[-3.5,"60-120",3],[-3.5,"120-180",18],[-3.5,"180-240",15],[-3.5,"240-300",26], + [-3.4,"0-30",44],[-3.4,"30-60",3],[-3.4,"60-120",6],[-3.4,"120-180",14],[-3.4,"180-240",15],[-3.4,"240-300",26], + [-3.3,"0-30",13],[-3.3,"30-60",11],[-3.3,"60-120",8],[-3.3,"120-180",14],[-3.3,"180-240",11],[-3.3,"240-300",27], + [-3.2,"0-30",13],[-3.2,"30-60",0],[-3.2,"60-120",9],[-3.2,"120-180",12],[-3.2,"180-240",16],[-3.2,"240-300",26], + [-3.1,"0-30",31],[-3.1,"30-60",1],[-3.1,"60-120",7],[-3.1,"120-180",13],[-3.1,"180-240",18],[-3.1,"240-300",32], + [-3.0,"0-30",30],[-3.0,"30-60",11],[-3.0,"60-120",13],[-3.0,"120-180",8],[-3.0,"180-240",23],[-3.0,"240-300",29], + [-2.9,"0-30",7],[-2.9,"30-60",6],[-2.9,"60-120",8],[-2.9,"120-180",10],[-2.9,"180-240",12],[-2.9,"240-300",22], + [-2.8,"0-30",19],[-2.8,"30-60",6],[-2.8,"60-120",10],[-2.8,"120-180",21],[-2.8,"180-240",23],[-2.8,"240-300",35], + [-2.7,"0-30",29],[-2.7,"30-60",16],[-2.7,"60-120",11],[-2.7,"120-180",20],[-2.7,"180-240",17],[-2.7,"240-300",46], + [-2.6,"0-30",21],[-2.6,"30-60",19],[-2.6,"60-120",11],[-2.6,"120-180",14],[-2.6,"180-240",15],[-2.6,"240-300",37], + [-2.5,"0-30",5],[-2.5,"30-60",5],[-2.5,"60-120",15],[-2.5,"120-180",19],[-2.5,"180-240",18],[-2.5,"240-300",35], + [-2.4,"0-30",16],[-2.4,"30-60",16],[-2.4,"60-120",12],[-2.4,"120-180",15],[-2.4,"180-240",29],[-2.4,"240-300",39], + [-2.3,"0-30",6],[-2.3,"30-60",14],[-2.3,"60-120",14],[-2.3,"120-180",13],[-2.3,"180-240",21],[-2.3,"240-300",42], + [-2.2,"0-30",30],[-2.2,"30-60",11],[-2.2,"60-120",23],[-2.2,"120-180",25],[-2.2,"180-240",24],[-2.2,"240-300",32], + [-2.1,"0-30",8],[-2.1,"30-60",9],[-2.1,"60-120",14],[-2.1,"120-180",21],[-2.1,"180-240",25],[-2.1,"240-300",37], + [-2.0,"0-30",12],[-2.0,"30-60",9],[-2.0,"60-120",20],[-2.0,"120-180",23],[-2.0,"180-240",28],[-2.0,"240-300",29], + [-1.9,"0-30",6],[-1.9,"30-60",13],[-1.9,"60-120",25],[-1.9,"120-180",23],[-1.9,"180-240",30],[-1.9,"240-300",32], + [-1.8,"0-30",15],[-1.8,"30-60",16],[-1.8,"60-120",18],[-1.8,"120-180",26],[-1.8,"180-240",27],[-1.8,"240-300",36], + [-1.7,"0-30",18],[-1.7,"30-60",13],[-1.7,"60-120",22],[-1.7,"120-180",27],[-1.7,"180-240",34],[-1.7,"240-300",30], + [-1.6,"0-30",12],[-1.6,"30-60",14],[-1.6,"60-120",23],[-1.6,"120-180",26],[-1.6,"180-240",28],[-1.6,"240-300",35], + [-1.5,"0-30",19],[-1.5,"30-60",16],[-1.5,"60-120",21],[-1.5,"120-180",30],[-1.5,"180-240",30],[-1.5,"240-300",38], + [-1.4,"0-30",24],[-1.4,"30-60",23],[-1.4,"60-120",20],[-1.4,"120-180",25],[-1.4,"180-240",29],[-1.4,"240-300",34], + [-1.3,"0-30",21],[-1.3,"30-60",17],[-1.3,"60-120",25],[-1.3,"120-180",29],[-1.3,"180-240",35],[-1.3,"240-300",44], + [-1.2,"0-30",21],[-1.2,"30-60",18],[-1.2,"60-120",24],[-1.2,"120-180",23],[-1.2,"180-240",36],[-1.2,"240-300",39], + [-1.1,"0-30",16],[-1.1,"30-60",18],[-1.1,"60-120",21],[-1.1,"120-180",31],[-1.1,"180-240",42],[-1.1,"240-300",42], + [-1.0,"0-30",16],[-1.0,"30-60",22],[-1.0,"60-120",24],[-1.0,"120-180",32],[-1.0,"180-240",37],[-1.0,"240-300",41], + [-0.9,"0-30",15],[-0.9,"30-60",20],[-0.9,"60-120",29],[-0.9,"120-180",36],[-0.9,"180-240",40],[-0.9,"240-300",45], + [-0.8,"0-30",19],[-0.8,"30-60",21],[-0.8,"60-120",31],[-0.8,"120-180",33],[-0.8,"180-240",41],[-0.8,"240-300",43], + [-0.7,"0-30",20],[-0.7,"30-60",27],[-0.7,"60-120",36],[-0.7,"120-180",38],[-0.7,"180-240",42],[-0.7,"240-300",46], + [-0.6,"0-30",15],[-0.6,"30-60",25],[-0.6,"60-120",36],[-0.6,"120-180",40],[-0.6,"180-240",41],[-0.6,"240-300",46], + [-0.5,"0-30",21],[-0.5,"30-60",27],[-0.5,"60-120",38],[-0.5,"120-180",40],[-0.5,"180-240",45],[-0.5,"240-300",45], + [-0.4,"0-30",27],[-0.4,"30-60",38],[-0.4,"60-120",41],[-0.4,"120-180",42],[-0.4,"180-240",39],[-0.4,"240-300",45], + [-0.3,"0-30",30],[-0.3,"30-60",38],[-0.3,"60-120",43],[-0.3,"120-180",46],[-0.3,"180-240",49],[-0.3,"240-300",47], + [-0.2,"0-30",36],[-0.2,"30-60",47],[-0.2,"60-120",48],[-0.2,"120-180",47],[-0.2,"180-240",48],[-0.2,"240-300",52], + [-0.1,"0-30",47],[-0.1,"30-60",47],[-0.1,"60-120",49],[-0.1,"120-180",52],[-0.1,"180-240",54],[-0.1,"240-300",55], + [0.0,"0-30",56],[0.0,"30-60",54],[0.0,"60-120",55],[0.0,"120-180",56],[0.0,"180-240",56],[0.0,"240-300",56], + [0.1,"0-30",71],[0.1,"30-60",63],[0.1,"60-120",59],[0.1,"120-180",60],[0.1,"180-240",60],[0.1,"240-300",57], + [0.2,"0-30",73],[0.2,"30-60",74],[0.2,"60-120",65],[0.2,"120-180",66],[0.2,"180-240",61],[0.2,"240-300",57], + [0.3,"0-30",88],[0.3,"30-60",75],[0.3,"60-120",66],[0.3,"120-180",68],[0.3,"180-240",64],[0.3,"240-300",58], + [0.4,"0-30",87],[0.4,"30-60",75],[0.4,"60-120",72],[0.4,"120-180",67],[0.4,"180-240",62],[0.4,"240-300",61], + [0.5,"0-30",90],[0.5,"30-60",76],[0.5,"60-120",70],[0.5,"120-180",65],[0.5,"180-240",69],[0.5,"240-300",59], + [0.6,"0-30",87],[0.6,"30-60",83],[0.6,"60-120",77],[0.6,"120-180",70],[0.6,"180-240",67],[0.6,"240-300",64], + [0.7,"0-30",88],[0.7,"30-60",81],[0.7,"60-120",78],[0.7,"120-180",75],[0.7,"180-240",68],[0.7,"240-300",64], + [0.8,"0-30",94],[0.8,"30-60",86],[0.8,"60-120",80],[0.8,"120-180",73],[0.8,"180-240",71],[0.8,"240-300",64], + [0.9,"0-30",93],[0.9,"30-60",81],[0.9,"60-120",82],[0.9,"120-180",77],[0.9,"180-240",70],[0.9,"240-300",67], + [1.0,"0-30",94],[1.0,"30-60",88],[1.0,"60-120",84],[1.0,"120-180",77],[1.0,"180-240",72],[1.0,"240-300",64], + [1.1,"0-30",93],[1.1,"30-60",94],[1.1,"60-120",82],[1.1,"120-180",78],[1.1,"180-240",72],[1.1,"240-300",68], + [1.2,"0-30",96],[1.2,"30-60",90],[1.2,"60-120",78],[1.2,"120-180",82],[1.2,"180-240",71],[1.2,"240-300",68], + [1.3,"0-30",99],[1.3,"30-60",94],[1.3,"60-120",85],[1.3,"120-180",78],[1.3,"180-240",74],[1.3,"240-300",67], + [1.4,"0-30",97],[1.4,"30-60",92],[1.4,"60-120",86],[1.4,"120-180",80],[1.4,"180-240",73],[1.4,"240-300",74], + [1.5,"0-30",99],[1.5,"30-60",98],[1.5,"60-120",91],[1.5,"120-180",81],[1.5,"180-240",77],[1.5,"240-300",74], + [1.6,"0-30",99],[1.6,"30-60",96],[1.6,"60-120",87],[1.6,"120-180",81],[1.6,"180-240",76],[1.6,"240-300",69], + [1.7,"0-30",99],[1.7,"30-60",96],[1.7,"60-120",92],[1.7,"120-180",82],[1.7,"180-240",75],[1.7,"240-300",74], + [1.8,"0-30",97],[1.8,"30-60",97],[1.8,"60-120",93],[1.8,"120-180",80],[1.8,"180-240",79],[1.8,"240-300",76], + [1.9,"0-30",99],[1.9,"30-60",97],[1.9,"60-120",89],[1.9,"120-180",81],[1.9,"180-240",76],[1.9,"240-300",75], + [2.0,"0-30",95],[2.0,"30-60",93],[2.0,"60-120",93],[2.0,"120-180",82],[2.0,"180-240",73],[2.0,"240-300",77], + [2.1,"0-30",91],[2.1,"30-60",93],[2.1,"60-120",95],[2.1,"120-180",84],[2.1,"180-240",85],[2.1,"240-300",75], + [2.2,"0-30",99],[2.2,"30-60",96],[2.2,"60-120",93],[2.2,"120-180",83],[2.2,"180-240",79],[2.2,"240-300",82], + [2.3,"0-30",96],[2.3,"30-60",95],[2.3,"60-120",87],[2.3,"120-180",89],[2.3,"180-240",81],[2.3,"240-300",72], + [2.4,"0-30",97],[2.4,"30-60",93],[2.4,"60-120",91],[2.4,"120-180",84],[2.4,"180-240",84],[2.4,"240-300",79], + [2.5,"0-30",100],[2.5,"30-60",95],[2.5,"60-120",92],[2.5,"120-180",84],[2.5,"180-240",84],[2.5,"240-300",82], + [2.6,"0-30",100],[2.6,"30-60",96],[2.6,"60-120",93],[2.6,"120-180",87],[2.6,"180-240",83],[2.6,"240-300",73], + [2.7,"0-30",99],[2.7,"30-60",97],[2.7,"60-120",98],[2.7,"120-180",89],[2.7,"180-240",84],[2.7,"240-300",72], + [2.8,"0-30",99],[2.8,"30-60",98],[2.8,"60-120",98],[2.8,"120-180",94],[2.8,"180-240",82],[2.8,"240-300",81], + [2.9,"0-30",100],[2.9,"30-60",100],[2.9,"60-120",98],[2.9,"120-180",92],[2.9,"180-240",84],[2.9,"240-300",84], + [3.0,"0-30",100],[3.0,"30-60",98],[3.0,"60-120",96],[3.0,"120-180",95],[3.0,"180-240",87],[3.0,"240-300",83], + [3.1,"0-30",96],[3.1,"30-60",95],[3.1,"60-120",96],[3.1,"120-180",94],[3.1,"180-240",89],[3.1,"240-300",85], + [3.2,"0-30",97],[3.2,"30-60",99],[3.2,"60-120",87],[3.2,"120-180",93],[3.2,"180-240",89],[3.2,"240-300",85], + [3.3,"0-30",97],[3.3,"30-60",93],[3.3,"60-120",95],[3.3,"120-180",96],[3.3,"180-240",86],[3.3,"240-300",85], + [3.4,"0-30",98],[3.4,"30-60",96],[3.4,"60-120",95],[3.4,"120-180",97],[3.4,"180-240",93],[3.4,"240-300",90], + [3.5,"0-30",100],[3.5,"30-60",100],[3.5,"60-120",99],[3.5,"120-180",92],[3.5,"180-240",87],[3.5,"240-300",95], + [3.6,"0-30",100],[3.6,"30-60",100],[3.6,"60-120",96],[3.6,"120-180",90],[3.6,"180-240",89],[3.6,"240-300",90], + [3.7,"0-30",100],[3.7,"30-60",99],[3.7,"60-120",97],[3.7,"120-180",94],[3.7,"180-240",88],[3.7,"240-300",86], + [3.8,"0-30",98],[3.8,"30-60",99],[3.8,"60-120",97],[3.8,"120-180",87],[3.8,"180-240",89],[3.8,"240-300",90], + [3.9,"0-30",98],[3.9,"30-60",97],[3.9,"60-120",96],[3.9,"120-180",95],[3.9,"180-240",86],[3.9,"240-300",95], + [4.0,"0-30",95],[4.0,"30-60",94],[4.0,"60-120",99],[4.0,"120-180",88],[4.0,"180-240",83],[4.0,"240-300",85], + [4.1,"0-30",90],[4.1,"30-60",94],[4.1,"60-120",93],[4.1,"120-180",93],[4.1,"180-240",75],[4.1,"240-300",59], + [4.2,"0-30",99],[4.2,"30-60",99],[4.2,"60-120",94],[4.2,"120-180",95],[4.2,"180-240",90],[4.2,"240-300",73], + [4.3,"0-30",96],[4.3,"30-60",98],[4.3,"60-120",94],[4.3,"120-180",95],[4.3,"180-240",95],[4.3,"240-300",79], + [4.4,"0-30",95],[4.4,"30-60",99],[4.4,"60-120",94],[4.4,"120-180",91],[4.4,"180-240",97],[4.4,"240-300",79], + [4.5,"0-30",97],[4.5,"30-60",97],[4.5,"60-120",99],[4.5,"120-180",98],[4.5,"180-240",90],[4.5,"240-300",83], + [4.6,"0-30",95],[4.6,"30-60",95],[4.6,"60-120",97],[4.6,"120-180",94],[4.6,"180-240",87],[4.6,"240-300",74], + [4.7,"0-30",99],[4.7,"30-60",98],[4.7,"60-120",94],[4.7,"120-180",94],[4.7,"180-240",92],[4.7,"240-300",72], + [4.8,"0-30",100],[4.8,"30-60",100],[4.8,"60-120",98],[4.8,"120-180",91],[4.8,"180-240",90],[4.8,"240-300",96], + [4.9,"0-30",96],[4.9,"30-60",100],[4.9,"60-120",94],[4.9,"120-180",93],[4.9,"180-240",82],[4.9,"240-300",100], + [5.0,"0-30",92],[5.0,"30-60",100],[5.0,"60-120",98],[5.0,"120-180",94],[5.0,"180-240",94],[5.0,"240-300",74], + [5.1,"0-30",100],[5.1,"30-60",97],[5.1,"60-120",99],[5.1,"120-180",81],[5.1,"180-240",79],[5.1,"240-300",55], + [5.2,"0-30",100],[5.2,"30-60",98],[5.2,"60-120",95],[5.2,"120-180",89],[5.2,"180-240",84],[5.2,"240-300",77], + [5.3,"0-30",100],[5.3,"30-60",97],[5.3,"60-120",86],[5.3,"120-180",94],[5.3,"180-240",95],[5.3,"240-300",81], + [5.4,"0-30",100],[5.4,"30-60",96],[5.4,"60-120",80],[5.4,"120-180",98],[5.4,"180-240",89],[5.4,"240-300",100], + [5.5,"0-30",100],[5.5,"30-60",98],[5.5,"60-120",93],[5.5,"120-180",99],[5.5,"180-240",87],[5.5,"240-300",91], + [5.6,"0-30",83],[5.6,"30-60",94],[5.6,"60-120",98],[5.6,"120-180",98],[5.6,"180-240",97],[5.6,"240-300",100], + [5.7,"0-30",100],[5.7,"30-60",95],[5.7,"60-120",98],[5.7,"120-180",100],[5.7,"180-240",100],[5.7,"240-300",86], + [5.8,"0-30",98],[5.8,"30-60",89],[5.8,"60-120",99],[5.8,"120-180",98],[5.8,"180-240",100],[5.8,"240-300",79], + [5.9,"0-30",100],[5.9,"30-60",88],[5.9,"60-120",98],[5.9,"120-180",100],[5.9,"180-240",100],[5.9,"240-300",88], + [6.0,"0-30",96],[6.0,"30-60",89],[6.0,"60-120",96],[6.0,"120-180",90],[6.0,"180-240",98],[6.0,"240-300",89], + [6.1,"0-30",98],[6.1,"30-60",97],[6.1,"60-120",97],[6.1,"120-180",93],[6.1,"180-240",100],[6.1,"240-300",92], + [6.2,"0-30",99],[6.2,"30-60",92],[6.2,"60-120",91],[6.2,"120-180",90],[6.2,"180-240",99],[6.2,"240-300",86], + [6.3,"0-30",100],[6.3,"30-60",90],[6.3,"60-120",94],[6.3,"120-180",100],[6.3,"180-240",100],[6.3,"240-300",94], + [6.4,"0-30",91],[6.4,"30-60",98],[6.4,"60-120",97],[6.4,"120-180",93],[6.4,"180-240",96],[6.4,"240-300",80], + [6.5,"0-30",95],[6.5,"30-60",99],[6.5,"60-120",93],[6.5,"120-180",98],[6.5,"180-240",96],[6.5,"240-300",88], + [6.6,"0-30",99],[6.6,"30-60",99],[6.6,"60-120",98],[6.6,"120-180",100],[6.6,"180-240",100],[6.6,"240-300",92], + [6.7,"0-30",95],[6.7,"30-60",98],[6.7,"60-120",97],[6.7,"120-180",100],[6.7,"180-240",100],[6.7,"240-300",55], + [6.8,"0-30",96],[6.8,"30-60",100],[6.8,"60-120",100],[6.8,"120-180",98],[6.8,"180-240",92],[6.8,"240-300",100], + [6.9,"0-30",95],[6.9,"30-60",97],[6.9,"60-120",100],[6.9,"120-180",100],[6.9,"180-240",100],[6.9,"240-300",100], + [7.0,"0-30",98],[7.0,"30-60",98],[7.0,"60-120",100],[7.0,"120-180",99],[7.0,"180-240",97],[7.0,"240-300",81], +]; + +// SOL 5m table (based on 13 days of ~950K tick statistics; step=0.01) +// Bucket design: step 0.01, range ±0.4 (covers 99% of the diff distribution) +const SOL_5M: RawData = [ + [-0.4,"0-30",100],[-0.4,"30-60",0],[-0.4,"60-120",0],[-0.4,"120-180",7],[-0.4,"180-240",22],[-0.4,"240-300",0], + [-0.39,"0-30",100],[-0.39,"30-60",30],[-0.39,"60-120",0],[-0.39,"120-180",0],[-0.39,"180-240",50],[-0.39,"240-300",0], + [-0.38,"0-30",100],[-0.38,"30-60",51],[-0.38,"60-120",0],[-0.38,"120-180",17],[-0.38,"180-240",71],[-0.38,"240-300",71], + [-0.37,"0-30",37],[-0.37,"30-60",37],[-0.37,"60-120",0],[-0.37,"120-180",21],[-0.37,"180-240",43],[-0.37,"240-300",87], + [-0.36,"0-30",100],[-0.36,"30-60",41],[-0.36,"60-120",0],[-0.36,"120-180",0],[-0.36,"180-240",30],[-0.36,"240-300",75], + [-0.35,"0-30",100],[-0.35,"30-60",100],[-0.35,"60-120",8],[-0.35,"120-180",21],[-0.35,"180-240",59],[-0.35,"240-300",83], + [-0.34,"0-30",88],[-0.34,"30-60",100],[-0.34,"60-120",0],[-0.34,"120-180",28],[-0.34,"180-240",16],[-0.34,"240-300",60], + [-0.33,"0-30",50],[-0.33,"30-60",67],[-0.33,"60-120",2],[-0.33,"120-180",34],[-0.33,"180-240",12],[-0.33,"240-300",11], + [-0.32,"0-30",17],[-0.32,"30-60",38],[-0.32,"60-120",2],[-0.32,"120-180",12],[-0.32,"180-240",17],[-0.32,"240-300",43], + [-0.31,"0-30",38],[-0.31,"30-60",61],[-0.31,"60-120",2],[-0.31,"120-180",19],[-0.31,"180-240",43],[-0.31,"240-300",100], + [-0.3,"0-30",33],[-0.3,"30-60",20],[-0.3,"60-120",2],[-0.3,"120-180",12],[-0.3,"180-240",32],[-0.3,"240-300",75], + [-0.29,"0-30",0],[-0.29,"30-60",18],[-0.29,"60-120",14],[-0.29,"120-180",12],[-0.29,"180-240",36],[-0.29,"240-300",17], + [-0.28,"0-30",50],[-0.28,"30-60",6],[-0.28,"60-120",23],[-0.28,"120-180",17],[-0.28,"180-240",41],[-0.28,"240-300",11], + [-0.27,"0-30",23],[-0.27,"30-60",0],[-0.27,"60-120",26],[-0.27,"120-180",21],[-0.27,"180-240",31],[-0.27,"240-300",10], + [-0.26,"0-30",0],[-0.26,"30-60",0],[-0.26,"60-120",11],[-0.26,"120-180",18],[-0.26,"180-240",30],[-0.26,"240-300",9], + [-0.25,"0-30",0],[-0.25,"30-60",0],[-0.25,"60-120",16],[-0.25,"120-180",16],[-0.25,"180-240",27],[-0.25,"240-300",31], + [-0.24,"0-30",0],[-0.24,"30-60",4],[-0.24,"60-120",19],[-0.24,"120-180",19],[-0.24,"180-240",16],[-0.24,"240-300",29], + [-0.23,"0-30",0],[-0.23,"30-60",0],[-0.23,"60-120",16],[-0.23,"120-180",14],[-0.23,"180-240",18],[-0.23,"240-300",18], + [-0.22,"0-30",30],[-0.22,"30-60",0],[-0.22,"60-120",9],[-0.22,"120-180",14],[-0.22,"180-240",22],[-0.22,"240-300",27], + [-0.21,"0-30",32],[-0.21,"30-60",31],[-0.21,"60-120",14],[-0.21,"120-180",14],[-0.21,"180-240",17],[-0.21,"240-300",38], + [-0.2,"0-30",31],[-0.2,"30-60",34],[-0.2,"60-120",8],[-0.2,"120-180",9],[-0.2,"180-240",10],[-0.2,"240-300",61], + [-0.19,"0-30",23],[-0.19,"30-60",10],[-0.19,"60-120",5],[-0.19,"120-180",8],[-0.19,"180-240",15],[-0.19,"240-300",49], + [-0.18,"0-30",12],[-0.18,"30-60",23],[-0.18,"60-120",4],[-0.18,"120-180",8],[-0.18,"180-240",14],[-0.18,"240-300",50], + [-0.17,"0-30",25],[-0.17,"30-60",1],[-0.17,"60-120",7],[-0.17,"120-180",8],[-0.17,"180-240",9],[-0.17,"240-300",39], + [-0.16,"0-30",13],[-0.16,"30-60",0],[-0.16,"60-120",3],[-0.16,"120-180",7],[-0.16,"180-240",15],[-0.16,"240-300",40], + [-0.15,"0-30",16],[-0.15,"30-60",2],[-0.15,"60-120",8],[-0.15,"120-180",7],[-0.15,"180-240",18],[-0.15,"240-300",33], + [-0.14,"0-30",14],[-0.14,"30-60",6],[-0.14,"60-120",12],[-0.14,"120-180",11],[-0.14,"180-240",17],[-0.14,"240-300",30], + [-0.13,"0-30",14],[-0.13,"30-60",12],[-0.13,"60-120",8],[-0.13,"120-180",17],[-0.13,"180-240",19],[-0.13,"240-300",24], + [-0.12,"0-30",16],[-0.12,"30-60",13],[-0.12,"60-120",9],[-0.12,"120-180",16],[-0.12,"180-240",26],[-0.12,"240-300",25], + [-0.11,"0-30",19],[-0.11,"30-60",8],[-0.11,"60-120",13],[-0.11,"120-180",23],[-0.11,"180-240",25],[-0.11,"240-300",29], + [-0.1,"0-30",18],[-0.1,"30-60",14],[-0.1,"60-120",13],[-0.1,"120-180",26],[-0.1,"180-240",25],[-0.1,"240-300",29], + [-0.09,"0-30",19],[-0.09,"30-60",15],[-0.09,"60-120",15],[-0.09,"120-180",27],[-0.09,"180-240",27],[-0.09,"240-300",23], + [-0.08,"0-30",21],[-0.08,"30-60",11],[-0.08,"60-120",20],[-0.08,"120-180",26],[-0.08,"180-240",27],[-0.08,"240-300",30], + [-0.07,"0-30",21],[-0.07,"30-60",19],[-0.07,"60-120",24],[-0.07,"120-180",27],[-0.07,"180-240",30],[-0.07,"240-300",35], + [-0.06,"0-30",21],[-0.06,"30-60",25],[-0.06,"60-120",27],[-0.06,"120-180",30],[-0.06,"180-240",34],[-0.06,"240-300",39], + [-0.05,"0-30",20],[-0.05,"30-60",20],[-0.05,"60-120",27],[-0.05,"120-180",29],[-0.05,"180-240",33],[-0.05,"240-300",38], + [-0.04,"0-30",22],[-0.04,"30-60",22],[-0.04,"60-120",30],[-0.04,"120-180",36],[-0.04,"180-240",39],[-0.04,"240-300",43], + [-0.03,"0-30",23],[-0.03,"30-60",28],[-0.03,"60-120",34],[-0.03,"120-180",41],[-0.03,"180-240",47],[-0.03,"240-300",48], + [-0.02,"0-30",29],[-0.02,"30-60",35],[-0.02,"60-120",39],[-0.02,"120-180",44],[-0.02,"180-240",48],[-0.02,"240-300",51], + [-0.01,"0-30",38],[-0.01,"30-60",46],[-0.01,"60-120",48],[-0.01,"120-180",51],[-0.01,"180-240",53],[-0.01,"240-300",56], + [0.0,"0-30",58],[0.0,"30-60",56],[0.0,"60-120",59],[0.0,"120-180",57],[0.0,"180-240",57],[0.0,"240-300",57], + [0.01,"0-30",74],[0.01,"30-60",69],[0.01,"60-120",67],[0.01,"120-180",65],[0.01,"180-240",63],[0.01,"240-300",59], + [0.02,"0-30",85],[0.02,"30-60",79],[0.02,"60-120",75],[0.02,"120-180",68],[0.02,"180-240",67],[0.02,"240-300",64], + [0.03,"0-30",91],[0.03,"30-60",85],[0.03,"60-120",79],[0.03,"120-180",75],[0.03,"180-240",70],[0.03,"240-300",67], + [0.04,"0-30",95],[0.04,"30-60",91],[0.04,"60-120",86],[0.04,"120-180",79],[0.04,"180-240",71],[0.04,"240-300",69], + [0.05,"0-30",96],[0.05,"30-60",90],[0.05,"60-120",86],[0.05,"120-180",79],[0.05,"180-240",74],[0.05,"240-300",70], + [0.06,"0-30",98],[0.06,"30-60",94],[0.06,"60-120",86],[0.06,"120-180",80],[0.06,"180-240",77],[0.06,"240-300",72], + [0.07,"0-30",96],[0.07,"30-60",93],[0.07,"60-120",89],[0.07,"120-180",82],[0.07,"180-240",79],[0.07,"240-300",74], + [0.08,"0-30",97],[0.08,"30-60",95],[0.08,"60-120",90],[0.08,"120-180",84],[0.08,"180-240",77],[0.08,"240-300",75], + [0.09,"0-30",97],[0.09,"30-60",93],[0.09,"60-120",92],[0.09,"120-180",88],[0.09,"180-240",79],[0.09,"240-300",76], + [0.1,"0-30",98],[0.1,"30-60",95],[0.1,"60-120",92],[0.1,"120-180",90],[0.1,"180-240",80],[0.1,"240-300",82], + [0.11,"0-30",98],[0.11,"30-60",97],[0.11,"60-120",94],[0.11,"120-180",89],[0.11,"180-240",85],[0.11,"240-300",87], + [0.12,"0-30",99],[0.12,"30-60",98],[0.12,"60-120",95],[0.12,"120-180",91],[0.12,"180-240",82],[0.12,"240-300",82], + [0.13,"0-30",99],[0.13,"30-60",96],[0.13,"60-120",98],[0.13,"120-180",89],[0.13,"180-240",87],[0.13,"240-300",89], + [0.14,"0-30",97],[0.14,"30-60",97],[0.14,"60-120",96],[0.14,"120-180",93],[0.14,"180-240",86],[0.14,"240-300",89], + [0.15,"0-30",99],[0.15,"30-60",97],[0.15,"60-120",93],[0.15,"120-180",92],[0.15,"180-240",89],[0.15,"240-300",79], + [0.16,"0-30",98],[0.16,"30-60",95],[0.16,"60-120",93],[0.16,"120-180",93],[0.16,"180-240",92],[0.16,"240-300",85], + [0.17,"0-30",100],[0.17,"30-60",98],[0.17,"60-120",96],[0.17,"120-180",93],[0.17,"180-240",96],[0.17,"240-300",74], + [0.18,"0-30",100],[0.18,"30-60",98],[0.18,"60-120",99],[0.18,"120-180",95],[0.18,"180-240",97],[0.18,"240-300",71], + [0.19,"0-30",100],[0.19,"30-60",97],[0.19,"60-120",98],[0.19,"120-180",98],[0.19,"180-240",94],[0.19,"240-300",58], + [0.2,"0-30",100],[0.2,"30-60",97],[0.2,"60-120",98],[0.2,"120-180",93],[0.2,"180-240",90],[0.2,"240-300",77], + [0.21,"0-30",98],[0.21,"30-60",98],[0.21,"60-120",97],[0.21,"120-180",95],[0.21,"180-240",81],[0.21,"240-300",77], + [0.22,"0-30",95],[0.22,"30-60",99],[0.22,"60-120",96],[0.22,"120-180",96],[0.22,"180-240",88],[0.22,"240-300",61], + [0.23,"0-30",97],[0.23,"30-60",100],[0.23,"60-120",100],[0.23,"120-180",100],[0.23,"180-240",93],[0.23,"240-300",65], + [0.24,"0-30",98],[0.24,"30-60",98],[0.24,"60-120",100],[0.24,"120-180",100],[0.24,"180-240",95],[0.24,"240-300",83], + [0.25,"0-30",95],[0.25,"30-60",96],[0.25,"60-120",100],[0.25,"120-180",99],[0.25,"180-240",98],[0.25,"240-300",89], + [0.26,"0-30",97],[0.26,"30-60",99],[0.26,"60-120",100],[0.26,"120-180",97],[0.26,"180-240",99],[0.26,"240-300",100], + [0.27,"0-30",100],[0.27,"30-60",99],[0.27,"60-120",100],[0.27,"120-180",97],[0.27,"180-240",100],[0.27,"240-300",96], + [0.28,"0-30",100],[0.28,"30-60",100],[0.28,"60-120",100],[0.28,"120-180",100],[0.28,"180-240",100],[0.28,"240-300",90], + [0.29,"0-30",100],[0.29,"30-60",100],[0.29,"60-120",100],[0.29,"120-180",100],[0.29,"180-240",100],[0.29,"240-300",100], + [0.3,"0-30",100],[0.3,"30-60",100],[0.3,"60-120",100],[0.3,"120-180",100],[0.3,"180-240",100],[0.3,"240-300",100], + [0.31,"0-30",100],[0.31,"30-60",100],[0.31,"60-120",100],[0.31,"120-180",100],[0.31,"180-240",100],[0.31,"240-300",86], + [0.32,"0-30",100],[0.32,"30-60",100],[0.32,"60-120",100],[0.32,"120-180",100],[0.32,"180-240",100],[0.32,"240-300",89], + [0.33,"0-30",100],[0.33,"30-60",100],[0.33,"60-120",100],[0.33,"120-180",100],[0.33,"180-240",100],[0.33,"240-300",100], + [0.34,"0-30",100],[0.34,"30-60",100],[0.34,"60-120",100],[0.34,"120-180",100],[0.34,"180-240",100],[0.34,"240-300",82], + [0.35,"0-30",100],[0.35,"30-60",100],[0.35,"60-120",100],[0.35,"120-180",100],[0.35,"180-240",100],[0.35,"240-300",77], + [0.36,"0-30",100],[0.36,"30-60",100],[0.36,"60-120",100],[0.36,"120-180",100],[0.36,"180-240",100],[0.36,"240-300",33], + [0.37,"0-30",100],[0.37,"30-60",100],[0.37,"60-120",100],[0.37,"120-180",100],[0.37,"180-240",100],[0.37,"240-300",50], + [0.38,"0-30",100],[0.38,"30-60",100],[0.38,"60-120",100],[0.38,"120-180",100],[0.38,"180-240",100],[0.38,"240-300",100], + [0.39,"0-30",100],[0.39,"30-60",100],[0.39,"60-120",100],[0.39,"120-180",100],[0.39,"180-240",100],[0.39,"240-300",100], + [0.4,"0-30",100],[0.4,"30-60",100],[0.4,"60-120",100],[0.4,"120-180",100],[0.4,"180-240",100],[0.4,"240-300",80], +]; + +// Fair probability tables per market — to add a new market, just add an entry here +export const FAIR_PROB_TABLES: Record = { + "btc-5m": BTC_5M, + "eth-5m": ETH_5M, + // "btc-15m": BTC_15M, // to be calibrated + "sol-5m": SOL_5M, +}; + +// Bucket design per market (diff step + range) —— ETH/SOL price magnitudes are far smaller than BTC and need finer buckets +interface BucketSpec { step: number; min: number; max: number; } +const BUCKET_SPECS: Record = { + "btc-5m": { step: 2, min: -140, max: 96 }, + "eth-5m": { step: 0.1, min: -7, max: 7 }, + "sol-5m": { step: 0.01, min: -0.4, max: 0.4 }, +}; + +// Precompile each table into a Map for O(1) lookup +const COMPILED_MAPS = new Map>(); +for (const [marketKey, raw] of Object.entries(FAIR_PROB_TABLES)) { + const m = new Map(); + for (const [diff, rb, prob] of raw) m.set(`${diff},${rb}`, prob); + COMPILED_MAPS.set(marketKey, m); +} + +export function getRemBin(rem: number): string | null { + // rem ≥ 240 all map to the 240-300 bin (handles rem = 300 or transient values right after a window switch) + // Note: all current tables are the 6 bins of the 5m period; adding a 15m table in the future requires extending this function + if (rem >= 240) return "240-300"; + if (rem >= 180) return "180-240"; + if (rem >= 120) return "120-180"; + if (rem >= 60) return "60-120"; + if (rem >= 30) return "30-60"; + if (rem >= 0) return "0-30"; + return null; +} + +/** Look up the fair probability (%) by diff and rem; returns null when the current market has no table */ +export function getFairProb(diff: number, rem: number): number | null { + const map = COMPILED_MAPS.get(activeMarketKey); + if (!map) return null; + const spec = BUCKET_SPECS[activeMarketKey]; + if (!spec) return null; + const rb = getRemBin(rem); + if (!rb) return null; + // round + clamp per the current market's bucket spec + const rawDb = Math.round(diff / spec.step) * spec.step; + const dbClamped = Math.max(spec.min, Math.min(spec.max, rawDb)); + // precision matches the generation side's round(x, 2): integer steps use integers; decimal steps keep 2 places + // (0.5 multiples are also exact at 2 places: 1.0/1.5/2.0...; a future 0.05 step would also match correctly) + const db = spec.step >= 1 ? dbClamped : Math.round(dbClamped * 100) / 100; + return map.get(`${db},${rb}`) ?? null; +} + +/** Whether the current market has a fair-probability table (the frontend can use this to decide whether to enable p-series strategies) */ +export function hasFairProbTable(marketKey?: string): boolean { + return COMPILED_MAPS.has(marketKey ?? activeMarketKey); +} + +// Current active market key (synced by server.ts when switching markets) +let activeMarketKey = "btc-5m"; +export function setActiveMarket(sym: string, period: string): void { + activeMarketKey = `${sym}-${period}`; +} +// Backward compatibility for old calls +export function setActiveSymbol(sym: string): void { + activeMarketKey = `${sym}-5m`; +} diff --git a/strategies/_core/s6-core.ts b/strategies/_core/s6-core.ts new file mode 100644 index 0000000..6428d19 --- /dev/null +++ b/strategies/_core/s6-core.ts @@ -0,0 +1,320 @@ +/** + * Momentum strategy shared core — entry logic + factor computation + * + * Implemented strictly per the original strategy spec, shared by s6/s13: + * 1. Current candle direction + 5-bar 1-minute momentum direction must agree + * 2. 6-factor scoring (RSI 25% / volume 20% / price 20% / candle 15% / consecutive 15% / MA7 5%) + * 3. Bonus when the 5-minute trend agrees (strength * 0.1) + * 4. MA120 long-term trend filter + 5-minute trend filter + * 5. UP threshold 0.55 / DOWN threshold 0.60 + */ + +import type { Kline, StrategyTickContext, StrategyDirection } from "../types.js"; + +// ── Entry parameters ──────────────────────────────────────────────── +export const UP_THRESHOLD = 0.55; +export const DOWN_THRESHOLD = 0.60; +export const WINDOW_MIN_REMAINING = 30; // do not enter when the window's remaining seconds are below this value + +// ── Momentum parameters ──────────────────────────────────────────────── +export const MOMENTUM_BARS = 5; // use the latest 5 1-minute K-lines to judge momentum +export const MOMENTUM_THRESHOLD_PCT = 0.05; // a 0.05% move determines the momentum direction + +// ── K-line indicator computation functions ──────────────────────────── + +export function calcRSI(klines: readonly Kline[], period = 14): number | null { + if (klines.length < period + 1) return null; + let gain = 0, loss = 0; + const start = klines.length - period; + for (let i = start; i < klines.length; i++) { + const change = klines[i].close - klines[i - 1].close; + if (change >= 0) gain += change; else loss -= change; + } + if (loss === 0) return 100; + const rs = gain / loss; + return 100 - 100 / (1 + rs); +} + +export function calcMA(klines: readonly Kline[], period: number): number | null { + if (klines.length < period) return null; + let sum = 0; + for (let i = klines.length - period; i < klines.length; i++) { + sum += klines[i].close; + } + return sum / period; +} + +/** Overall momentum direction of the latest N K-lines */ +export function momentumDirection(klines: readonly Kline[], bars: number, thresholdPct: number): "up" | "down" | "neutral" { + if (klines.length < bars) return "neutral"; + const first = klines[klines.length - bars]; + const last = klines[klines.length - 1]; + const change = (last.close - first.open) / first.open * 100; + if (change > thresholdPct) return "up"; + if (change < -thresholdPct) return "down"; + return "neutral"; +} + +/** Current candle direction (strictly by bullish/bearish close, no body-ratio filter) */ +export function currentCandleDirection(klines: readonly Kline[]): "up" | "down" | "neutral" { + if (!klines.length) return "neutral"; + const k = klines[klines.length - 1]; + if (k.close > k.open) return "up"; + if (k.close < k.open) return "down"; + return "neutral"; +} + +/** Number of consecutive same-direction K-lines */ +export function consecutiveSameDirection(klines: readonly Kline[], dir: "up" | "down"): number { + let count = 0; + for (let i = klines.length - 1; i >= 0; i--) { + const k = klines[i]; + const kDir = k.close > k.open ? "up" : k.close < k.open ? "down" : "neutral"; + if (kDir === dir) count++; + else break; + } + return count; +} + +// ── Individual scoring functions ──────────────────────────── + +export function scoreConsecutive(count: number): number { + return Math.min(count / 3, 1.0); +} + +export function scoreRSI(rsi: number): number { + if (rsi > 70) return 1.0; + if (rsi > 60) return 0.8; + if (rsi > 50) return 0.6; + if (rsi > 40) return 0.4; + return 0.2; +} + +export function scoreVolume(klines: readonly Kline[]): number { + if (klines.length < 20) return 0; + let sum = 0; + for (let i = klines.length - 20; i < klines.length - 1; i++) { + sum += klines[i].volume; + } + const avg = sum / 19; + if (avg === 0) return 0; + const ratio = Math.max(0.1, Math.min(5.0, klines[klines.length - 1].volume / avg)); + if (ratio > 1) return Math.min(ratio / 2, 1.0); + return ratio * 0.5; +} + +export function scorePriceChange(klines: readonly Kline[], bars: number): number { + if (klines.length < bars + 1) return 0; + const from = klines[klines.length - 1 - bars].close; + const to = klines[klines.length - 1].close; + const changePct = Math.abs((to - from) / from * 100); + return Math.min(changePct / 0.1, 1.0); +} + +export function scoreCandle(klines: readonly Kline[]): number { + if (!klines.length) return 0; + const k = klines[klines.length - 1]; + const range = k.high - k.low; + if (range === 0) return 0; + const body = Math.abs(k.close - k.open); + const upperWick = k.high - Math.max(k.open, k.close); + const lowerWick = Math.min(k.open, k.close) - k.low; + const bodyRatio = body / range; + const wickPenalty = (upperWick + lowerWick) / range; + return Math.max(0, Math.min(1, bodyRatio - wickPenalty * 0.3)); +} + +export function scoreMA7(klines: readonly Kline[], dir: "up" | "down"): number { + const ma7 = calcMA(klines, 7); + if (ma7 == null) return 0; + const price = klines[klines.length - 1].close; + if (dir === "up") return price > ma7 ? 1.0 : 0; + return price < ma7 ? 1.0 : 0; +} + +// ── 5-minute trend analysis ─────────────────────────────────────────── + +export interface Trend5m { + direction: "up" | "down" | "neutral"; + strength: number; +} + +export function analyze5mTrend(klines5m: readonly Kline[]): Trend5m { + if (klines5m.length < 20) return { direction: "neutral", strength: 0 }; + + const ma5 = calcMA(klines5m, 5); + const ma10 = calcMA(klines5m, 10); + const ma20 = calcMA(klines5m, 20); + if (ma5 == null || ma10 == null || ma20 == null) return { direction: "neutral", strength: 0 }; + + let upVotes = 0, downVotes = 0; + + // a. MA structure + if (ma5 > ma10 && ma10 > ma20) upVotes++; + else if (ma5 < ma10 && ma10 < ma20) downVotes++; + + // b. momentum of the latest 5 bars + const mom = momentumDirection(klines5m, 5, 0.05); + if (mom === "up") upVotes++; + else if (mom === "down") downVotes++; + + // c. consecutive same-direction + const consecUp = consecutiveSameDirection(klines5m, "up"); + const consecDown = consecutiveSameDirection(klines5m, "down"); + if (consecUp >= 2) upVotes++; + if (consecDown >= 2) downVotes++; + + const total = upVotes + downVotes; + if (total === 0) return { direction: "neutral", strength: 0 }; + if (upVotes > downVotes) return { direction: "up", strength: upVotes / 3 }; + if (downVotes > upVotes) return { direction: "down", strength: downVotes / 3 }; + return { direction: "neutral", strength: 0 }; +} + +// ── Long-term trend (MA120) ──────────────────────────────────────── + +export function longTermTrend(klines1m: readonly Kline[]): "up" | "down" | "neutral" { + const ma120 = calcMA(klines1m, 120); + if (ma120 == null || !klines1m.length) return "neutral"; + const price = klines1m[klines1m.length - 1].close; + const diffPct = (price - ma120) / ma120 * 100; + if (diffPct > 1) return "up"; + if (diffPct < -1) return "down"; + return "neutral"; +} + +// ── Factor aggregation ──────────────────────────────────────────────── + +export interface S6Factors { + currDir: "up" | "down" | "neutral"; + momDir: "up" | "down" | "neutral"; + rsi: number | null; + consecutive: number; + scRsi: number; + scVolume: number; + scPriceChange: number; + scCandle: number; + scMa7: number; + scConsecutive: number; + totalScore: number; + threshold: number; + longTrend: "up" | "down" | "neutral"; + trend5mDir: "up" | "down" | "neutral"; + trend5mStrength: number; + dataReady: boolean; +} + +/** Compute a factor snapshot (including current-direction scoring) */ +export function computeFactors(ctx: StrategyTickContext): S6Factors { + const { kline1m, kline5m } = ctx; + const dataReady = kline1m.length >= 120 && kline5m.length >= 20; + + const currDir = currentCandleDirection(kline1m); + const momDir = momentumDirection(kline1m, MOMENTUM_BARS, MOMENTUM_THRESHOLD_PCT); + const rsi = calcRSI(kline1m, 14); + const dir: "up" | "down" = currDir !== "neutral" ? currDir : "up"; + + const consecutive = consecutiveSameDirection(kline1m, dir); + const scConsecutive = scoreConsecutive(consecutive); + const scRsi = rsi != null ? scoreRSI(rsi) : 0; + const scVolume = scoreVolume(kline1m); + const scPriceChange = scorePriceChange(kline1m, MOMENTUM_BARS); + const scCandle = scoreCandle(kline1m); + const scMa7 = scoreMA7(kline1m, dir); + const longTrend = longTermTrend(kline1m); + const trend5m = analyze5mTrend(kline5m); + + let totalScore = + scConsecutive * 0.15 + + scRsi * 0.25 + + scVolume * 0.20 + + scPriceChange * 0.20 + + scCandle * 0.15 + + scMa7 * 0.05; + if (trend5m.direction === dir) totalScore += trend5m.strength * 0.1; + + return { + currDir, momDir, rsi, consecutive, + scRsi, scVolume, scPriceChange, scCandle, scMa7, scConsecutive, + totalScore: Math.round(totalScore * 1000) / 1000, + threshold: dir === "up" ? UP_THRESHOLD : DOWN_THRESHOLD, + longTrend, + trend5mDir: trend5m.direction, + trend5mStrength: Math.round(trend5m.strength * 100) / 100, + dataReady, + }; +} + +/** + * Determine whether it is currently US stock market hours (rough filter only, covering both DST and standard time). + * Monday~Friday UTC 13:30 - 21:00 (covers all of ET 9:30-16:00) + * Weekends are treated as closed all day. + */ +export function isUSMarketOpen(nowMs = Date.now()): boolean { + const d = new Date(nowMs); + const day = d.getUTCDay(); // 0=Sunday, 6=Saturday + if (day === 0 || day === 6) return false; + const minutesUTC = d.getUTCHours() * 60 + d.getUTCMinutes(); + return minutesUTC >= 13 * 60 + 30 && minutesUTC < 21 * 60; +} + +/** Check entry conditions (strictly per the original strategy) */ +export function checkMomentumEntry( + ctx: StrategyTickContext, + minRem: number = WINDOW_MIN_REMAINING, +): { direction: StrategyDirection; entryScore: number } | null { + const { rem, kline1m, kline5m, marketHoursOnly } = ctx; + if (rem <= minRem) return null; + if (kline1m.length < 120) return null; + if (kline5m.length < 20) return null; + // US market hours filter + if (marketHoursOnly && !isUSMarketOpen()) return null; + + // 1. current candle direction + const currDir = currentCandleDirection(kline1m); + if (currDir === "neutral") return null; + + // 2. 5-bar momentum direction + const momDir = momentumDirection(kline1m, MOMENTUM_BARS, MOMENTUM_THRESHOLD_PCT); + if (momDir !== currDir) return null; + + const dir: StrategyDirection = currDir; + + // 3. long-term trend filter + const longTrend = longTermTrend(kline1m); + if (dir === "up" && longTrend === "down") return null; + if (dir === "down" && longTrend === "up") return null; + + // 4. 5-minute trend filter + const trend5m = analyze5mTrend(kline5m); + if (trend5m.strength > 0.3) { + if (dir === "up" && trend5m.direction === "down") return null; + if (dir === "down" && trend5m.direction === "up") return null; + } + + // 5. 6-factor scoring + const rsi = calcRSI(kline1m, 14); + if (rsi == null) return null; + + const consecutive = consecutiveSameDirection(kline1m, dir); + const scConsecutive = scoreConsecutive(consecutive); + const scRsi = scoreRSI(rsi); + const scVolume = scoreVolume(kline1m); + const scPriceChange = scorePriceChange(kline1m, MOMENTUM_BARS); + const scCandle = scoreCandle(kline1m); + const scMa7 = scoreMA7(kline1m, dir); + + let totalScore = + scConsecutive * 0.15 + + scRsi * 0.25 + + scVolume * 0.20 + + scPriceChange * 0.20 + + scCandle * 0.15 + + scMa7 * 0.05; + if (trend5m.direction === dir) totalScore += trend5m.strength * 0.1; + + const threshold = dir === "up" ? UP_THRESHOLD : DOWN_THRESHOLD; + if (totalScore < threshold) return null; + + return { direction: dir, entryScore: totalScore }; +} diff --git a/strategies/_runtime/loader.ts b/strategies/_runtime/loader.ts new file mode 100644 index 0000000..235d5fd --- /dev/null +++ b/strategies/_runtime/loader.ts @@ -0,0 +1,132 @@ +/** + * Strategy dynamic loader (plugin-ization core) + * + * On startup, automatically scans the following directories: + * - strategies/*.ts main project built-in strategies + * - strategies/extensions/* optional extension strategies (can be gitignored) + * + * As long as the filename matches sN.ts (s1 ~ s999), it is automatically imported and registered. + * The main project code needs no changes anywhere, including types.ts / registry.ts. + */ + +import { readdirSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath, pathToFileURL } from "url"; +import type { IStrategy } from "../types.js"; +import { __setStrategyKeys } from "../types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const STRATEGIES_DIR = resolve(__dirname, ".."); +const EXTENSIONS_DIR = resolve(__dirname, "..", "extensions"); + +/** Recognize strategy filenames: .ts (e.g. s1.ts, d1.ts, p1.ts, m1.ts, le1.ts) */ +/** Excludes the _core / _runtime / extensions subdirectory names, and shared modules like s6-core.ts (containing `-`) */ +const STRATEGY_FILE_REGEX = /^([a-z]+)(\d+)\.ts$/; + +interface LoadedFile { + filePath: string; + prefix: string; // letter prefix, used for sorting + number: number; // numeric part + source: "core" | "ext"; +} + +function scanDir(dir: string, source: "core" | "ext"): LoadedFile[] { + if (!existsSync(dir)) return []; + const files: LoadedFile[] = []; + for (const f of readdirSync(dir)) { + const match = f.match(STRATEGY_FILE_REGEX); + if (!match) continue; + files.push({ + filePath: resolve(dir, f), + prefix: match[1], + number: parseInt(match[2], 10), + source, + }); + } + return files; +} + +const strategies: Map = new Map(); +let loaded = false; + +/** Called on startup to dynamically load all strategy files */ +export async function loadAllStrategies(): Promise { + if (loaded) return [...strategies.values()]; + + const files = [ + ...scanDir(STRATEGIES_DIR, "core"), + ...scanDir(EXTENSIONS_DIR, "ext"), + ].sort((a, b) => a.number - b.number); + + const instances: IStrategy[] = []; + for (const f of files) { + try { + const url = pathToFileURL(f.filePath).href; + const mod = await import(url); + // Supports two export styles: + // 1) Old-style class: `export class S1Enhanced implements IStrategy`, using `new ClassName()` + // 2) New style: `export default defineStrategy({...})` or `export default ` + let instance: IStrategy | null = null; + + if (mod.default) { + // New style: default export + if (typeof mod.default === "function") { + instance = new mod.default(); + } else { + instance = mod.default as IStrategy; + } + } else { + // Old style: find the first class export + for (const key of Object.keys(mod)) { + const v = mod[key]; + if (typeof v === "function" && /^[A-Z]/.test(key)) { + instance = new v(); + break; + } + } + } + + if (!instance || typeof instance.checkEntry !== "function") { + console.warn(`[StrategyLoader] ${f.filePath} no valid strategy instance found, skipping`); + continue; + } + + strategies.set(instance.key, instance); + instances.push(instance); + } catch (err) { + console.warn(`[StrategyLoader] failed to load ${f.filePath}: ${err instanceof Error ? err.message : String(err)}`); + } + } + + // Sort by key: first by letter prefix (d s.key), + instances.map(s => s.number), + ); + + loaded = true; + console.log(`[StrategyLoader] loaded ${instances.length} strategies: ${instances.map(s => s.key).join(", ")}`); + return instances; +} + +export function getStrategy(key: string): IStrategy | undefined { + return strategies.get(key); +} + +function sortByKey(a: IStrategy, b: IStrategy): number { + const [, aPrefix = "", aNum = "0"] = a.key.match(/^([a-z]+)(\d+)?$/) ?? []; + const [, bPrefix = "", bNum = "0"] = b.key.match(/^([a-z]+)(\d+)?$/) ?? []; + if (aPrefix !== bPrefix) return aPrefix.localeCompare(bPrefix); + return parseInt(aNum, 10) - parseInt(bNum, 10); +} + +export function getAllStrategies(): IStrategy[] { + return [...strategies.values()].sort(sortByKey); +} + +export function getAllStrategyKeys(): string[] { + return getAllStrategies().map(s => s.key); +} diff --git a/strategies/d1.ts b/strategies/d1.ts new file mode 100644 index 0000000..0487603 --- /dev/null +++ b/strategies/d1.ts @@ -0,0 +1,68 @@ +/** + * Strategy 3 · Tail-Scalp — large-diff entry at the tail of the window + */ + +import type { + IStrategy, StrategyKey, StrategyNumber, StrategyDirection, + StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription, +} from "./types.js"; + +const WINDOW_MAX_REMAINING = 30; +const ENTRY_DIFF = 40; +const ENTRY_PROB_CAP = 80; +const STOP_LOSS_DIFF = 0; + +export class D1Sweep implements IStrategy { + readonly key: StrategyKey = "d1"; + readonly number: StrategyNumber = 1; + readonly name = "Tail-Scalp"; + + getDescription(): StrategyDescription { + return { + key: this.key, + number: this.number, + name: this.name, + title: "Diff 1 · Tail-Scalp", + category: { id: "diff", label: "Diff", color: "#58a6ff" }, + supportedMarkets: ["btc-5m"], + lines: [ + { text: `⏱ Checked when remaining ${WINDOW_MAX_REMAINING}s~0s (the very tail, highest win rate)` }, + { text: `📈 Buy up: diff >+${ENTRY_DIFF} and up probability <${ENTRY_PROB_CAP}%` }, + { text: `📉 Buy down: diff <-${ENTRY_DIFF} and down probability <${ENTRY_PROB_CAP}%` }, + { text: `Stop loss: diff crosses 0 (buy up diff≤0 / buy down diff≥0)`, color: "#f85149", marginTop: true }, + { text: "No take profit, hold to window end", color: "#3fb950" }, + { text: "Backtest 24 days: ~7 trades/day / 86% win rate / $0.70 per trade (passed all 5 validations)", color: "#888", marginTop: true }, + ], + }; + } + + updateGuards(_ctx: StrategyTickContext): void {} + + checkEntry(ctx: StrategyTickContext): EntrySignal | null { + const { rem, upPct, dnPct, diff } = ctx; + if (upPct == null || dnPct == null || diff == null) return null; + if (rem > WINDOW_MAX_REMAINING || rem <= 0) return null; + + if (diff > ENTRY_DIFF && upPct < ENTRY_PROB_CAP) return { direction: "up" }; + if (diff < -ENTRY_DIFF && dnPct < ENTRY_PROB_CAP) return { direction: "down" }; + return null; + } + + checkExit(ctx: StrategyTickContext, direction: StrategyDirection): ExitSignal { + const { diff } = ctx; + if (diff == null) return null; + if (direction === "up" && diff <= STOP_LOSS_DIFF) { + return { signal: "sl", reason: `stop loss diff=${Math.round(diff)}≤${STOP_LOSS_DIFF}` }; + } + if (direction === "down" && diff >= -STOP_LOSS_DIFF) { + return { signal: "sl", reason: `stop loss diff=${Math.round(diff)}≥${-STOP_LOSS_DIFF}` }; + } + return null; + } + + resetState(): void {} + + getStatePayload(): Record { + return {}; + } +} diff --git a/strategies/p1.ts b/strategies/p1.ts new file mode 100644 index 0000000..49675c1 --- /dev/null +++ b/strategies/p1.ts @@ -0,0 +1,135 @@ +/** + * Strategy 5 · Prob Chase — when diff crosses, the probability is too low; hold to window settlement + * + * Core logic: at the moment diff breaks the threshold, if the probability has not caught up yet (deviates from the historical fair probability), + * it means the market is reacting slowly, so enter and buy. + * + * Exit: no take profit, no timeout; only stop loss when diff crosses to the reverse ±5; otherwise hold to window-end settlement. + */ + +import type { + IStrategy, StrategyKey, StrategyNumber, StrategyDirection, + StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription, +} from "./types.js"; +import { getFairProb } from "./_core/fair-prob.js"; + +// ── Entry parameters ──────────────────────────────────────────────── +const ENTRY_DIFF = 25; // check the deviation when diff crosses this threshold +const ENTRY_BIAS_MIN = 10; // enter only when the probability deviation is at least this many percentage points (fair probability - actual probability ≥ 10) +const WINDOW_MAX_REMAINING = 90; // entry scan start: remaining ≤90s +const WINDOW_MIN_REMAINING = 30; // entry scan end: remaining ≤30s + +// ── Exit parameters ──────────────────────────────────────────────── +const STOP_LOSS_DIFF = 5; // cross -5 stop loss: buy up diff≤-5 / buy down diff≥5 + +interface S5State { + lastDiff: number | null; + entryBias: number; // deviation value at entry + entryTs: number; // entry timestamp +} + +function createState(): S5State { + return { + lastDiff: null, + entryBias: 0, + entryTs: 0, + }; +} + +export class P1ProbChase implements IStrategy { + readonly key: StrategyKey = "p1"; + readonly number: StrategyNumber = 1; + readonly name = "Prob Chase"; + + private s: S5State = createState(); + + getDescription(): StrategyDescription { + return { + key: this.key, + number: this.number, + name: this.name, + title: "Prob Chase 1", + category: { id: "prob-chase", label: "Prob Chase", color: "#f0a500" }, + supportedMarkets: ["btc-5m"], + lines: [ + { text: `⏱ Checked when remaining ${WINDOW_MAX_REMAINING}s~${WINDOW_MIN_REMAINING}s` }, + { text: `📈 Enter when diff crosses ±${ENTRY_DIFF} and the probability deviation ≥${ENTRY_BIAS_MIN}%` }, + { text: "deviation = historical fair probability - current probability (probability has not caught up with diff)" }, + { text: `Stop loss: buy up diff≤-${STOP_LOSS_DIFF} / buy down diff≥${STOP_LOSS_DIFF}`, color: "#f85149", marginTop: true }, + { text: "No take profit, no timeout; hold to window end and let settlement decide", color: "#f0a500" }, + { text: "Fair probability judged from a diff+rem 2D mapping table", color: "#888", marginTop: true }, + ], + }; + } + + updateGuards(_ctx: StrategyTickContext): void {} + + checkEntry(ctx: StrategyTickContext): EntrySignal | null { + const { rem, upPct, dnPct, diff } = ctx; + if (upPct == null || dnPct == null || diff == null) return null; + if (rem > WINDOW_MAX_REMAINING || rem <= WINDOW_MIN_REMAINING) return null; + + const lastDiff = this.s.lastDiff; + if (lastDiff == null) return null; + + // buy-up crossing + if (lastDiff <= ENTRY_DIFF && diff > ENTRY_DIFF) { + const fair = getFairProb(diff, rem); + if (fair != null && fair - upPct >= ENTRY_BIAS_MIN) { + this.s.entryBias = fair - upPct; + return { direction: "up" }; + } + } + + // buy-down crossing + if (lastDiff >= -ENTRY_DIFF && diff < -ENTRY_DIFF) { + const fair = getFairProb(diff, rem); + if (fair != null) { + const fairDn = 100 - fair; + const bias = fairDn - dnPct; + if (bias >= ENTRY_BIAS_MIN) { + this.s.entryBias = bias; + return { direction: "down" }; + } + } + } + + return null; + } + + onEntryFilled(ctx: StrategyTickContext, _direction: StrategyDirection): void { + this.s.entryTs = ctx.now; + } + + checkExit(ctx: StrategyTickContext, direction: StrategyDirection): ExitSignal { + const { diff } = ctx; + if (diff == null) return null; + + // cross -5 stop loss: buy up diff≤-5 / buy down diff≥5 + if (direction === "up" && diff <= -STOP_LOSS_DIFF) { + return { signal: "sl", reason: `reverse-cross stop loss diff ${Math.round(diff)}≤-${STOP_LOSS_DIFF}` }; + } + if (direction === "down" && diff >= STOP_LOSS_DIFF) { + return { signal: "sl", reason: `reverse-cross stop loss diff ${Math.round(diff)}≥${STOP_LOSS_DIFF}` }; + } + + // no take profit, no timeout, no forced close → return null, hold to window-end settlement + return null; + } + + finalizeTick(diff: number | null): void { + this.s.lastDiff = diff; + } + + resetState(): void { + this.s = createState(); + } + + getStatePayload(): Record { + return { + lastDiff: this.s.lastDiff, + entryBias: this.s.entryBias, + entryTs: this.s.entryTs, + }; + } +} diff --git a/strategies/p2.ts b/strategies/p2.ts new file mode 100644 index 0000000..9bda547 --- /dev/null +++ b/strategies/p2.ts @@ -0,0 +1,137 @@ +/** + * Strategy p2 · Prob Chase · End-Game Crossing version + * + * Entry (checked each tick): + * - 30 < rem ≤ 90 (window remaining 90s ~ 30s) + * - diff crosses ±25 (previous tick within ±25 / current tick crosses out) + * - entry-direction current probability < 65% (market has not caught up) + * - entry-direction deviation = historical fair probability - current probability ≥ 10% + * + * Exit: + * - no take profit + * - stop loss: buy up diff ≤ -5 / buy down diff ≥ 5 + * - no timeout; with no stop loss, hold to window end and let settlement decide + */ + +import type { + IStrategy, StrategyKey, StrategyNumber, StrategyDirection, + StrategyTickContext, EntrySignal, ExitSignal, StrategyDescription, +} from "./types.js"; +import { getFairProb } from "./_core/fair-prob.js"; + +const REM_MAX = 90; +const REM_MIN = 30; +const ENTRY_DIFF = 25; +const ENTRY_BIAS_MIN = 10; +const ENTRY_PCT_MAX = 65; +const SL_DIFF = 5; + +interface P2State { + lastDiff: number | null; + entryBias: number; + entryTs: number; +} + +function createState(): P2State { + return { lastDiff: null, entryBias: 0, entryTs: 0 }; +} + +export class P2ProbChaseTail implements IStrategy { + readonly key: StrategyKey = "p2"; + readonly number: StrategyNumber = 2; + readonly name = "Prob Chase · End-Game Crossing"; + + private s: P2State = createState(); + + getDescription(): StrategyDescription { + return { + key: this.key, + number: this.number, + name: this.name, + title: "Prob Chase 2 · End-Game Crossing", + category: { id: "prob-chase", label: "Prob Chase", color: "#f0a500" }, + supportedMarkets: ["btc-5m"], + lines: [ + { text: `⏱ Checked when remaining ${REM_MIN}s ~ ${REM_MAX}s` }, + { text: `📈 Enter when diff crosses ±${ENTRY_DIFF} and deviation ≥ ${ENTRY_BIAS_MIN}%` }, + { text: "deviation = historical fair probability - current probability (probability has not caught up with diff)" }, + { text: `🔒 Entry-direction current probability < ${ENTRY_PCT_MAX}% (enter only when the market has not caught up)` }, + { text: `Stop loss: buy up diff≤-${SL_DIFF} / buy down diff≥${SL_DIFF}`, color: "#f85149", marginTop: true }, + { text: "No take profit, no timeout; hold to window end and let settlement decide", color: "#3fb950" }, + { text: "Fair probability judged from a diff+rem 2D mapping table", color: "#888", marginTop: true }, + ], + }; + } + + updateGuards(_ctx: StrategyTickContext): void {} + + checkEntry(ctx: StrategyTickContext): EntrySignal | null { + const { rem, upPct, dnPct, diff } = ctx; + if (upPct == null || dnPct == null || diff == null) return null; + if (rem > REM_MAX || rem <= REM_MIN) return null; + + const lastDiff = this.s.lastDiff; + if (lastDiff == null) return null; + + // cross up over +ENTRY_DIFF → buy up + if (lastDiff <= ENTRY_DIFF && diff > ENTRY_DIFF && upPct < ENTRY_PCT_MAX) { + const fair = getFairProb(diff, rem); + if (fair != null) { + const upBias = fair - upPct; + if (upBias >= ENTRY_BIAS_MIN) { + this.s.entryBias = upBias; + return { direction: "up" }; + } + } + } + + // cross down below -ENTRY_DIFF → buy down + if (lastDiff >= -ENTRY_DIFF && diff < -ENTRY_DIFF && dnPct < ENTRY_PCT_MAX) { + const fair = getFairProb(diff, rem); + if (fair != null) { + const fairDn = 100 - fair; + const dnBias = fairDn - dnPct; + if (dnBias >= ENTRY_BIAS_MIN) { + this.s.entryBias = dnBias; + return { direction: "down" }; + } + } + } + + return null; + } + + onEntryFilled(ctx: StrategyTickContext, _direction: StrategyDirection): void { + this.s.entryTs = ctx.now; + } + + checkExit(ctx: StrategyTickContext, direction: StrategyDirection): ExitSignal { + const { diff } = ctx; + if (diff == null) return null; + + if (direction === "up" && diff <= -SL_DIFF) { + return { signal: "sl", reason: `reverse cross diff=${Math.round(diff)} ≤ -${SL_DIFF}` }; + } + if (direction === "down" && diff >= SL_DIFF) { + return { signal: "sl", reason: `reverse cross diff=${Math.round(diff)} ≥ +${SL_DIFF}` }; + } + return null; + } + + /** Called externally at the end of each tick to record lastDiff for crossing detection */ + finalizeTick(diff: number | null): void { + this.s.lastDiff = diff; + } + + resetState(): void { + this.s = createState(); + } + + getStatePayload(): Record { + return { + entryBias: this.s.entryBias, + entryTs: this.s.entryTs, + lastDiff: this.s.lastDiff, + }; + } +} diff --git a/strategies/registry.ts b/strategies/registry.ts new file mode 100644 index 0000000..fbf19b7 --- /dev/null +++ b/strategies/registry.ts @@ -0,0 +1,41 @@ +/** + * Strategy registry —— public interface layer + * + * After plugin-ization, the actual loading logic lives in `_runtime/loader.ts`. + * This file is kept as the public interface so server.ts's imports do not need to change. + * + * Startup flow: server.ts awaits initStrategies() on startup, after which other modules can make calls. + */ + +import type { IStrategy, StrategyDescription } from "./types.js"; +import { + loadAllStrategies, + getStrategy as _getStrategy, + getAllStrategies as _getAllStrategies, + getAllStrategyKeys as _getAllStrategyKeys, +} from "./_runtime/loader.js"; + +let ready = false; + +/** Must be called once on startup (server.ts awaits it before server.listen) */ +export async function initStrategies(): Promise { + if (ready) return; + await loadAllStrategies(); + ready = true; +} + +export function getStrategy(key: string): IStrategy | undefined { + return _getStrategy(key); +} + +export function getAllStrategies(): IStrategy[] { + return _getAllStrategies(); +} + +export function getAllStrategyKeys(): string[] { + return _getAllStrategyKeys(); +} + +export function getAllDescriptions(): StrategyDescription[] { + return _getAllStrategies().map(s => s.getDescription()); +} diff --git a/strategies/types.ts b/strategies/types.ts new file mode 100644 index 0000000..769dd5a --- /dev/null +++ b/strategies/types.ts @@ -0,0 +1,291 @@ +/** + * Shared types for strategy modules + */ + +// ── Strategy types (after plugin-ization: strategies are self-governing, no hard-coded list) ─────────── +// Add a strategy: create a new sN.ts under strategies/ or strategies/extensions/ +// Remove a strategy: delete the corresponding sN.ts +// The main project server.ts / index.html need no changes at all +export type StrategyKey = string; // e.g. "s1" "s6" "s13" +export type StrategyNumber = number; // e.g. 1 6 13 + +// Populated at runtime by the loader. Uses a mutable array + in-place splice, so that the +// binding obtained via import sees the latest content (any array method like .map/.filter/.forEach works normally) +export const ALL_STRATEGY_KEYS: StrategyKey[] = []; +export const ALL_STRATEGY_NUMBERS: StrategyNumber[] = []; +export function __setStrategyKeys(keys: readonly StrategyKey[], numbers: readonly StrategyNumber[]): void { + ALL_STRATEGY_KEYS.splice(0, ALL_STRATEGY_KEYS.length, ...keys); + ALL_STRATEGY_NUMBERS.splice(0, ALL_STRATEGY_NUMBERS.length, ...numbers); +} + +export type StrategyDirection = "up" | "down"; +export type StrategyLifecycleState = + | "IDLE" + | "SCANNING" + | "BUYING" + | "WAIT_FILL" + | "RECONCILING_FILL" + | "HOLDING" + | "SELLING" + | "WAIT_SELL_FILL" + | "DONE"; + +/** Binance K-line */ +export interface Kline { + openTime: number; + open: number; + high: number; + low: number; + close: number; + volume: number; + closed: boolean; +} + +/** Read-only market snapshot passed to strategies each tick */ +export interface StrategyTickContext { + rem: number; + upPct: number | null; + dnPct: number | null; + diff: number | null; // Binance current price - window open price (absolute USD value), strongly correlated with the coin's price magnitude + diffBps: number | null; // diff as a fraction of the window open price in bps (diff/PTB*10000), cross-coin generic + volPct: number | null; // 30-second rolling amplitude percentage = (max - min) / min * 100; null within the first 30s of startup / when data is insufficient + now: number; + prevUpPct: number | null; + kline1m: readonly Kline[]; // Binance 1-minute K-lines (latest at the end) + kline5m: readonly Kline[]; // Binance 5-minute K-lines + marketHoursOnly: boolean; // whether momentum strategies only enter during US stock market hours +} + +/** Strategy entry signal */ +export interface EntrySignal { + direction: StrategyDirection; +} + +/** Limit open-order signal (used by checkLimitOrder) */ +export interface LimitOrderSignal { + direction: StrategyDirection; + /** Order price (0-1, absolute price) */ + price: number; + /** Shares (minimum 5, limited by Polymarket maker minimum) */ + shares: number; +} + +/** Presign request (strategy declares the limit-order parameters that need presigning) + * + * How it works (server implementation): + * - When rem ∈ [remMin, remMax], the server asynchronously createOrder in the background and caches the signed package + * - On trigger checkLimitOrder returns a LimitOrderSignal; if the server hits the presign cache it skips signing and postOrder directly + * - All presign caches are cleared on window switch + * - Conservative failure handling: a failed presign is not retried; if the cache is empty on trigger it falls back to live signing (the original path) + */ +export interface PresignRequest { + /** Which rem interval to presign in (e.g. [152, 160] corresponds to elapsed 140~148s) */ + remMin: number; + remMax: number; + /** Directions to presign (e.g. ["up", "down"] for both sides or ["up"] for one side) */ + directions: StrategyDirection[]; + /** Limit price (0~1 absolute price) */ + price: number; + /** Shares (minimum 5) */ + shares: number; +} + +/** Limit-order runtime state (passed to checkCancelOrder) */ +export interface LimitOrderRuntime { + direction: StrategyDirection; + price: number; + shares: number; + filledSize: number; + windowStart: number; +} + +/** Strategy exit signal */ +export interface ExitSignalResult { + signal: "tp" | "sl"; + reason: string; +} + +export type ExitSignal = ExitSignalResult | null; + +/** Description line for the frontend hover tooltip */ +export interface StrategyDescriptionLine { + text: string; + color?: string; + marginTop?: boolean; +} + +/** Strategy category (for frontend grouped display) */ +export interface StrategyCategory { + id: string; // category id, e.g. "momentum" + label: string; // display name, e.g. "Momentum" + color: string; // category primary color, e.g. "#3fb950" +} + +/** A single row of the observe panel (generic rendering) */ +export type ObserveRow = + | { type: "score"; label: string; value: number | null; threshold?: number; unit?: string } + | { type: "direction"; label: string; value: "up" | "down" | "neutral" | null; extra?: string } + | { type: "text"; label: string; value: string | number | null; color?: string } + | { type: "separator" }; + +/** Strategy observe panel (generic display area in the top status bar) */ +export interface ObservePanelData { + title: string; // panel title, e.g. "Entry factors" + color?: string; // title color + rows: ObserveRow[]; // data rows +} + +/** Strategy description (used by the frontend to dynamically generate the UI) */ +export interface StrategyDescription { + key: StrategyKey; + number: StrategyNumber; + name: string; + title: string; + lines: StrategyDescriptionLine[]; + /** Frontend category (defaults to "Uncategorized" if omitted) */ + category?: StrategyCategory; + /** List of supported markets (market key, e.g. ["btc-5m", "btc-15m"]). Omitted or empty array = all markets */ + supportedMarkets?: string[]; + /** Order type: market (market only, frontend shows amount box) / limit (limit only, shows shares box) / both (shows both). Defaults to "market" if omitted */ + orderType?: "market" | "limit" | "both"; + /** Tunable parameter declarations (frontend renders extra input boxes accordingly). Each item has key/label/default value/minimum/step */ + tunableParams?: TunableParam[]; + /** Parameter groups (optional; frontend renders by group with separators between groups). Each group lists the parameter keys it contains. + * Parameters not specified are automatically placed in a trailing "Other" group. If paramGroups is omitted entirely, parameters are rendered flat. */ + paramGroups?: StrategyParamGroup[]; +} + +export interface StrategyParamGroup { + /** Group display name (e.g. "Trigger" "Position" "Session") */ + label: string; + /** List of parameter keys contained in this group (must appear in tunableParams) */ + params: string[]; + /** Optional: group color (side bar / label color) */ + color?: string; +} + +/** Strategy tunable parameter definition (frontend renders input box, server persists to strategyConfig.params) */ +export interface TunableParam { + /** Parameter key (e.g. "tpDelta" "slDiff"), matching the strategy instance field name */ + key: string; + /** Display name (e.g. "Take profit" "Stop loss diff") */ + label: string; + /** Default value (used for the frontend's first render) */ + defaultValue: number; + /** Minimum (inclusive) */ + min: number; + /** Maximum (inclusive); unbounded if omitted */ + max?: number; + /** Step (input step) */ + step: number; + /** Unit hint (shown to the right of the input box, e.g. "¢" "USD") */ + unit?: string; + /** hover tooltip */ + title?: string; +} + +/** Strategy interface — every strategy must implement it */ +export interface IStrategy { + readonly key: StrategyKey; + readonly number: StrategyNumber; + readonly name: string; + + /** Return the frontend hover description */ + getDescription(): StrategyDescription; + + /** Update internal guard state (cooldown locks, etc.) each tick, called before checkEntry */ + updateGuards(ctx: StrategyTickContext): void; + + /** Check entry conditions (called during the SCANNING phase) */ + checkEntry(ctx: StrategyTickContext): EntrySignal | null; + + /** Check exit conditions (called during the HOLDING phase) */ + checkExit(ctx: StrategyTickContext, direction: StrategyDirection): ExitSignal; + + /** Reset the strategy's private state on window switch */ + resetState(): void; + + /** Serialize the strategy's private state for broadcasting to the frontend */ + getStatePayload(): Record; + + /** Notify the strategy that it has entered a position (called after a buy fills) */ + onEntryFilled?(ctx: StrategyTickContext, direction: StrategyDirection): void; + + // ── Plugin extension points (all optional) ──────────────────────────── + + /** Whether the strategy needs to "compute data every tick even when disabled" (e.g. s6's factor panel) */ + readonly alwaysComputeData?: boolean; + + /** Compute data each tick (called when alwaysComputeData=true, regardless of whether the strategy is enabled) */ + computeData?(ctx: StrategyTickContext): void; + + /** Return observe panel data (the frontend's generic renderer will render this) */ + getObservePanel?(): ObservePanelData | null; + + // ── Limit-order strategy extension points (only implemented by strategies with orderType=limit/both) ── + + /** + * Check whether a limit order needs to be placed (called each tick, only when this strategy has no active open order) + * Returns LimitOrderSignal → server will place a GTC limit order + * Returns null → skip this tick + */ + checkLimitOrder?(ctx: StrategyTickContext): LimitOrderSignal | null; + + /** + * Check whether an already-placed order needs to be canceled (called each tick, only when there is an active open order) + * Returns true → server cancels the order + * + * Note: the strategy does not need to manage "whether to cancel the remainder after a fill" — the server cancels automatically + */ + checkCancelOrder?(ctx: StrategyTickContext, order: LimitOrderRuntime): boolean; + + /** + * Market strategy: the absolute target price (0~1) for the GTC sell take-profit placed after entry + * - Returns a number → server places a sell @ targetPrice via the cond system after MINED + * - Returns null / not implemented → no take profit placed, exit controlled by checkExit + * Note: stop loss still goes through checkExit's sl signal (market sell), unaffected by this interface + */ + getMarketTakeProfitPrice?(): number | null; + + /** + * Limit strategy: declare presign requirements (not implemented = no presigning needed) + * - Returns PresignRequest → server pre-signs and caches in the background when rem ∈ [remMin, remMax] + * - When checkLimitOrder triggers, the server preferentially sends using the presign package; a hit = skip the signing latency + * - All presign packages are invalidated on window switch + * - Conservative on failure: a failed presign is not retried; if the cache is empty on trigger it falls back to live signing + * + * Note: strategy code does not need to be aware of whether the presign hit; it only needs to return a LimitOrderSignal. + * Presigning is purely a server-side latency optimization. + */ + getPresignRequest?(): PresignRequest | null; + + /** + * Limit strategy: whether placing a new order is allowed in this window after a cancel (default false, only one order per window) + * - false (default): no further order after cancel, wait for the next window + * - true: the window marker is cleared after cancel, and a new order may be placed on the next tick if conditions are met + */ + readonly limitAllowReplaceAfterCancel?: boolean; + + /** + * The price (0~1 absolute price) for the take-profit sell order placed after a limit maker order is confirmed filled on-chain + * - Returns a number → server immediately places a same-direction GTC sell order after MINED (partial fills are placed in batches) + * - Returns null / not implemented → no take profit placed; filled shares are held to settlement + * + * Note: mutually exclusive with getLimitConditionOrder. If the strategy implements getLimitConditionOrder, + * the server preferentially uses the cond system (TP+SL managed in a shared group) and ignores this interface. + */ + getLimitTakeProfitPrice?(): number | null; + + /** + * Create a conditional order (take profit + stop loss, both directions) after a limit maker order is confirmed filled on-chain + * - Reuses the manual conditional-order infrastructure (cond-tp / cond-sl); TP/SL share a groupId and clean each other up + * - stopProfit.pctDelta: take-profit price = entryPrice + pctDelta (e.g. entry=0.50, delta=0.05 → tp=0.55) + * - stopProfit.targetPrice: take-profit absolute price (e.g. 0.99); choose one of this or pctDelta + * - stopLoss.diffValue: triggered by diff crossing (buy up: diff ≤ -diffValue / buy down: diff ≥ diffValue) + * - Returns null / not implemented → falls back to the getLimitTakeProfitPrice path + */ + getLimitConditionOrder?(): { + stopProfit?: { pctDelta?: number; targetPrice?: number }; + stopLoss?: { diffValue?: number; slippage?: number }; + } | null; +} diff --git a/terminal.css b/terminal.css new file mode 100644 index 0000000..8151fef --- /dev/null +++ b/terminal.css @@ -0,0 +1,1300 @@ +/* + * BTC 5m Terminal UI + * Standalone visual layer: only covers typography, layering, color, and responsiveness; does not change any business JS bindings. + */ + +:root { + --terminal-bg: #04070b; + --terminal-ink: #f2f7ff; + --terminal-muted: #9aa8b8; + --terminal-faint: #5f6f83; + --terminal-line: rgba(151, 166, 188, 0.15); + --terminal-line-strong: rgba(151, 166, 188, 0.28); + --terminal-card: rgba(9, 15, 24, 0.78); + --terminal-card-strong: rgba(13, 22, 34, 0.94); + --terminal-card-soft: rgba(255, 255, 255, 0.045); + --terminal-green: #2ee6a6; + --terminal-red: #ff6474; + --terminal-amber: #f5bd4f; + --terminal-blue: #62bdff; + --terminal-cyan: #41e8ff; + --terminal-radius: 20px; + --terminal-radius-lg: 28px; + --terminal-shadow: 0 22px 70px rgba(0, 0, 0, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.055); + --terminal-shadow-soft: 0 14px 44px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.045); + --terminal-font: "Avenir Next", "DIN Alternate", "PingFang SC", "Microsoft YaHei", sans-serif; + --terminal-num: "JetBrains Mono", "SF Mono", "Fira Code", "Cascadia Code", monospace; +} + +html { + background: var(--terminal-bg); +} + +body { + min-width: 0; + color: var(--terminal-ink); + font-family: var(--terminal-font); + letter-spacing: 0.01em; + background: + radial-gradient(circle at 8% -8%, rgba(98, 189, 255, 0.24), transparent 30rem), + radial-gradient(circle at 92% 2%, rgba(46, 230, 166, 0.16), transparent 34rem), + radial-gradient(circle at 55% 110%, rgba(245, 189, 79, 0.11), transparent 24rem), + linear-gradient(135deg, #04070b 0%, #07111a 48%, #060a10 100%); +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background-image: + linear-gradient(rgba(255, 255, 255, 0.022) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px); + background-size: 44px 44px; + mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.9), transparent 76%); +} + +button, +input { + font-family: inherit; +} + +input { + color-scheme: dark; +} + +.up { color: var(--terminal-green) !important; } +.down { color: var(--terminal-red) !important; } +.yellow { color: var(--terminal-amber) !important; } +.gray { color: var(--terminal-faint) !important; } + +::-webkit-scrollbar { + width: 9px; + height: 9px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.028); +} + +::-webkit-scrollbar-thumb { + background: rgba(151, 166, 188, 0.26); + border: 2px solid rgba(4, 7, 11, 0.85); + border-radius: 999px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(151, 166, 188, 0.42); +} + +#header, +#price-bar, +#strat-bar, +#observe-bar, +#main-wrap, +#pm-pnl, +#app-footer { + width: calc(100% - 32px); + max-width: 1880px; + margin-left: auto !important; + margin-right: auto !important; +} + +/* Top Command Bar */ +#header { + position: sticky; + top: 12px; + z-index: 5000; + margin-top: 14px !important; + padding: 12px 16px !important; + gap: 12px 14px !important; + border: 1px solid var(--terminal-line) !important; + border-radius: var(--terminal-radius-lg) !important; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.072), rgba(255, 255, 255, 0.022)), + rgba(7, 12, 20, 0.96) !important; + box-shadow: var(--terminal-shadow); + backdrop-filter: none; + overflow: visible !important; +} + +#header:has(.result-dot:hover), +#header:has(.order-latency-trigger:hover) { + z-index: 8000; +} + +#header .window-info { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--terminal-ink); + font-size: 16px !important; + font-weight: 900 !important; + letter-spacing: 0.02em; +} + +#header .window-info::before { + content: ""; + width: 10px; + height: 32px; + flex: 0 0 auto; + border-radius: 999px; + background: linear-gradient(180deg, var(--terminal-green), var(--terminal-blue)); + box-shadow: 0 0 24px rgba(46, 230, 166, 0.44); +} + +#header .window-info span { + color: var(--terminal-amber) !important; +} + +#app-version { + padding: 2px 7px; + border: 1px solid rgba(151, 166, 188, 0.13); + border-radius: 999px; + background: rgba(255, 255, 255, 0.065); + color: var(--terminal-faint) !important; +} + +#header > a { + padding: 6px 10px; + border: 1px solid rgba(245, 189, 79, 0.24); + border-radius: 999px; + background: rgba(245, 189, 79, 0.08); + color: var(--terminal-amber) !important; +} + +#recent-results { + gap: 7px !important; + margin-left: auto; +} + +.result-dot { + width: 24px !important; + height: 24px !important; + border: 1px solid var(--terminal-line); + background: rgba(255, 255, 255, 0.055); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +.result-dot.up { + background: linear-gradient(135deg, rgba(46, 230, 166, 0.94), rgba(14, 128, 94, 0.94)) !important; +} + +.result-dot.down { + background: linear-gradient(135deg, rgba(255, 100, 116, 0.96), rgba(138, 38, 52, 0.96)) !important; +} + +#conn-dot { + width: 10px !important; + height: 10px !important; + background: var(--terminal-green) !important; + box-shadow: 0 0 0 5px rgba(46, 230, 166, 0.12), 0 0 22px rgba(46, 230, 166, 0.76) !important; +} + +#conn-dot.disconnected { + background: var(--terminal-red) !important; + box-shadow: 0 0 0 5px rgba(255, 100, 116, 0.12), 0 0 20px rgba(255, 100, 116, 0.74) !important; +} + +#ws-status { + gap: 7px !important; + flex-wrap: wrap; +} + +.ws-light, +#kline-status { + min-height: 26px; + padding: 5px 9px; + gap: 6px; + border: 1px solid rgba(151, 166, 188, 0.12); + border-radius: 999px; + background: rgba(255, 255, 255, 0.045); + color: var(--terminal-muted) !important; + font-size: 11px !important; + white-space: nowrap; +} + +.ws-light .dot { + width: 7px !important; + height: 7px !important; + background: var(--terminal-red); + box-shadow: 0 0 10px rgba(255, 100, 116, 0.75); +} + +.ws-light.on .dot, +.ws-light.http-fast .dot { + background: var(--terminal-green) !important; + box-shadow: 0 0 12px rgba(46, 230, 166, 0.78) !important; +} + +.ws-light.http-slow .dot { + background: var(--terminal-amber) !important; + box-shadow: 0 0 12px rgba(245, 189, 79, 0.72) !important; +} + +.mode-toggle { + margin-left: 0 !important; + padding: 5px 6px 5px 11px !important; + border-color: var(--terminal-line) !important; + background: rgba(255, 255, 255, 0.045) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06); +} + +.mode-toggle-label { + color: var(--terminal-muted) !important; +} + +.mode-toggle-btn { + border-color: rgba(151, 166, 188, 0.22) !important; + background: rgba(255, 255, 255, 0.08) !important; +} + +.mode-toggle-btn::after { + background: #d7e4f2 !important; + box-shadow: 0 3px 10px rgba(0, 0, 0, 0.45); +} + +.mode-toggle-btn.on { + border-color: rgba(46, 230, 166, 0.65) !important; + background: rgba(46, 230, 166, 0.18) !important; +} + +.mode-toggle-btn.on::after { + background: var(--terminal-green) !important; +} + +/* Decision Strip */ +#price-bar { + margin-top: 14px !important; + padding: 0 !important; + display: grid !important; + grid-template-columns: minmax(190px, 1.25fr) minmax(190px, 1.25fr) repeat(3, minmax(145px, 0.82fr)) minmax(132px, 0.72fr); + gap: 12px !important; + border: 0 !important; + background: transparent !important; +} + +.price-item { + position: relative; + min-height: 104px; + padding: 17px 18px !important; + justify-content: space-between; + overflow: hidden; + border: 1px solid var(--terminal-line); + border-radius: var(--terminal-radius); + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.088), rgba(255, 255, 255, 0.018)), + var(--terminal-card) !important; + box-shadow: var(--terminal-shadow-soft); +} + +.price-item::before { + content: ""; + position: absolute; + inset: 0; + pointer-events: none; + background: + linear-gradient(120deg, rgba(255, 255, 255, 0.08), transparent 34%), + radial-gradient(circle at 85% 18%, rgba(98, 189, 255, 0.13), transparent 36%); +} + +.price-item:nth-child(1), +.price-item:nth-child(2) { + min-height: 122px; +} + +.price-item:nth-child(1)::before { + background: + linear-gradient(120deg, rgba(245, 189, 79, 0.13), transparent 38%), + radial-gradient(circle at 84% 22%, rgba(245, 189, 79, 0.18), transparent 36%); +} + +.price-item:nth-child(2)::before { + background: + linear-gradient(120deg, rgba(46, 230, 166, 0.11), transparent 38%), + radial-gradient(circle at 84% 22%, rgba(65, 232, 255, 0.15), transparent 36%); +} + +.price-label, +.compact-label { + position: relative; + z-index: 1; + color: var(--terminal-faint) !important; + font-size: 10px !important; + font-weight: 900; + letter-spacing: 0.14em !important; +} + +.price-val, +#btc-diff, +#btc-diff-bn, +#btc-vol { + position: relative; + z-index: 1; + color: var(--terminal-ink); + font-family: var(--terminal-num); + font-size: clamp(21px, 1.7vw, 28px) !important; + font-weight: 900 !important; + letter-spacing: -0.045em; +} + +.price-item:nth-child(1) .price-val, +.price-item:nth-child(2) .price-val { + font-size: clamp(26px, 2.1vw, 36px) !important; +} + +#remaining { + color: var(--terminal-amber) !important; + text-shadow: 0 0 24px rgba(245, 189, 79, 0.35); +} + +#remaining.ending { + color: var(--terminal-red) !important; + text-shadow: 0 0 24px rgba(255, 100, 116, 0.55); +} + +/* Strategy And Observability */ +#strat-bar, +#observe-bar { + position: relative; + margin-top: 12px !important; + border: 1px solid var(--terminal-line) !important; + border-radius: var(--terminal-radius) !important; + background: + linear-gradient(135deg, rgba(255, 255, 255, 0.062), rgba(255, 255, 255, 0.018)), + rgba(8, 14, 23, 0.96) !important; + box-shadow: var(--terminal-shadow-soft); + backdrop-filter: none; +} + +#strat-bar { + padding: 14px 16px !important; + z-index: 4000; + overflow: visible !important; +} + +#strat-bar:has(.strat-tip:hover), +#strat-bar:has(.tg-help-trigger:hover), +#strat-bar:has(#tg-config-body[style*="display: flex"]), +#strat-bar:has(.strat-popup.open) { + z-index: 9000; +} + +#observe-bar { + padding: 10px 14px !important; + color: var(--terminal-muted) !important; + z-index: 200; +} + +.strat-row { + position: relative; + z-index: 1; + align-items: center; + padding: 4px 0; +} + +.strat-row:has(.strat-tip:hover), +.strat-row:has(.tip-box:hover), +.strat-row:has(.strat-popup.open) { + z-index: 100010; +} + +.strat-group { + position: relative; + z-index: 1; +} + +.strat-group:has(.strat-tip:hover), +.strat-group:has(.tip-box:hover) { + z-index: 100011; +} + +.strat-group label { + position: relative; + z-index: 1; + padding: 6px 10px; + border: 1px solid rgba(151, 166, 188, 0.12); + border-radius: 999px; + background: rgba(255, 255, 255, 0.035); + color: var(--terminal-muted); + transition: transform 0.16s ease, border-color 0.16s ease, background 0.16s ease; +} + +.strat-group label:hover { + z-index: 100012; + transform: translateY(-1px); + border-color: rgba(245, 189, 79, 0.32); + background: rgba(245, 189, 79, 0.06); +} + +.strat-group input[type=checkbox] + .strat-tip::before { + color: rgba(151, 166, 188, 0.42); +} + +.strat-group input[type=checkbox]:checked + .strat-tip::before, +.strat-group label:has(input:checked) { + color: var(--terminal-amber) !important; +} + +.strat-amt { + height: 26px; + border-radius: 9px !important; +} + +#strat-status { + min-height: 30px; + padding: 6px 11px; + border: 1px solid rgba(245, 189, 79, 0.18); + border-radius: 999px; + background: rgba(245, 189, 79, 0.075); +} + +#strat-state-text { + font-weight: 900 !important; + letter-spacing: 0.02em; +} + +#strat-state-text.idle { color: var(--terminal-faint) !important; } +#strat-state-text.scan { color: var(--terminal-muted) !important; } +#strat-state-text.active { color: var(--terminal-amber) !important; } +#strat-state-text.hold { color: var(--terminal-green) !important; } +#strat-state-text.done { color: var(--terminal-blue) !important; } +#strat-state-text.fail { color: var(--terminal-red) !important; } + +.strat-divider { + background: var(--terminal-line-strong) !important; +} + +/* Workbench */ +#main-wrap { + position: relative; + z-index: 100; + margin-top: 14px !important; + padding: 0 !important; + display: grid !important; + grid-template-columns: minmax(0, 1fr) 380px; + gap: 14px !important; + align-items: start; +} + +#main-wrap:has(.allowance-wrap:hover) { + z-index: 7000; +} + +#book, +#order-panel, +#pm-pnl { + position: relative; + border: 1px solid var(--terminal-line) !important; + border-radius: var(--terminal-radius-lg) !important; + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.065), rgba(255, 255, 255, 0.018)), + rgba(7, 12, 20, 0.96) !important; + box-shadow: var(--terminal-shadow); + backdrop-filter: none; +} + +#book { + z-index: 10; + padding: 0 !important; + overflow: hidden; +} + +#chart-toggle, +#book-toggle { + margin: 0 !important; + padding: 15px 17px !important; + border-bottom: 1px solid var(--terminal-line) !important; + background: rgba(255, 255, 255, 0.018); +} + +#chart-toggle:hover, +#book-toggle:hover { + border-radius: 0 !important; + background: rgba(98, 189, 255, 0.075) !important; +} + +#chart-toggle .toggle-label, +#book-toggle .toggle-label { + color: var(--terminal-muted) !important; + font-size: 11px !important; + font-weight: 900; + letter-spacing: 0.17em !important; +} + +#chart-toggle .toggle-label::before, +#book-toggle .toggle-label::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + margin-right: 8px; + border-radius: 50%; + background: var(--terminal-blue); + box-shadow: 0 0 12px rgba(98, 189, 255, 0.72); +} + +#book-toggle .toggle-label::before { + background: var(--terminal-amber); + box-shadow: 0 0 12px rgba(245, 189, 79, 0.72); +} + +#chart-toggle .toggle-arrow, +#book-toggle .toggle-arrow { + color: var(--terminal-muted) !important; +} + +#chart-collapsible { + max-height: 620px !important; +} + +#chart-collapsible.collapsed { + max-height: 0 !important; +} + +#price-chart { + margin: 12px !important; + overflow: hidden; + border: 1px solid rgba(151, 166, 188, 0.12) !important; + border-radius: 22px; + background: + radial-gradient(circle at 68% 14%, rgba(98, 189, 255, 0.13), transparent 34%), + linear-gradient(180deg, rgba(7, 12, 20, 0.92), rgba(4, 7, 11, 0.94)) !important; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.045); +} + +#price-chart canvas { + height: min(54vh, 560px) !important; + min-height: 400px; +} + +#book-collapsible { + max-height: 720px !important; + padding: 0 12px 14px; +} + +#book-collapsible.collapsed { + max-height: 0 !important; + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +#momentum-bar, +#spread-bar { + border-color: var(--terminal-line) !important; + color: var(--terminal-muted) !important; +} + +#momentum-bar { + margin-top: 10px; + padding: 9px 10px !important; + border: 1px solid var(--terminal-line); + border-radius: 15px; + background: rgba(255, 255, 255, 0.035); +} + +.mom-label, +.mom-name { + color: var(--terminal-faint) !important; +} + +.mom-val { + font-family: var(--terminal-num); +} + +.mom-val.up { color: var(--terminal-green) !important; } +.mom-val.down { color: var(--terminal-red) !important; } +.mom-val.flat { color: var(--terminal-faint) !important; } + +.book-header { + margin-top: 8px; + padding: 9px 10px !important; + color: var(--terminal-faint) !important; + font-weight: 900; +} + +.order-row { + min-height: 27px; + align-items: center; + margin: 2px 0; + padding: 5px 10px !important; + border: 1px solid transparent; + border-radius: 11px !important; + background: rgba(255, 255, 255, 0.025); + font-family: var(--terminal-num); +} + +.order-row:hover { + border-color: rgba(151, 166, 188, 0.16); + background: rgba(255, 255, 255, 0.05); +} + +.order-row.ask .depth-bar { + background: linear-gradient(90deg, transparent, rgba(255, 100, 116, 0.58)) !important; + opacity: 0.2 !important; +} + +.order-row.bid .depth-bar { + background: linear-gradient(90deg, rgba(46, 230, 166, 0.58), transparent) !important; + opacity: 0.2 !important; +} + +.order-row .qty { + color: var(--terminal-muted) !important; +} + +#spread-bar { + margin: 8px 0 !important; + padding: 10px !important; + border: 1px solid var(--terminal-line) !important; + border-radius: 15px; + background: rgba(255, 255, 255, 0.035); +} + +#prob-bar { + padding: 12px 2px 0 !important; +} + +.prob-label { + color: var(--terminal-muted) !important; +} + +.prob-val { + font-family: var(--terminal-num); + font-size: 24px !important; +} + +#prob-track { + height: 10px !important; + border: 1px solid rgba(151, 166, 188, 0.12); + border-radius: 999px; + background: rgba(255, 255, 255, 0.06) !important; +} + +#prob-fill-up { + background: linear-gradient(90deg, var(--terminal-green), var(--terminal-cyan), var(--terminal-amber)) !important; + box-shadow: 0 0 18px rgba(65, 232, 255, 0.25); +} + +/* Order Panel */ +#order-panel { + position: sticky; + top: 94px; + z-index: 60; + width: auto !important; + padding: 16px !important; + gap: 12px !important; + overflow: visible !important; +} + +#order-panel:has(.allowance-wrap:hover) { + z-index: 7001; +} + +#order-panel .panel-title { + color: var(--terminal-ink) !important; + font-size: 15px !important; + font-weight: 900; + letter-spacing: 0.14em !important; +} + +#order-panel > div:first-child { + padding-bottom: 10px; + border-bottom: 1px solid var(--terminal-line); +} + +#account-block { + overflow: visible !important; + padding: 12px !important; + border: 1px solid rgba(151, 166, 188, 0.14); + border-radius: 19px !important; + background: rgba(255, 255, 255, 0.04) !important; +} + +.acct-item { + gap: 8px; + min-height: 29px; +} + +.acct-label, +.acct-sub, +#sync-time { + color: var(--terminal-faint) !important; +} + +.acct-val { + font-family: var(--terminal-num); + color: var(--terminal-ink); +} + +.verified-tag { + border: 1px solid rgba(151, 166, 188, 0.14); + border-radius: 999px !important; + background: rgba(255, 255, 255, 0.045) !important; + color: var(--terminal-faint) !important; +} + +.verified-tag.ok { + border-color: rgba(46, 230, 166, 0.28); + background: rgba(46, 230, 166, 0.1) !important; + color: var(--terminal-green) !important; +} + +.btn-group { + gap: 9px !important; +} + +.tog-btn, +.side-btn, +.quick-amt-btn, +.pm-refresh-btn, +.max-btn, +.step-btn, +#clear-all-btn, +#confirm-btn, +#limit-confirm-btn { + border-radius: 14px !important; + transition: transform 0.15s ease, border-color 0.15s ease, background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease; +} + +.tog-btn, +.side-btn, +.quick-amt-btn { + border-color: rgba(151, 166, 188, 0.18) !important; + background: rgba(255, 255, 255, 0.055) !important; + color: var(--terminal-muted) !important; +} + +.tog-btn:hover, +.side-btn:hover, +.quick-amt-btn:hover, +.pm-refresh-btn:hover, +.max-btn:hover, +.step-btn:hover { + transform: translateY(-1px); +} + +.tog-btn.active { + border-color: rgba(98, 189, 255, 0.55) !important; + background: rgba(98, 189, 255, 0.13) !important; + color: var(--terminal-blue) !important; + box-shadow: 0 0 0 3px rgba(98, 189, 255, 0.08); +} + +.tog-btn.active-up { + border-color: rgba(46, 230, 166, 0.54) !important; + background: rgba(46, 230, 166, 0.13) !important; + color: var(--terminal-green) !important; +} + +.tog-btn.active-down { + border-color: rgba(255, 100, 116, 0.54) !important; + background: rgba(255, 100, 116, 0.13) !important; + color: var(--terminal-red) !important; +} + +.side-btn.active-buy { + border-color: rgba(98, 189, 255, 0.5) !important; + background: linear-gradient(135deg, rgba(98, 189, 255, 0.28), rgba(98, 189, 255, 0.09)) !important; + color: #c7eaff !important; +} + +.side-btn.active-sell { + border-color: rgba(245, 189, 79, 0.5) !important; + background: linear-gradient(135deg, rgba(245, 189, 79, 0.3), rgba(245, 189, 79, 0.08)) !important; + color: #ffe4aa !important; +} + +.dir-pct, +#amount-unit { + font-family: var(--terminal-num); +} + +#amount-unit { + color: var(--terminal-faint) !important; +} + +.input-row { + border-color: rgba(151, 166, 188, 0.18) !important; + border-radius: 15px !important; + background: rgba(255, 255, 255, 0.04) !important; +} + +.input-row:focus-within { + border-color: rgba(98, 189, 255, 0.65) !important; + box-shadow: 0 0 0 3px rgba(98, 189, 255, 0.1); +} + +.input-row input { + background: transparent !important; + color: var(--terminal-ink) !important; + font-family: var(--terminal-num); +} + +.step-btn, +.max-btn { + background: rgba(255, 255, 255, 0.055) !important; + color: var(--terminal-muted) !important; +} + +.quick-amt-btn { + min-height: 28px; +} + +.quick-amt-btn:hover, +.quick-amt-btn.active { + border-color: rgba(245, 189, 79, 0.5) !important; + background: rgba(245, 189, 79, 0.1) !important; + color: var(--terminal-amber) !important; +} + +#est-row, +#pos-summary, +#cond-panel, +#slippage-row, +#manual-limit-list { + border: 1px solid rgba(151, 166, 188, 0.12) !important; + border-radius: 15px !important; + background: rgba(255, 255, 255, 0.035) !important; +} + +#est-row { + padding: 9px 11px !important; + color: var(--terminal-muted) !important; +} + +#est-row span { + color: var(--terminal-ink) !important; + font-family: var(--terminal-num); +} + +#confirm-btn, +#limit-confirm-btn { + min-height: 52px; + border: 1px solid rgba(151, 166, 188, 0.16) !important; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.035)) !important; + color: var(--terminal-muted) !important; + letter-spacing: 0.03em; +} + +#confirm-btn.ready-buy, +#limit-confirm-btn.ready-buy { + border-color: rgba(46, 230, 166, 0.45) !important; + background: linear-gradient(135deg, rgba(46, 230, 166, 0.96), rgba(18, 129, 91, 0.96)) !important; + color: #04130e !important; + box-shadow: 0 14px 38px rgba(46, 230, 166, 0.22); +} + +#confirm-btn.ready-sell, +#limit-confirm-btn.ready-sell { + border-color: rgba(245, 189, 79, 0.48) !important; + background: linear-gradient(135deg, rgba(245, 189, 79, 0.96), rgba(164, 88, 28, 0.96)) !important; + color: #170d04 !important; + box-shadow: 0 14px 38px rgba(245, 189, 79, 0.2); +} + +#confirm-btn:not(:disabled):hover, +#limit-confirm-btn:not(:disabled):hover, +#clear-all-btn:hover:not(:disabled) { + transform: translateY(-1px); +} + +#clear-all-btn { + border-color: rgba(255, 100, 116, 0.3) !important; + background: rgba(255, 100, 116, 0.08) !important; + color: var(--terminal-red) !important; +} + +#clear-all-btn:hover:not(:disabled) { + border-color: rgba(255, 100, 116, 0.5) !important; + background: rgba(255, 100, 116, 0.13) !important; +} + +#cond-panel, +#slippage-row { + padding: 9px 10px; +} + +#cond-head { + color: var(--terminal-muted) !important; +} + +#cond-body { + border-left-color: var(--terminal-line) !important; +} + +/* Floating Surfaces */ +.result-dot .dot-tip, +.allowance-tooltip, +.strat-tip .tip-box, +.tg-help-tip, +.order-latency-tip, +.order-latency-tip > div, +#tg-config-body { + z-index: 100000 !important; + border-color: var(--terminal-line-strong) !important; + background: rgba(7, 12, 20, 0.96) !important; + box-shadow: 0 22px 70px rgba(0, 0, 0, 0.56) !important; + backdrop-filter: none; +} + +.result-dot, +.allowance-wrap, +.strat-tip, +.tg-help-trigger, +.order-latency-trigger { + position: relative; +} + +.result-dot:hover, +.allowance-wrap:hover, +.strat-tip:hover, +.tg-help-trigger:hover, +.order-latency-trigger:hover { + z-index: 100001; +} + +.result-dot .dot-tip, +.allowance-tooltip, +.strat-tip .tip-box { + z-index: 100002 !important; +} + +#tg-config-body { + width: min(460px, calc(100vw - 32px)) !important; + max-height: min(620px, calc(100vh - 120px)); + overflow-y: auto; + overscroll-behavior: contain; + z-index: 100003 !important; +} + +#tg-config-panel { + position: relative; + z-index: 100004; +} + +.tg-help-tip { + z-index: 100005 !important; + pointer-events: auto; +} + +#claim-tip { + position: relative; +} + +#claim-tip:hover, +#claim-tip:has(.tg-help-tip:hover) { + z-index: 100010 !important; +} + +#claim-tip::after { + left: auto !important; + right: 0 !important; + width: min(340px, calc(100vw - 32px)); + height: 18px !important; + top: 100% !important; + z-index: 100009; +} + +#claim-tip .tg-help-tip { + z-index: 100011 !important; +} + +#tg-bot-token, +#tg-chat-id, +#vol-threshold, +.strat-amt, +#max-round-entries, +#strat-slippage, +#tg-interval, +#cond-tp-pct, +#cond-sl-pct, +#cond-sl-slippage, +#slippage-input { + border-color: rgba(151, 166, 188, 0.22) !important; + background: rgba(255, 255, 255, 0.05) !important; + color: var(--terminal-ink) !important; +} + +#backtest-btn, +#tg-config-body button, +#cond-cancel-all-btn { + border-color: rgba(151, 166, 188, 0.22) !important; + background: rgba(255, 255, 255, 0.06) !important; + color: var(--terminal-muted) !important; +} + +/* PnL Table */ +#pm-pnl { + margin-top: 14px !important; + margin-bottom: 16px !important; + padding: 14px !important; + overflow-x: auto; +} + +#pm-pnl .hist-title { + margin-bottom: 10px !important; + color: var(--terminal-muted) !important; + font-size: 12px !important; + font-weight: 900; + letter-spacing: 0.14em !important; +} + +#pm-pnl .pm-total { + color: var(--terminal-muted) !important; + font-family: var(--terminal-num); +} + +#pm-pnl .pm-total .val-pos { color: var(--terminal-green) !important; } +#pm-pnl .pm-total .val-neg { color: var(--terminal-red) !important; } + +.pm-refresh-btn { + padding: 6px 12px !important; + border-color: rgba(151, 166, 188, 0.18) !important; + background: rgba(255, 255, 255, 0.055) !important; + color: var(--terminal-muted) !important; +} + +.pm-refresh-btn:hover { + border-color: rgba(98, 189, 255, 0.5) !important; + background: rgba(98, 189, 255, 0.12) !important; + color: #d9f1ff !important; +} + +.pm-countdown { + color: var(--terminal-faint) !important; +} + +#pm-pnl-list { + gap: 5px !important; + height: min(42vh, 420px) !important; +} + +.pm-item { + min-width: 920px; + padding: 7px 9px !important; + border: 1px solid rgba(151, 166, 188, 0.08); + border-radius: 12px !important; + background: rgba(255, 255, 255, 0.035) !important; + color: var(--terminal-muted) !important; + font-family: var(--terminal-num); +} + +.pm-item:hover { + border-color: rgba(151, 166, 188, 0.17); + background: rgba(255, 255, 255, 0.055) !important; +} + +.pm-header { + border-color: var(--terminal-line) !important; + background: transparent !important; +} + +.pm-header > span { + color: var(--terminal-faint) !important; +} + +.pm-item .pm-time, +.pm-item .pm-detail, +.pm-item .pm-title, +.pm-item .pm-amount .pm-amount-label, +.pm-item .pm-pos-pnl .pm-pos-pnl-label { + color: var(--terminal-faint) !important; +} + +.pm-item .pm-kind.buy, +.pm-item .pm-outcome.up { + background: rgba(46, 230, 166, 0.12) !important; + color: var(--terminal-green) !important; +} + +.pm-item .pm-kind.sell, +.pm-item .pm-outcome.down { + background: rgba(255, 100, 116, 0.12) !important; + color: var(--terminal-red) !important; +} + +.pm-item .pm-kind.claim { + background: rgba(245, 189, 79, 0.13) !important; + color: var(--terminal-amber) !important; +} + +.pm-item .pm-source { + background: rgba(98, 189, 255, 0.12) !important; + color: var(--terminal-blue) !important; +} + +.pm-item .pm-source.manual { + background: rgba(245, 189, 79, 0.13) !important; + color: var(--terminal-amber) !important; +} + +#app-footer { + margin-top: 0 !important; + margin-bottom: 16px !important; + border: 1px solid var(--terminal-line) !important; + border-radius: 16px; + background: rgba(255, 255, 255, 0.025); + color: var(--terminal-faint) !important; +} + +/* Low Data Mode */ +body.low-data-mode { + background: + radial-gradient(circle at 50% -10%, rgba(98, 189, 255, 0.18), transparent 30rem), + linear-gradient(135deg, #04070b, #071018 58%, #06080c) !important; +} + +body.low-data-mode #header, +body.low-data-mode #price-bar, +body.low-data-mode #strat-bar, +body.low-data-mode #observe-bar { + width: min(980px, calc(100% - 32px)) !important; + max-width: none; +} + +body.low-data-mode #header { + padding: 12px 16px !important; +} + +body.low-data-mode #price-bar { + display: block !important; + margin-top: 12px !important; + border: 1px solid var(--terminal-line) !important; + border-radius: var(--terminal-radius-lg) !important; + background: var(--terminal-card-strong) !important; + box-shadow: var(--terminal-shadow); +} + +body.low-data-mode #low-data-hero { + padding: 24px 24px 10px !important; +} + +body.low-data-mode #low-data-detail { + padding: 10px 24px 18px !important; + border-top-color: var(--terminal-line) !important; + color: var(--terminal-muted) !important; +} + +/* Responsive */ +@media (max-width: 1320px) { + #price-bar { + grid-template-columns: repeat(3, minmax(180px, 1fr)); + } + + #main-wrap { + grid-template-columns: 1fr !important; + } + + #order-panel { + position: static; + width: 100% !important; + } +} + +@media (max-width: 860px) { + #header, + #price-bar, + #strat-bar, + #observe-bar, + #main-wrap, + #pm-pnl, + #app-footer { + width: calc(100% - 20px); + } + + #header { + position: static; + margin-top: 10px !important; + border-radius: 20px !important; + } + + #header .window-info { + width: 100%; + } + + #recent-results { + margin-left: 0; + order: 3; + } + + #ws-status { + order: 5; + width: 100%; + } + + .mode-toggle { + order: 4; + } + + #price-bar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .price-item, + .price-item:nth-child(1), + .price-item:nth-child(2) { + min-height: 90px; + padding: 14px !important; + } + + #price-chart canvas { + min-height: 320px; + height: 44vh !important; + } + + #momentum-bar { + flex-direction: column; + } + + .mom-group { + width: 100%; + } + + #prob-bar { + gap: 12px !important; + } +} + +@media (max-width: 560px) { + body { + font-size: 12px; + } + + #price-bar { + grid-template-columns: 1fr; + } + + #book-collapsible { + padding: 0 8px 10px; + } + + #price-chart { + margin: 8px !important; + } + + #price-chart canvas { + min-height: 280px; + } + + #order-panel { + padding: 12px !important; + } + + .tog-btn, + .side-btn { + font-size: 13px !important; + } + + #low-data-hero { + flex-direction: column; + align-items: flex-start !important; + } + + body.low-data-mode #header, + body.low-data-mode #price-bar, + body.low-data-mode #strat-bar, + body.low-data-mode #observe-bar { + width: calc(100% - 20px) !important; + } +} diff --git a/tg-push.ts b/tg-push.ts new file mode 100644 index 0000000..8125b0d --- /dev/null +++ b/tg-push.ts @@ -0,0 +1,115 @@ +/** + * Telegram push module + * + * Features: + * - Scheduled push of account status (balance, PnL, enabled strategies, latest 5 trades) + * - Bot Token / Chat ID / frequency configurable from the frontend + * - Config persisted to .tg-config.json + */ + +import { readFileSync, writeFileSync, existsSync } from "fs"; +import { resolve, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TG_CONFIG_FILE = resolve(__dirname, ".tg-config.json"); + +export interface TgConfig { + enabled: boolean; // Master switch: when off, no push is sent at all + botToken: string; + chatId: string; + intervalMinutes: number; // Scheduled push frequency (minutes), minimum 5 + scheduledEnabled: boolean; // Whether to enable scheduled push + postTradeEnabled: boolean; // Whether to enable the "10 minutes after buy MINED" push +} + +const DEFAULT_CONFIG: TgConfig = { + enabled: false, + botToken: "", + chatId: "", + intervalMinutes: 60, + scheduledEnabled: true, + postTradeEnabled: false, +}; + +export function loadTgConfig(): TgConfig { + try { + if (!existsSync(TG_CONFIG_FILE)) return { ...DEFAULT_CONFIG }; + const raw = JSON.parse(readFileSync(TG_CONFIG_FILE, "utf-8")); + return { + enabled: !!raw.enabled, + botToken: typeof raw.botToken === "string" ? raw.botToken : "", + chatId: typeof raw.chatId === "string" ? raw.chatId : "", + intervalMinutes: typeof raw.intervalMinutes === "number" && raw.intervalMinutes >= 5 + ? raw.intervalMinutes + : 60, + // Backward compatibility: when an old config lacks these two fields, scheduled push defaults to on (keeps original behavior), post-trade push defaults to off + scheduledEnabled: typeof raw.scheduledEnabled === "boolean" ? raw.scheduledEnabled : true, + postTradeEnabled: typeof raw.postTradeEnabled === "boolean" ? raw.postTradeEnabled : false, + }; + } catch (err) { + console.warn(`[TG] Failed to load config: ${err instanceof Error ? err.message : String(err)}`); + return { ...DEFAULT_CONFIG }; + } +} + +export function saveTgConfig(cfg: TgConfig): void { + try { + writeFileSync(TG_CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf-8"); + } catch (err) { + console.warn(`[TG] Failed to save config: ${err instanceof Error ? err.message : String(err)}`); + } +} + +/** Auto-detect Chat ID via getUpdates (only available after the user has sent the bot a message) */ +export async function autoDetectChatId(botToken: string): Promise<{ ok: boolean; chatId?: string; error?: string }> { + if (!botToken) return { ok: false, error: "Bot Token is empty" }; + const url = `https://api.telegram.org/bot${botToken}/getUpdates`; + try { + const res = await fetch(url); + const data = await res.json() as { ok: boolean; result?: Array<{ message?: { chat?: { id?: number } } }>; description?: string }; + if (!data.ok) { + return { ok: false, error: data.description || `HTTP ${res.status}` }; + } + const results = data.result || []; + if (results.length === 0) { + return { ok: false, error: "No messages found. Please first send the bot a message in Telegram (e.g. /start), then try again" }; + } + // Take the chat.id of the last message + for (let i = results.length - 1; i >= 0; i--) { + const id = results[i].message?.chat?.id; + if (typeof id === "number") { + return { ok: true, chatId: String(id) }; + } + } + return { ok: false, error: "chat.id not found in messages" }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} + +/** Send a text message to Telegram */ +export async function sendTgMessage(cfg: TgConfig, text: string): Promise<{ ok: boolean; error?: string }> { + if (!cfg.botToken || !cfg.chatId) { + return { ok: false, error: "Bot Token or Chat ID not configured" }; + } + const url = `https://api.telegram.org/bot${cfg.botToken}/sendMessage`; + try { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + chat_id: cfg.chatId, + text, + disable_web_page_preview: true, + }), + }); + const data = await res.json() as { ok: boolean; description?: string }; + if (!data.ok) { + return { ok: false, error: data.description || `HTTP ${res.status}` }; + } + return { ok: true }; + } catch (err) { + return { ok: false, error: err instanceof Error ? err.message : String(err) }; + } +} diff --git a/tunnels/tunnel-arl-2t4.sh b/tunnels/tunnel-arl-2t4.sh new file mode 100755 index 0000000..7c71f5f --- /dev/null +++ b/tunnels/tunnel-arl-2t4.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Polymarket VPS SSH tunnel management (autossh auto-reconnect) +# +# Usage: +# ./tunnel.sh foreground start (default; Ctrl+C to exit = close tunnel; output shown directly) +# ./tunnel.sh bg background start (tunnel keeps running after closing the terminal, log at /tmp/pm-tunnel.log) +# ./tunnel.sh down close the background tunnel +# ./tunnel.sh status check running status +# ./tunnel.sh logs tail the background log +# +# Multiple accounts: edit the PORTS array below and add more ports + +# ── Config ───────────────────────────────────────────── +PEM="$HOME/Desktop/arl.pem" +HOST=ubuntu@34.249.106.114 +PORTS=(3556 3557 3558 3559 3560 3561 3562) # 6 accounts + +# ── Internal ───────────────────────────────────────────── +# LOG generated from the script filename (avoids multiple scripts sharing one log file) +SCRIPT_NAME=$(basename "$0" .sh) +LOG="/tmp/${SCRIPT_NAME}.log" +TAG="autossh-pm-${HOST//[@.]/_}" # use host as the identifier, for pkill / pgrep + +# Assemble port-forwarding arguments +PORT_ARGS="" +for p in "${PORTS[@]}"; do + PORT_ARGS="$PORT_ARGS -L $p:localhost:$p" +done + +cmd="${1:-up}" + +# Common: check dependencies + config +check_prereq() { + if ! command -v autossh >/dev/null 2>&1; then + echo "❌ autossh not installed, install it first:" + echo " brew install autossh" + exit 1 + fi + if [ ! -f "$PEM" ]; then + echo "❌ pem file does not exist: $PEM" + exit 1 + fi +} + +case "$cmd" in + up|fg) + # Foreground run: Ctrl+C closes the tunnel directly, output shown in real time + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "⚠ Already running in the background pid=$(pgrep -f "autossh.*$HOST" | head -1)" + echo " Run ./tunnel.sh down first, then restart in foreground; or ./tunnel.sh logs to view the background log" + exit 1 + fi + check_prereq + echo "✓ Starting tunnel (foreground mode, Ctrl+C to exit)" + echo " HOST: $HOST" + echo " Ports: ${PORTS[*]}" + echo "" + # No -f: foreground run; no log file, straight to stdout + exec autossh -M 0 -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + ;; + + bg) + # Background run: closing the terminal has no effect, log written to file + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "✓ Already running (pid=$(pgrep -f "autossh.*$HOST" | tr '\n' ' '))" + exit 0 + fi + check_prereq + AUTOSSH_LOGFILE="$LOG" \ + autossh -M 0 -f -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -E "$LOG" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + sleep 1 + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Tunnel started (background) pid=$pid" + echo " Local ports: ${PORTS[*]}" + echo " Log: $LOG (./tunnel.sh logs to view)" + else + echo "⚠ No process found after autossh started; it may have failed immediately, see $LOG" + fi + ;; + + down) + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + pkill -f "autossh.*$HOST" + pkill -f "ssh.*$HOST" 2>/dev/null + sleep 0.3 + echo "✓ Tunnel closed" + else + echo "Not running" + fi + ;; + + status) + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Running pid=$pid" + echo " Ports: ${PORTS[*]}" + # Test whether each port is actually reachable + for p in "${PORTS[@]}"; do + if curl -sf -m 2 "http://localhost:$p/api/state" >/dev/null 2>&1; then + echo " $p: ✓ reachable" + else + echo " $p: ✗ unreachable (tunnel up but remote server not started?)" + fi + done + else + echo "✗ Not running" + fi + ;; + + logs) + if [ -f "$LOG" ]; then + tail -f "$LOG" + else + echo "No log yet (tunnel has not run)" + fi + ;; + + *) + echo "Usage: $0 {up|bg|down|status|logs}" + echo " up foreground start (default; Ctrl+C to exit = close tunnel)" + echo " bg background start (closing the terminal has no effect)" + echo " down close the background tunnel" + echo " status check running status + port connectivity" + echo " logs tail the background log" + exit 1 + ;; +esac diff --git a/tunnels/tunnel-arl-large.bat b/tunnels/tunnel-arl-large.bat new file mode 100644 index 0000000..036959b --- /dev/null +++ b/tunnels/tunnel-arl-large.bat @@ -0,0 +1,5 @@ +@echo off +REM Double-click this file to start the tunnel (no need to change the Windows execution policy) +cd /d "%~dp0" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0tunnel-arl-large.ps1" +pause diff --git a/tunnels/tunnel-arl-large.ps1 b/tunnels/tunnel-arl-large.ps1 new file mode 100644 index 0000000..2e57128 --- /dev/null +++ b/tunnels/tunnel-arl-large.ps1 @@ -0,0 +1,61 @@ +# Polymarket VPS SSH tunnel (PowerShell version, auto-reconnect) +# +# Usage: +# Right-click the .ps1 → Run with PowerShell (foreground mode, closing the window = closing the tunnel) +# Or run in PowerShell: .\tunnel-arl-large.ps1 +# +# The first run may be blocked by the execution policy; open PowerShell as administrator and run once: +# Set-ExecutionPolicy -Scope CurrentUser RemoteSigned + +# ── Config (matches the .sh version) ───────────────────────────── +$PEM = "$env:USERPROFILE\Desktop\arl.pem" +$HOST_ = "ubuntu@52.51.231.134" +$PORTS = @(3556, 3557, 3558, 3559, 3560, 3561) + +# ── Internal ──────────────────────────────────────────────── + +# Assemble port-forwarding arguments +$portArgs = @() +foreach ($p in $PORTS) { + $portArgs += "-L" + $portArgs += "${p}:localhost:${p}" +} + +# pem file check +if (-not (Test-Path $PEM)) { + Write-Host "❌ pem file does not exist: $PEM" -ForegroundColor Red + exit 1 +} + +# Windows OpenSSH requires correct pem file permissions (owner-only); +# if permissions were not set before, ssh reports "WARNING: UNPROTECTED PRIVATE KEY FILE" and refuses to connect +# Auto chmod is skipped here (icacls on Windows is too complex); if you hit an error, please: +# 1. Right-click the pem file → Properties → Security → Advanced +# 2. Disable inheritance → remove other users → keep only yourself +# 3. For a detailed guide, search "windows ssh permissions are too open" + +Write-Host "✓ Starting tunnel (foreground mode, Ctrl+C to exit)" +Write-Host " HOST: $HOST_" +Write-Host " Ports: $($PORTS -join ' ')" +Write-Host "" + +# Reconnect loop: ssh exits → wait 3 seconds → reconnect +$attempt = 0 +while ($true) { + $attempt++ + Write-Host "[$([DateTime]::Now.ToString('HH:mm:ss'))] Attempting connection (count $attempt)..." -ForegroundColor Cyan + & ssh -N ` + -i $PEM ` + @portArgs ` + $HOST_ ` + -o "ServerAliveInterval=30" ` + -o "ServerAliveCountMax=3" ` + -o "ExitOnForwardFailure=yes" ` + -o "TCPKeepAlive=yes" ` + -o "StrictHostKeyChecking=no" ` + -o "UserKnownHostsFile=NUL" + + $exitCode = $LASTEXITCODE + Write-Host "[$([DateTime]::Now.ToString('HH:mm:ss'))] Tunnel disconnected (exit=$exitCode), reconnecting in 3 seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds 3 +} diff --git a/tunnels/tunnel-arl-ot.sh b/tunnels/tunnel-arl-ot.sh new file mode 100755 index 0000000..7fdd41a --- /dev/null +++ b/tunnels/tunnel-arl-ot.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Polymarket VPS SSH tunnel management (autossh auto-reconnect) +# +# Usage: +# ./tunnel.sh foreground start (default; Ctrl+C to exit = close tunnel; output shown directly) +# ./tunnel.sh bg background start (tunnel keeps running after closing the terminal, log at /tmp/pm-tunnel.log) +# ./tunnel.sh down close the background tunnel +# ./tunnel.sh status check running status +# ./tunnel.sh logs tail the background log +# +# Multiple accounts: edit the PORTS array below and add more ports + +# ── Config ───────────────────────────────────────────── +PEM="$HOME/Desktop/arl.pem" +HOST=ubuntu@34.244.122.101 +PORTS=(5000 5001 5002 5003) # 4 accounts + +# ── Internal ───────────────────────────────────────────── +# LOG generated from the script filename (avoids multiple scripts sharing one log file) +SCRIPT_NAME=$(basename "$0" .sh) +LOG="/tmp/${SCRIPT_NAME}.log" +TAG="autossh-pm-${HOST//[@.]/_}" # use host as the identifier, for pkill / pgrep + +# Assemble port-forwarding arguments +PORT_ARGS="" +for p in "${PORTS[@]}"; do + PORT_ARGS="$PORT_ARGS -L $p:localhost:$p" +done + +cmd="${1:-up}" + +# Common: check dependencies + config +check_prereq() { + if ! command -v autossh >/dev/null 2>&1; then + echo "❌ autossh not installed, install it first:" + echo " brew install autossh" + exit 1 + fi + if [ ! -f "$PEM" ]; then + echo "❌ pem file does not exist: $PEM" + exit 1 + fi +} + +case "$cmd" in + up|fg) + # Foreground run: Ctrl+C closes the tunnel directly, output shown in real time + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "⚠ Already running in the background pid=$(pgrep -f "autossh.*$HOST" | head -1)" + echo " Run ./tunnel.sh down first, then restart in foreground; or ./tunnel.sh logs to view the background log" + exit 1 + fi + check_prereq + echo "✓ Starting tunnel (foreground mode, Ctrl+C to exit)" + echo " HOST: $HOST" + echo " Ports: ${PORTS[*]}" + echo "" + # No -f: foreground run; no log file, straight to stdout + exec autossh -M 0 -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + ;; + + bg) + # Background run: closing the terminal has no effect, log written to file + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "✓ Already running (pid=$(pgrep -f "autossh.*$HOST" | tr '\n' ' '))" + exit 0 + fi + check_prereq + AUTOSSH_LOGFILE="$LOG" \ + autossh -M 0 -f -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -E "$LOG" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + sleep 1 + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Tunnel started (background) pid=$pid" + echo " Local ports: ${PORTS[*]}" + echo " Log: $LOG (./tunnel.sh logs to view)" + else + echo "⚠ No process found after autossh started; it may have failed immediately, see $LOG" + fi + ;; + + down) + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + pkill -f "autossh.*$HOST" + pkill -f "ssh.*$HOST" 2>/dev/null + sleep 0.3 + echo "✓ Tunnel closed" + else + echo "Not running" + fi + ;; + + status) + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Running pid=$pid" + echo " Ports: ${PORTS[*]}" + # Test whether each port is actually reachable + for p in "${PORTS[@]}"; do + if curl -sf -m 2 "http://localhost:$p/api/state" >/dev/null 2>&1; then + echo " $p: ✓ reachable" + else + echo " $p: ✗ unreachable (tunnel up but remote server not started?)" + fi + done + else + echo "✗ Not running" + fi + ;; + + logs) + if [ -f "$LOG" ]; then + tail -f "$LOG" + else + echo "No log yet (tunnel has not run)" + fi + ;; + + *) + echo "Usage: $0 {up|bg|down|status|logs}" + echo " up foreground start (default; Ctrl+C to exit = close tunnel)" + echo " bg background start (closing the terminal has no effect)" + echo " down close the background tunnel" + echo " status check running status + port connectivity" + echo " logs tail the background log" + exit 1 + ;; +esac diff --git a/tunnels/tunnel-arl-t4.bat b/tunnels/tunnel-arl-t4.bat new file mode 100644 index 0000000..b101233 --- /dev/null +++ b/tunnels/tunnel-arl-t4.bat @@ -0,0 +1,5 @@ +@echo off +REM Double-click this file to start the tunnel (no need to change the Windows execution policy) +cd /d "%~dp0" +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0tunnel-arl-t4.ps1" +pause diff --git a/tunnels/tunnel-arl-t4.ps1 b/tunnels/tunnel-arl-t4.ps1 new file mode 100644 index 0000000..60cfbe5 --- /dev/null +++ b/tunnels/tunnel-arl-t4.ps1 @@ -0,0 +1,61 @@ +# Polymarket VPS SSH tunnel (PowerShell version, auto-reconnect) +# +# Usage: +# Right-click the .ps1 → Run with PowerShell (foreground mode, closing the window = closing the tunnel) +# Or run in PowerShell: .\tunnel-arl-large.ps1 +# +# The first run may be blocked by the execution policy; open PowerShell as administrator and run once: +# Set-ExecutionPolicy -Scope CurrentUser RemoteSigned + +# ── Config (matches the .sh version) ───────────────────────────── +$PEM = "$env:USERPROFILE\Desktop\arl.pem" +$HOST_ = "ubuntu@3.255.100.208" +$PORTS = @(3456, 3457, 3458) + +# ── Internal ──────────────────────────────────────────────── + +# Assemble port-forwarding arguments +$portArgs = @() +foreach ($p in $PORTS) { + $portArgs += "-L" + $portArgs += "${p}:localhost:${p}" +} + +# pem file check +if (-not (Test-Path $PEM)) { + Write-Host "❌ pem file does not exist: $PEM" -ForegroundColor Red + exit 1 +} + +# Windows OpenSSH requires correct pem file permissions (owner-only); +# if permissions were not set before, ssh reports "WARNING: UNPROTECTED PRIVATE KEY FILE" and refuses to connect +# Auto chmod is skipped here (icacls on Windows is too complex); if you hit an error, please: +# 1. Right-click the pem file → Properties → Security → Advanced +# 2. Disable inheritance → remove other users → keep only yourself +# 3. For a detailed guide, search "windows ssh permissions are too open" + +Write-Host "✓ Starting tunnel (foreground mode, Ctrl+C to exit)" +Write-Host " HOST: $HOST_" +Write-Host " Ports: $($PORTS -join ' ')" +Write-Host "" + +# Reconnect loop: ssh exits → wait 3 seconds → reconnect +$attempt = 0 +while ($true) { + $attempt++ + Write-Host "[$([DateTime]::Now.ToString('HH:mm:ss'))] Attempting connection (count $attempt)..." -ForegroundColor Cyan + & ssh -N ` + -i $PEM ` + @portArgs ` + $HOST_ ` + -o "ServerAliveInterval=30" ` + -o "ServerAliveCountMax=3" ` + -o "ExitOnForwardFailure=yes" ` + -o "TCPKeepAlive=yes" ` + -o "StrictHostKeyChecking=no" ` + -o "UserKnownHostsFile=NUL" + + $exitCode = $LASTEXITCODE + Write-Host "[$([DateTime]::Now.ToString('HH:mm:ss'))] Tunnel disconnected (exit=$exitCode), reconnecting in 3 seconds..." -ForegroundColor Yellow + Start-Sleep -Seconds 3 +} diff --git a/tunnels/tunnel-arl-t4.sh b/tunnels/tunnel-arl-t4.sh new file mode 100755 index 0000000..9e9147c --- /dev/null +++ b/tunnels/tunnel-arl-t4.sh @@ -0,0 +1,147 @@ +#!/bin/bash +# Polymarket VPS SSH tunnel management (autossh auto-reconnect) +# +# Usage: +# ./tunnel.sh foreground start (default; Ctrl+C to exit = close tunnel; output shown directly) +# ./tunnel.sh bg background start (tunnel keeps running after closing the terminal, log at /tmp/pm-tunnel.log) +# ./tunnel.sh down close the background tunnel +# ./tunnel.sh status check running status +# ./tunnel.sh logs tail the background log +# +# Multiple accounts: edit the PORTS array below and add more ports + +# ── Config ───────────────────────────────────────────── +PEM="$HOME/Desktop/arl.pem" +HOST=ubuntu@3.255.100.208 +PORTS=(3456 3457 3458 3459 3460) # 3 accounts + +# ── Internal ───────────────────────────────────────────── +# LOG generated from the script filename (avoids multiple scripts sharing one log file) +SCRIPT_NAME=$(basename "$0" .sh) +LOG="/tmp/${SCRIPT_NAME}.log" +TAG="autossh-pm-${HOST//[@.]/_}" # use host as the identifier, for pkill / pgrep + +# Assemble port-forwarding arguments +PORT_ARGS="" +for p in "${PORTS[@]}"; do + PORT_ARGS="$PORT_ARGS -L $p:localhost:$p" +done + +cmd="${1:-up}" + +# Common: check dependencies + config +check_prereq() { + if ! command -v autossh >/dev/null 2>&1; then + echo "❌ autossh not installed, install it first:" + echo " brew install autossh" + exit 1 + fi + if [ ! -f "$PEM" ]; then + echo "❌ pem file does not exist: $PEM" + exit 1 + fi +} + +case "$cmd" in + up|fg) + # Foreground run: Ctrl+C closes the tunnel directly, output shown in real time + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "⚠ Already running in the background pid=$(pgrep -f "autossh.*$HOST" | head -1)" + echo " Run ./tunnel.sh down first, then restart in foreground; or ./tunnel.sh logs to view the background log" + exit 1 + fi + check_prereq + echo "✓ Starting tunnel (foreground mode, Ctrl+C to exit)" + echo " HOST: $HOST" + echo " Ports: ${PORTS[*]}" + echo "" + # No -f: foreground run; no log file, straight to stdout + exec autossh -M 0 -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + ;; + + bg) + # Background run: closing the terminal has no effect, log written to file + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + echo "✓ Already running (pid=$(pgrep -f "autossh.*$HOST" | tr '\n' ' '))" + exit 0 + fi + check_prereq + AUTOSSH_LOGFILE="$LOG" \ + autossh -M 0 -f -N \ + -i "$PEM" \ + $PORT_ARGS \ + "$HOST" \ + -E "$LOG" \ + -o "ServerAliveInterval=30" \ + -o "ServerAliveCountMax=3" \ + -o "ExitOnForwardFailure=yes" \ + -o "TCPKeepAlive=yes" \ + -o "StrictHostKeyChecking=no" \ + -o "UserKnownHostsFile=/dev/null" + sleep 1 + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Tunnel started (background) pid=$pid" + echo " Local ports: ${PORTS[*]}" + echo " Log: $LOG (./tunnel.sh logs to view)" + else + echo "⚠ No process found after autossh started; it may have failed immediately, see $LOG" + fi + ;; + + down) + if pgrep -f "autossh.*$HOST" >/dev/null 2>&1; then + pkill -f "autossh.*$HOST" + pkill -f "ssh.*$HOST" 2>/dev/null + sleep 0.3 + echo "✓ Tunnel closed" + else + echo "Not running" + fi + ;; + + status) + pid=$(pgrep -f "autossh.*$HOST" | head -1) + if [ -n "$pid" ]; then + echo "✓ Running pid=$pid" + echo " Ports: ${PORTS[*]}" + # Test whether each port is actually reachable + for p in "${PORTS[@]}"; do + if curl -sf -m 2 "http://localhost:$p/api/state" >/dev/null 2>&1; then + echo " $p: ✓ reachable" + else + echo " $p: ✗ unreachable (tunnel up but remote server not started?)" + fi + done + else + echo "✗ Not running" + fi + ;; + + logs) + if [ -f "$LOG" ]; then + tail -f "$LOG" + else + echo "No log yet (tunnel has not run)" + fi + ;; + + *) + echo "Usage: $0 {up|bg|down|status|logs}" + echo " up foreground start (default; Ctrl+C to exit = close tunnel)" + echo " bg background start (closing the terminal has no effect)" + echo " down close the background tunnel" + echo " status check running status + port connectivity" + echo " logs tail the background log" + exit 1 + ;; +esac